Polish 'Add ANSI 8-bit color support'

See gh-18264
This commit is contained in:
Phillip Webb 2019-09-21 13:03:44 -07:00
parent 65a27ef6d6
commit 7f79c26b6b
12 changed files with 272 additions and 304 deletions

View File

@ -28,7 +28,6 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ansi.Ansi256PropertySource;
import org.springframework.boot.ansi.AnsiPropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
@ -82,7 +81,6 @@ public class ResourceBanner implements Banner {
resolvers.add(environment);
resolvers.add(getVersionResolver(sourceClass));
resolvers.add(getAnsiResolver());
resolvers.add(getAnsi256Resolver());
resolvers.add(getTitleResolver(sourceClass));
return resolvers;
}
@ -126,12 +124,6 @@ public class ResourceBanner implements Banner {
return new PropertySourcesPropertyResolver(sources);
}
private PropertyResolver getAnsi256Resolver() {
MutablePropertySources sources = new MutablePropertySources();
sources.addFirst(new Ansi256PropertySource("ansi256"));
return new PropertySourcesPropertyResolver(sources);
}
private PropertyResolver getTitleResolver(Class<?> sourceClass) {
MutablePropertySources sources = new MutablePropertySources();
String applicationTitle = getApplicationTitle(sourceClass);

View File

@ -1,84 +0,0 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ansi;
/**
* {@link AnsiElement} implementation for Ansi 256 colors.
* <p>
* use {@link Ansi256Color.Foreground} or {@link Ansi256Color.Background} as a concrete
* class.
*
* @author Toshiaki Maki
* @since 2.2.0
*/
public abstract class Ansi256Color implements AnsiElement {
/**
* color code
*/
final int colorCode;
/**
* @param colorCode color code (must be 0-255)
* @throws IllegalArgumentException if color code is not between 0 and 255.
*/
Ansi256Color(int colorCode) {
if (colorCode < 0 || colorCode > 255) {
throw new IllegalArgumentException("'colorCode' must be between 0 and 255.");
}
this.colorCode = colorCode;
}
/**
* {@link Ansi256Color} foreground colors.
*
* @author Toshiaki Maki
* @since 2.2.0
*/
public static class Foreground extends Ansi256Color {
public Foreground(int colorCode) {
super(colorCode);
}
@Override
public String toString() {
return "38;5;" + super.colorCode;
}
}
/**
* {@link Ansi256Color} background colors.
*
* @author Toshiaki Maki
* @since 2.2.0
*/
public static class Background extends Ansi256Color {
public Background(int colorCode) {
super(colorCode);
}
@Override
public String toString() {
return "48;5;" + super.colorCode;
}
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ansi;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
/**
* {@link PropertyResolver} for {@link Ansi256Color.Background} and
* {@link Ansi256Color.Foreground} elements. Supports properties of the form
* {@code Ansi256Color.Foreground_N} and {@code Ansi256Color.Background_N} ({@code N} must
* be between 0 and 255).
*
* @author Toshiaki Maki
* @since 2.2.0
*/
public class Ansi256PropertySource extends PropertySource<AnsiElement> {
private static final String PREFIX = "Ansi256Color.";
private static final String FOREGROUND_PREFIX = PREFIX + "Foreground_";
private static final String BACKGROUND_PREFIX = PREFIX + "Background_";
/**
* Create a new {@link Ansi256PropertySource} instance.
* @param name the name of the property source
*/
public Ansi256PropertySource(String name) {
super(name);
}
@Override
public Object getProperty(String name) {
if (StringUtils.hasLength(name)) {
if (name.startsWith(FOREGROUND_PREFIX)) {
final int colorCode = Integer.parseInt(name.substring(FOREGROUND_PREFIX.length()));
return AnsiOutput.encode(new Ansi256Color.Foreground(colorCode));
}
else if (name.startsWith(BACKGROUND_PREFIX)) {
final int colorCode = Integer.parseInt(name.substring(BACKGROUND_PREFIX.length()));
return AnsiOutput.encode(new Ansi256Color.Background(colorCode));
}
}
return null;
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ansi;
import org.springframework.util.Assert;
/**
* {@link AnsiElement} implementation for ANSI 8-bit foreground or background color codes.
*
* @author Toshiaki Maki
* @author Phillip Webb
* @since 2.2.0
* @see #foreground(int)
* @see #background(int)
*/
public final class Ansi8BitColor implements AnsiElement {
private final String prefix;
private final int code;
/**
* Create a new {@link Ansi8BitColor} instance.
* @param prefix the prefix escape chars
* @param code color code (must be 0-255)
* @throws IllegalArgumentException if color code is not between 0 and 255.
*/
private Ansi8BitColor(String prefix, int code) {
Assert.isTrue(code >= 0 && code <= 255, "Code must be between 0 and 255");
this.prefix = prefix;
this.code = code;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Ansi8BitColor other = (Ansi8BitColor) obj;
return this.prefix.equals(other.prefix) && this.code == other.code;
}
@Override
public int hashCode() {
return this.prefix.hashCode() * 31 + this.code;
}
@Override
public String toString() {
return this.prefix + this.code;
}
/**
* Return a foreground ANSI color code instance for the given code.
* @param code the color code
* @return an ANSI color code instance
*/
public static Ansi8BitColor foreground(int code) {
return new Ansi8BitColor("38;5;", code);
}
/**
* Return a background ANSI color code instance for the given code.
* @param code the color code
* @return an ANSI color code instance
*/
public static Ansi8BitColor background(int code) {
return new Ansi8BitColor("48;5;", code);
}
}

View File

@ -21,34 +21,43 @@ import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.function.IntFunction;
import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
import org.springframework.util.StringUtils;
/**
* {@link PropertyResolver} for {@link AnsiStyle}, {@link AnsiColor} and
* {@link AnsiBackground} elements. Supports properties of the form
* {@code AnsiStyle.BOLD}, {@code AnsiColor.RED} or {@code AnsiBackground.GREEN}. Also
* supports a prefix of {@code Ansi.} which is an aggregation of everything (with
* {@link PropertyResolver} for {@link AnsiStyle}, {@link AnsiColor},
* {@link AnsiBackground} and {@link Ansi8BitColor} elements. Supports properties of the
* form {@code AnsiStyle.BOLD}, {@code AnsiColor.RED} or {@code AnsiBackground.GREEN}.
* Also supports a prefix of {@code Ansi.} which is an aggregation of everything (with
* background colors prefixed {@code BG_}).
* <p>
* ANSI 8-bit color codes can be used with {@code AnsiColor} and {@code AnsiBackground}.
* For example, {@code AnsiColor.208} will render orange text.
* <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia</a> has a complete
* list of the 8-bit color codes that can be used.
*
* @author Phillip Webb
* @author Toshiaki Maki
* @since 1.3.0
*/
public class AnsiPropertySource extends PropertySource<AnsiElement> {
private static final Iterable<MappedEnum<?>> MAPPED_ENUMS;
private static final Iterable<Mapping> MAPPINGS;
static {
List<MappedEnum<?>> enums = new ArrayList<>();
enums.add(new MappedEnum<>("AnsiStyle.", AnsiStyle.class));
enums.add(new MappedEnum<>("AnsiColor.", AnsiColor.class));
enums.add(new MappedEnum<>("AnsiBackground.", AnsiBackground.class));
enums.add(new MappedEnum<>("Ansi.", AnsiStyle.class));
enums.add(new MappedEnum<>("Ansi.", AnsiColor.class));
enums.add(new MappedEnum<>("Ansi.BG_", AnsiBackground.class));
MAPPED_ENUMS = Collections.unmodifiableList(enums);
List<Mapping> mappings = new ArrayList<>();
mappings.add(new EnumMapping<>("AnsiStyle.", AnsiStyle.class));
mappings.add(new EnumMapping<>("AnsiColor.", AnsiColor.class));
mappings.add(new Ansi8BitColorMapping("AnsiColor.", Ansi8BitColor::foreground));
mappings.add(new EnumMapping<>("AnsiBackground.", AnsiBackground.class));
mappings.add(new Ansi8BitColorMapping("AnsiBackground.", Ansi8BitColor::background));
mappings.add(new EnumMapping<>("Ansi.", AnsiStyle.class));
mappings.add(new EnumMapping<>("Ansi.", AnsiColor.class));
mappings.add(new EnumMapping<>("Ansi.BG_", AnsiBackground.class));
MAPPINGS = Collections.unmodifiableList(mappings);
}
private final boolean encode;
@ -66,16 +75,13 @@ public class AnsiPropertySource extends PropertySource<AnsiElement> {
@Override
public Object getProperty(String name) {
if (StringUtils.hasLength(name)) {
for (MappedEnum<?> mappedEnum : MAPPED_ENUMS) {
if (name.startsWith(mappedEnum.getPrefix())) {
String enumName = name.substring(mappedEnum.getPrefix().length());
for (Enum<?> ansiEnum : mappedEnum.getEnums()) {
if (ansiEnum.name().equals(enumName)) {
if (this.encode) {
return AnsiOutput.encode((AnsiElement) ansiEnum);
}
return ansiEnum;
}
for (Mapping mapping : MAPPINGS) {
String prefix = mapping.getPrefix();
if (name.startsWith(prefix)) {
String postfix = name.substring(prefix.length());
AnsiElement element = mapping.getElement(postfix);
if (element != null) {
return (this.encode) ? AnsiOutput.encode(element) : element;
}
}
}
@ -84,26 +90,79 @@ public class AnsiPropertySource extends PropertySource<AnsiElement> {
}
/**
* Mapping between an enum and the pseudo property source.
* Mapping between a name and the pseudo property source.
*/
private static class MappedEnum<E extends Enum<E>> {
private abstract static class Mapping {
private final String prefix;
private final Set<E> enums;
MappedEnum(String prefix, Class<E> enumType) {
Mapping(String prefix) {
this.prefix = prefix;
this.enums = EnumSet.allOf(enumType);
}
String getPrefix() {
return this.prefix;
}
Set<E> getEnums() {
return this.enums;
abstract AnsiElement getElement(String postfix);
}
/**
* {@link Mapping} for {@link AnsiElement} enums.
*/
private static class EnumMapping<E extends Enum<E> & AnsiElement> extends Mapping {
private final Set<E> enums;
EnumMapping(String prefix, Class<E> enumType) {
super(prefix);
this.enums = EnumSet.allOf(enumType);
}
@Override
AnsiElement getElement(String postfix) {
for (Enum<?> candidate : this.enums) {
if (candidate.name().equals(postfix)) {
return (AnsiElement) candidate;
}
}
return null;
}
}
/**
* {@link Mapping} for {@link Ansi8BitColor}.
*/
private static class Ansi8BitColorMapping extends Mapping {
private final IntFunction<Ansi8BitColor> factory;
Ansi8BitColorMapping(String prefix, IntFunction<Ansi8BitColor> factory) {
super(prefix);
this.factory = factory;
}
@Override
AnsiElement getElement(String postfix) {
if (containsOnlyDigits(postfix)) {
try {
return this.factory.apply(Integer.parseInt(postfix));
}
catch (IllegalArgumentException ex) {
}
}
return null;
}
private boolean containsOnlyDigits(String postfix) {
for (int i = 0; i < postfix.length(); i++) {
if (!Character.isDigit(postfix.charAt(i))) {
return false;
}
}
return postfix.length() > 0;
}
}

View File

@ -47,6 +47,12 @@
"description": "Whether images should be inverted for dark terminal themes.",
"defaultValue": false
},
{
"name": "spring.banner.image.bitdepth",
"type": "java.lang.Integer",
"description": "The bit depth to use for ANSI colors. Supported values are 4 (16 color) or 8 (256 color).",
"defaultValue": 4
},
{
"name": "debug",
"type": "java.lang.Boolean",

View File

@ -23,6 +23,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ansi.Ansi8BitColor;
import org.springframework.boot.ansi.AnsiBackground;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiOutput;
@ -168,6 +169,16 @@ class ImageBannerTests {
assertThat(lines).hasSize(frames * linesPerFrame - 1);
}
@Test
void printBannerWhenBitDepthIs8ShouldUseColors() {
String banner = printBanner("colors.gif", "spring.banner.image.bitdepth=8");
assertThat(banner.contains(AnsiOutput.encode(Ansi8BitColor.foreground(124))));
assertThat(banner.contains(AnsiOutput.encode(Ansi8BitColor.foreground(130))));
assertThat(banner.contains(AnsiOutput.encode(Ansi8BitColor.foreground(19))));
assertThat(banner.contains(AnsiOutput.encode(Ansi8BitColor.foreground(127))));
assertThat(banner.contains(AnsiOutput.encode(Ansi8BitColor.foreground(37))));
}
private int getBannerHeight(String banner) {
return banner.split(System.lineSeparator()).length - 3;
}

View File

@ -98,8 +98,7 @@ class ResourceBannerTests {
@Test
void renderWith256Colors() {
Resource resource = new ByteArrayResource(
"${Ansi256Color.Foreground_208}This is orange.${Ansi.NORMAL}".getBytes());
Resource resource = new ByteArrayResource("${AnsiColor.208}This is orange.${Ansi.NORMAL}".getBytes());
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);
String banner = printBanner(resource, null, null, null);
assertThat(banner).startsWith("\033[38;5;208mThis is orange.\u001B[0m");
@ -107,8 +106,7 @@ class ResourceBannerTests {
@Test
void renderWith256ColorsButDisabled() {
Resource resource = new ByteArrayResource(
"${Ansi256Color.Foreground_208}This is orange.${Ansi.NORMAL}".getBytes());
Resource resource = new ByteArrayResource("${AnsiColor.208}This is orange.${Ansi.NORMAL}".getBytes());
AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER);
String banner = printBanner(resource, null, null, null);
assertThat(banner).startsWith("This is orange.");

View File

@ -1,54 +0,0 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ansi;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown;
/**
* Tests for {@link Ansi256Color}.
*
* @author Toshiaki Maki
*/
class Ansi256ColorTest {
@Test
void testForeground() {
final Ansi256Color ansi256Color = new Ansi256Color.Foreground(208);
assertThat(ansi256Color.toString()).isEqualTo("38;5;208");
}
@Test
void testBackground() {
final Ansi256Color ansi256Color = new Ansi256Color.Background(208);
assertThat(ansi256Color.toString()).isEqualTo("48;5;208");
}
@Test
void testIllegalColorCode() {
try {
new Ansi256Color.Foreground(256);
failBecauseExceptionWasNotThrown(IllegalArgumentException.class);
}
catch (IllegalArgumentException ex) {
assertThat(ex.getMessage()).isEqualTo("'colorCode' must be between 0 and 255.");
}
}
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ansi;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Ansi256PropertySource}.
*
* @author Toshiaki Maki
*/
class Ansi256PropertySourceTest {
private Ansi256PropertySource source = new Ansi256PropertySource("ansi256");
@AfterEach
void reset() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.DETECT);
}
@Test
void getPropertyShouldConvertAnsi256ColorForeground() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);
final Object property = this.source.getProperty("Ansi256Color.Foreground_100");
assertThat(property).isEqualTo("\033[38;5;100m");
}
@Test
void getPropertyShouldConvertAnsi256ColorBackground() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);
final Object property = this.source.getProperty("Ansi256Color.Background_100");
assertThat(property).isEqualTo("\033[48;5;100m");
}
@Test
void getMissingPropertyShouldReturnNull() {
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS);
final Object property = this.source.getProperty("Ansi256Color.ForeGround_100");
assertThat(property).isNull();
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2012-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.ansi;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Ansi8BitColor}.
*
* @author Toshiaki Maki
* @author Phillip Webb
*/
class Ansi8BitColorTests {
@Test
void toStringWhenForegroundAddsCorrectPrefix() {
assertThat(Ansi8BitColor.foreground(208).toString()).isEqualTo("38;5;208");
}
@Test
void toStringWhenBackgroundAddsCorrectPrefix() {
assertThat(Ansi8BitColor.background(208).toString()).isEqualTo("48;5;208");
}
@Test
void forgroundWhenOutsideBoundsThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Ansi8BitColor.foreground(-1))
.withMessage("Code must be between 0 and 255");
assertThatIllegalArgumentException().isThrownBy(() -> Ansi8BitColor.foreground(256))
.withMessage("Code must be between 0 and 255");
}
@Test
void backgroundWhenOutsideBoundsThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> Ansi8BitColor.background(-1))
.withMessage("Code must be between 0 and 255");
assertThatIllegalArgumentException().isThrownBy(() -> Ansi8BitColor.background(256))
.withMessage("Code must be between 0 and 255");
}
@Test
void equalsAndHashCode() {
Ansi8BitColor one = Ansi8BitColor.foreground(123);
Ansi8BitColor two = Ansi8BitColor.foreground(123);
Ansi8BitColor three = Ansi8BitColor.background(123);
assertThat(one.hashCode()).isEqualTo(two.hashCode());
assertThat(one).isEqualTo(one).isEqualTo(two).isNotEqualTo(three).isNotEqualTo(null).isNotEqualTo("foo");
}
}

View File

@ -27,6 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link AnsiPropertySource}.
*
* @author Phillip Webb
* @author Toshiaki Maki
*/
class AnsiPropertySourceTests {
@ -45,11 +46,13 @@ class AnsiPropertySourceTests {
@Test
void getAnsiColor() {
assertThat(this.source.getProperty("AnsiColor.RED")).isEqualTo(AnsiColor.RED);
assertThat(this.source.getProperty("AnsiColor.100")).isEqualTo(Ansi8BitColor.foreground(100));
}
@Test
void getAnsiBackground() {
assertThat(this.source.getProperty("AnsiBackground.GREEN")).isEqualTo(AnsiBackground.GREEN);
assertThat(this.source.getProperty("AnsiBackground.100")).isEqualTo(Ansi8BitColor.background(100));
}
@Test
@ -69,6 +72,8 @@ class AnsiPropertySourceTests {
AnsiOutput.setEnabled(Enabled.ALWAYS);
AnsiPropertySource source = new AnsiPropertySource("ansi", true);
assertThat(source.getProperty("Ansi.RED")).isEqualTo("\033[31m");
assertThat(source.getProperty("AnsiColor.100")).isEqualTo("\033[38;5;100m");
assertThat(source.getProperty("AnsiBackground.100")).isEqualTo("\033[48;5;100m");
}
@Test
@ -76,6 +81,8 @@ class AnsiPropertySourceTests {
AnsiOutput.setEnabled(Enabled.NEVER);
AnsiPropertySource source = new AnsiPropertySource("ansi", true);
assertThat(source.getProperty("Ansi.RED")).isEqualTo("");
assertThat(source.getProperty("AnsiColor.100")).isEqualTo("");
assertThat(source.getProperty("AnsiBackground.100")).isEqualTo("");
}
}