diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index e56067e90de..a60362f4016 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -72,6 +72,10 @@ display (surrounded with brackets and prefixed with `v`). For example `(v1.0)`. |`${spring-boot.formatted-version}` |The Spring Boot version that you are using formatted for display (surrounded with brackets and prefixed with `v`). For example `(v{spring-boot-version})`. + +|`${Ansi.NAME}`, +|Where `NAME` is the name of an ANSI escape code. See +{sc-spring-boot}/ansi/AnsiPropertySource.{sc-ext}[`AnsiPropertySource`] for details. |=== TIP: The `SpringBootApplication.setBanner(...)` method can be used if you want to generate diff --git a/spring-boot/src/main/java/org/springframework/boot/ResourceBanner.java b/spring-boot/src/main/java/org/springframework/boot/ResourceBanner.java index 0bc021c887a..89a82a0b840 100644 --- a/spring-boot/src/main/java/org/springframework/boot/ResourceBanner.java +++ b/spring-boot/src/main/java/org/springframework/boot/ResourceBanner.java @@ -25,6 +25,7 @@ import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.ansi.AnsiPropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; @@ -77,6 +78,7 @@ public class ResourceBanner implements Banner { List resolvers = new ArrayList(); resolvers.add(environment); resolvers.add(getVersionResolver(sourceClass)); + resolvers.add(getAnsiResolver()); return resolvers; } @@ -114,4 +116,10 @@ public class ResourceBanner implements Banner { return (format ? " (v" + version + ")" : version); } + private PropertyResolver getAnsiResolver() { + MutablePropertySources sources = new MutablePropertySources(); + sources.addFirst(new AnsiPropertySource("ansi", true)); + return new PropertySourcesPropertyResolver(sources); + } + } diff --git a/spring-boot/src/main/java/org/springframework/boot/SpringBootBanner.java b/spring-boot/src/main/java/org/springframework/boot/SpringBootBanner.java index 4c738781135..2489859dfb6 100644 --- a/spring-boot/src/main/java/org/springframework/boot/SpringBootBanner.java +++ b/spring-boot/src/main/java/org/springframework/boot/SpringBootBanner.java @@ -21,9 +21,9 @@ import java.io.PrintStream; import org.springframework.boot.ansi.AnsiOutput; import org.springframework.core.env.Environment; -import static org.springframework.boot.ansi.AnsiElement.DEFAULT; -import static org.springframework.boot.ansi.AnsiElement.FAINT; -import static org.springframework.boot.ansi.AnsiElement.GREEN; +import static org.springframework.boot.ansi.AnsiColor.DEFAULT; +import static org.springframework.boot.ansi.AnsiColor.GREEN; +import static org.springframework.boot.ansi.AnsiStyle.FAINT; /** * Default Banner implementation which writes the 'Spring' banner. diff --git a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiBackground.java b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiBackground.java new file mode 100644 index 00000000000..cf174e0ccfd --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiBackground.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2015 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 + * + * http://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 Ansi} background colors. + * + * @author Phillip Webb + * @author Geoffrey Chandler + * @since 1.3.0 + */ +public enum AnsiBackground implements AnsiElement { + + DEFAULT("49"), + + BLACK("40"), + + RED("41"), + + GREEN("42"), + + YELLOW("43"), + + BLUE("44"), + + MAGENTA("45"), + + CYAN("46"), + + WHITE("47"), + + BRIGHT_BLACK("100"), + + BRIGHT_RED("101"), + + BRIGHT_GREEN("102"), + + BRIGHT_YELLOW("103"), + + BRIGHT_BLUE("104"), + + BRIGHT_MAGENTA("105"), + + BRIGHT_CYAN("106"), + + BRIGHT_WHITE("107"); + + private String code; + + private AnsiBackground(String code) { + this.code = code; + } + + @Override + public String toString() { + return this.code; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColor.java b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColor.java new file mode 100644 index 00000000000..c5a56ae3e87 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiColor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2012-2015 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 + * + * http://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 Ansi} colors. + * + * @author Phillip Webb + * @author Geoffrey Chandler + * @since 1.3.0 + */ +public enum AnsiColor implements AnsiElement { + + DEFAULT("39"), + + BLACK("30"), + + RED("31"), + + GREEN("32"), + + YELLOW("33"), + + BLUE("34"), + + MAGENTA("35"), + + CYAN("36"), + + WHITE("37"), + + BRIGHT_BLACK("90"), + + BRIGHT_RED("91"), + + BRIGHT_GREEN("92"), + + BRIGHT_YELLOW("93"), + + BRIGHT_BLUE("94"), + + BRIGHT_MAGENTA("95"), + + BRIGHT_CYAN("96"), + + BRIGHT_WHITE("97"); + + private final String code; + + private AnsiColor(String code) { + this.code = code; + } + + @Override + public String toString() { + return this.code; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiElement.java b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiElement.java index decbf0e2f75..249400dd332 100644 --- a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiElement.java +++ b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiElement.java @@ -23,32 +23,88 @@ package org.springframework.boot.ansi; */ public interface AnsiElement { + /** + * @deprecated in 1.3.0 in favor of {@link AnsiStyle#NORMAL} + */ + @Deprecated public static final AnsiElement NORMAL = new DefaultAnsiElement("0"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiStyle#BOLD} + */ + @Deprecated public static final AnsiElement BOLD = new DefaultAnsiElement("1"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiStyle#FAINT} + */ + @Deprecated public static final AnsiElement FAINT = new DefaultAnsiElement("2"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiStyle#ITALIC} + */ + @Deprecated public static final AnsiElement ITALIC = new DefaultAnsiElement("3"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiStyle#UNDERLINE} + */ + @Deprecated public static final AnsiElement UNDERLINE = new DefaultAnsiElement("4"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#BLACK} + */ + @Deprecated public static final AnsiElement BLACK = new DefaultAnsiElement("30"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#RED} + */ + @Deprecated public static final AnsiElement RED = new DefaultAnsiElement("31"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#GREEN} + */ + @Deprecated public static final AnsiElement GREEN = new DefaultAnsiElement("32"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#YELLOW} + */ + @Deprecated public static final AnsiElement YELLOW = new DefaultAnsiElement("33"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#BLUE} + */ + @Deprecated public static final AnsiElement BLUE = new DefaultAnsiElement("34"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#MAGENTA} + */ + @Deprecated public static final AnsiElement MAGENTA = new DefaultAnsiElement("35"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#CYAN} + */ + @Deprecated public static final AnsiElement CYAN = new DefaultAnsiElement("36"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#WHITE} + */ + @Deprecated public static final AnsiElement WHITE = new DefaultAnsiElement("37"); + /** + * @deprecated in 1.3.0 in favor of {@link AnsiColor#DEFAULT} + */ + @Deprecated public static final AnsiElement DEFAULT = new DefaultAnsiElement("39"); /** diff --git a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index a38eb208a2f..e46313022b4 100644 --- a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -39,7 +39,7 @@ public abstract class AnsiOutput { private static final String ENCODE_END = "m"; - private static final String RESET = "0;" + AnsiElement.DEFAULT; + private static final String RESET = "0;" + AnsiColor.DEFAULT; /** * Sets if ANSI output is enabled. @@ -63,6 +63,18 @@ public abstract class AnsiOutput { return AnsiOutput.enabled; } + /** + * Encode a single {@link AnsiElement} if output is enabled. + * @param element the element to encode + * @return the encoded element or an empty string + */ + public static String encode(AnsiElement element) { + if (isEnabled()) { + return ENCODE_START + element + ENCODE_END; + } + return ""; + } + /** * Create a new ANSI string from the specified elements. Any {@link AnsiElement}s will * be encoded as required. diff --git a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java new file mode 100644 index 00000000000..eec6ad0fd4b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java @@ -0,0 +1,110 @@ +/* + * Copyright 2012-2015 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 + * + * http://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 java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +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 background colors prefixed + * {@code BG_}). + * + * @author Phillip Webb + * @since 1.3.0 + */ +public class AnsiPropertySource extends PropertySource { + + private static final Iterable> MAPPED_ENUMS; + static { + List> 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); + } + + private final boolean encode; + + /** + * Create a new {@link AnsiPropertySource} instance. + * @param name the name of the property source + * @param encode if the output should be encoded + */ + public AnsiPropertySource(String name, boolean encode) { + super(name); + this.encode = encode; + } + + @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; + } + } + } + } + } + return null; + } + + /** + * Mapping between an enum and the pseudo property source. + */ + private static class MappedEnum> { + + private final String prefix; + + private final Set enums; + + public MappedEnum(String prefix, Class enumType) { + this.prefix = prefix; + this.enums = EnumSet.allOf(enumType); + + } + + public String getPrefix() { + return this.prefix; + } + + public Set getEnums() { + return this.enums; + } + + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java new file mode 100644 index 00000000000..567bedf9a3b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2015 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 + * + * http://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 Ansi} styles. + * + * @author Phillip Webb + * @since 1.3.0 + */ +public enum AnsiStyle implements AnsiElement { + + NORMAL("0"), + + BOLD("1"), + + FAINT("2"), + + ITALIC("3"), + + UNDERLINE("4"); + + private final String code; + + private AnsiStyle(String code) { + this.code = code; + } + + @Override + public String toString() { + return this.code; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java b/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java index 49817701daf..a1838c5b39a 100644 --- a/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java +++ b/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java @@ -20,8 +20,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.springframework.boot.ansi.AnsiColor; import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.ansi.AnsiStyle; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.spi.ILoggingEvent; @@ -39,21 +41,21 @@ public class ColorConverter extends CompositeConverter { private static final Map ELEMENTS; static { Map elements = new HashMap(); - elements.put("faint", AnsiElement.FAINT); - elements.put("red", AnsiElement.RED); - elements.put("green", AnsiElement.GREEN); - elements.put("yellow", AnsiElement.YELLOW); - elements.put("blue", AnsiElement.BLUE); - elements.put("magenta", AnsiElement.MAGENTA); - elements.put("cyan", AnsiElement.CYAN); + elements.put("faint", AnsiStyle.FAINT); + elements.put("red", AnsiColor.RED); + elements.put("green", AnsiColor.GREEN); + elements.put("yellow", AnsiColor.YELLOW); + elements.put("blue", AnsiColor.BLUE); + elements.put("magenta", AnsiColor.MAGENTA); + elements.put("cyan", AnsiColor.CYAN); ELEMENTS = Collections.unmodifiableMap(elements); } private static final Map LEVELS; static { Map levels = new HashMap(); - levels.put(Level.ERROR_INTEGER, AnsiElement.RED); - levels.put(Level.WARN_INTEGER, AnsiElement.YELLOW); + levels.put(Level.ERROR_INTEGER, AnsiColor.RED); + levels.put(Level.WARN_INTEGER, AnsiColor.YELLOW); LEVELS = Collections.unmodifiableMap(levels); } @@ -63,7 +65,7 @@ public class ColorConverter extends CompositeConverter { if (element == null) { // Assume highlighting element = LEVELS.get(event.getLevel().toInteger()); - element = (element == null ? AnsiElement.GREEN : element); + element = (element == null ? AnsiColor.GREEN : element); } return toAnsiString(in, element); } diff --git a/spring-boot/src/test/java/org/springframework/boot/ResourceBannerTests.java b/spring-boot/src/test/java/org/springframework/boot/ResourceBannerTests.java index b1c0ff4af19..2b5b1e8ff94 100644 --- a/spring-boot/src/test/java/org/springframework/boot/ResourceBannerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/ResourceBannerTests.java @@ -21,7 +21,10 @@ import java.io.PrintStream; import java.util.Collections; import java.util.Map; +import org.junit.After; import org.junit.Test; +import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.ansi.AnsiOutput.Enabled; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.io.ByteArrayResource; @@ -38,6 +41,11 @@ import static org.junit.Assert.assertThat; */ public class ResourceBannerTests { + @After + public void reset() { + AnsiOutput.setEnabled(Enabled.DETECT); + } + @Test public void renderVersions() throws Exception { Resource resource = new ByteArrayResource( @@ -72,6 +80,24 @@ public class ResourceBannerTests { assertThat(banner, startsWith("banner 1")); } + @Test + public void renderWithColors() throws Exception { + Resource resource = new ByteArrayResource( + "${Ansi.RED}This is red.${Ansi.NORMAL}".getBytes()); + AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS); + String banner = printBanner(resource, null, null); + assertThat(banner, startsWith("\u001B[31mThis is red.\u001B[0m")); + } + + @Test + public void renderWithColorsButDisabled() throws Exception { + Resource resource = new ByteArrayResource( + "${Ansi.RED}This is red.${Ansi.NORMAL}".getBytes()); + AnsiOutput.setEnabled(AnsiOutput.Enabled.NEVER); + String banner = printBanner(resource, null, null); + assertThat(banner, startsWith("This is red.")); + } + private String printBanner(Resource resource, String bootVersion, String applicationVersion) { ResourceBanner banner = new MockResourceBanner(resource, bootVersion, diff --git a/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiPropertySourceTests.java b/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiPropertySourceTests.java new file mode 100644 index 00000000000..f84018425b2 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/ansi/AnsiPropertySourceTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.After; +import org.junit.Test; +import org.springframework.boot.ansi.AnsiOutput.Enabled; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link AnsiPropertySource}. + * + * @author Phillip Webb + */ +public class AnsiPropertySourceTests { + + private AnsiPropertySource source = new AnsiPropertySource("ansi", false); + + @After + public void reset() { + AnsiOutput.setEnabled(Enabled.DETECT); + } + + @Test + public void getAnsiStyle() throws Exception { + assertThat(this.source.getProperty("AnsiStyle.BOLD"), + equalTo((Object) AnsiStyle.BOLD)); + } + + @Test + public void getAnsiColor() throws Exception { + assertThat(this.source.getProperty("AnsiColor.RED"), + equalTo((Object) AnsiColor.RED)); + } + + @Test + public void getAnsiBackground() throws Exception { + assertThat(this.source.getProperty("AnsiBackground.GREEN"), + equalTo((Object) AnsiBackground.GREEN)); + } + + @Test + public void getAnsi() throws Exception { + assertThat(this.source.getProperty("Ansi.BOLD"), equalTo((Object) AnsiStyle.BOLD)); + assertThat(this.source.getProperty("Ansi.RED"), equalTo((Object) AnsiColor.RED)); + assertThat(this.source.getProperty("Ansi.BG_RED"), + equalTo((Object) AnsiBackground.RED)); + } + + @Test + public void getMissing() throws Exception { + assertThat(this.source.getProperty("AnsiStyle.NOPE"), nullValue()); + } + + @Test + public void encodeEnabled() throws Exception { + AnsiOutput.setEnabled(Enabled.ALWAYS); + AnsiPropertySource source = new AnsiPropertySource("ansi", true); + assertThat(source.getProperty("Ansi.RED"), equalTo((Object) "\033[31m")); + } + + @Test + public void encodeDisabled() throws Exception { + AnsiOutput.setEnabled(Enabled.NEVER); + AnsiPropertySource source = new AnsiPropertySource("ansi", true); + assertThat(source.getProperty("Ansi.RED"), equalTo((Object) "")); + } + +}