Commit d6200389 authored by Geoffrey Chandler's avatar Geoffrey Chandler Committed by Phillip Webb

Add support for ANSI colored resource banners

Add AnsiPropertySource which allows named ANSI codes to be resolved
and update ResourceBanner to include it.

This commit also deprecates constants defined in AnsiElement and
replaces them with AnsiStyle, AnsiColor and AnsiBackground enums.

Closes gh-2704
parent 9f143ad9
...@@ -72,6 +72,10 @@ display (surrounded with brackets and prefixed with `v`). For example `(v1.0)`. ...@@ -72,6 +72,10 @@ display (surrounded with brackets and prefixed with `v`). For example `(v1.0)`.
|`${spring-boot.formatted-version}` |`${spring-boot.formatted-version}`
|The Spring Boot version that you are using formatted for display (surrounded with |The Spring Boot version that you are using formatted for display (surrounded with
brackets and prefixed with `v`). For example `(v{spring-boot-version})`. 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 TIP: The `SpringBootApplication.setBanner(...)` method can be used if you want to generate
......
...@@ -25,6 +25,7 @@ import java.util.Map; ...@@ -25,6 +25,7 @@ import java.util.Map;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ansi.AnsiPropertySource;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.MutablePropertySources;
...@@ -77,6 +78,7 @@ public class ResourceBanner implements Banner { ...@@ -77,6 +78,7 @@ public class ResourceBanner implements Banner {
List<PropertyResolver> resolvers = new ArrayList<PropertyResolver>(); List<PropertyResolver> resolvers = new ArrayList<PropertyResolver>();
resolvers.add(environment); resolvers.add(environment);
resolvers.add(getVersionResolver(sourceClass)); resolvers.add(getVersionResolver(sourceClass));
resolvers.add(getAnsiResolver());
return resolvers; return resolvers;
} }
...@@ -114,4 +116,10 @@ public class ResourceBanner implements Banner { ...@@ -114,4 +116,10 @@ public class ResourceBanner implements Banner {
return (format ? " (v" + version + ")" : version); return (format ? " (v" + version + ")" : version);
} }
private PropertyResolver getAnsiResolver() {
MutablePropertySources sources = new MutablePropertySources();
sources.addFirst(new AnsiPropertySource("ansi", true));
return new PropertySourcesPropertyResolver(sources);
}
} }
...@@ -21,9 +21,9 @@ import java.io.PrintStream; ...@@ -21,9 +21,9 @@ import java.io.PrintStream;
import org.springframework.boot.ansi.AnsiOutput; import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import static org.springframework.boot.ansi.AnsiElement.DEFAULT; import static org.springframework.boot.ansi.AnsiColor.DEFAULT;
import static org.springframework.boot.ansi.AnsiElement.FAINT; import static org.springframework.boot.ansi.AnsiColor.GREEN;
import static org.springframework.boot.ansi.AnsiElement.GREEN; import static org.springframework.boot.ansi.AnsiStyle.FAINT;
/** /**
* Default Banner implementation which writes the 'Spring' banner. * Default Banner implementation which writes the 'Spring' banner.
......
/*
* 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;
}
}
/*
* 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;
}
}
...@@ -23,32 +23,88 @@ package org.springframework.boot.ansi; ...@@ -23,32 +23,88 @@ package org.springframework.boot.ansi;
*/ */
public interface AnsiElement { public interface AnsiElement {
/**
* @deprecated in 1.3.0 in favor of {@link AnsiStyle#NORMAL}
*/
@Deprecated
public static final AnsiElement NORMAL = new DefaultAnsiElement("0"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); public static final AnsiElement DEFAULT = new DefaultAnsiElement("39");
/** /**
......
...@@ -39,7 +39,7 @@ public abstract class AnsiOutput { ...@@ -39,7 +39,7 @@ public abstract class AnsiOutput {
private static final String ENCODE_END = "m"; 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. * Sets if ANSI output is enabled.
...@@ -63,6 +63,18 @@ public abstract class AnsiOutput { ...@@ -63,6 +63,18 @@ public abstract class AnsiOutput {
return AnsiOutput.enabled; 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 * Create a new ANSI string from the specified elements. Any {@link AnsiElement}s will
* be encoded as required. * be encoded as required.
......
/*
* 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<AnsiElement> {
private static final Iterable<MappedEnum<?>> MAPPED_ENUMS;
static {
List<MappedEnum<?>> enums = new ArrayList<MappedEnum<?>>();
enums.add(new MappedEnum<AnsiStyle>("AnsiStyle.", AnsiStyle.class));
enums.add(new MappedEnum<AnsiColor>("AnsiColor.", AnsiColor.class));
enums.add(new MappedEnum<AnsiBackground>("AnsiBackground.", AnsiBackground.class));
enums.add(new MappedEnum<AnsiStyle>("Ansi.", AnsiStyle.class));
enums.add(new MappedEnum<AnsiColor>("Ansi.", AnsiColor.class));
enums.add(new MappedEnum<AnsiBackground>("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<E extends Enum<E>> {
private final String prefix;
private final Set<E> enums;
public MappedEnum(String prefix, Class<E> enumType) {
this.prefix = prefix;
this.enums = EnumSet.allOf(enumType);
}
public String getPrefix() {
return this.prefix;
}
public Set<E> getEnums() {
return this.enums;
}
}
}
/*
* 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;
}
}
...@@ -20,8 +20,10 @@ import java.util.Collections; ...@@ -20,8 +20,10 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.boot.ansi.AnsiColor;
import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiElement;
import org.springframework.boot.ansi.AnsiOutput; import org.springframework.boot.ansi.AnsiOutput;
import org.springframework.boot.ansi.AnsiStyle;
import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ILoggingEvent;
...@@ -39,21 +41,21 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> { ...@@ -39,21 +41,21 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
private static final Map<String, AnsiElement> ELEMENTS; private static final Map<String, AnsiElement> ELEMENTS;
static { static {
Map<String, AnsiElement> elements = new HashMap<String, AnsiElement>(); Map<String, AnsiElement> elements = new HashMap<String, AnsiElement>();
elements.put("faint", AnsiElement.FAINT); elements.put("faint", AnsiStyle.FAINT);
elements.put("red", AnsiElement.RED); elements.put("red", AnsiColor.RED);
elements.put("green", AnsiElement.GREEN); elements.put("green", AnsiColor.GREEN);
elements.put("yellow", AnsiElement.YELLOW); elements.put("yellow", AnsiColor.YELLOW);
elements.put("blue", AnsiElement.BLUE); elements.put("blue", AnsiColor.BLUE);
elements.put("magenta", AnsiElement.MAGENTA); elements.put("magenta", AnsiColor.MAGENTA);
elements.put("cyan", AnsiElement.CYAN); elements.put("cyan", AnsiColor.CYAN);
ELEMENTS = Collections.unmodifiableMap(elements); ELEMENTS = Collections.unmodifiableMap(elements);
} }
private static final Map<Integer, AnsiElement> LEVELS; private static final Map<Integer, AnsiElement> LEVELS;
static { static {
Map<Integer, AnsiElement> levels = new HashMap<Integer, AnsiElement>(); Map<Integer, AnsiElement> levels = new HashMap<Integer, AnsiElement>();
levels.put(Level.ERROR_INTEGER, AnsiElement.RED); levels.put(Level.ERROR_INTEGER, AnsiColor.RED);
levels.put(Level.WARN_INTEGER, AnsiElement.YELLOW); levels.put(Level.WARN_INTEGER, AnsiColor.YELLOW);
LEVELS = Collections.unmodifiableMap(levels); LEVELS = Collections.unmodifiableMap(levels);
} }
...@@ -63,7 +65,7 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> { ...@@ -63,7 +65,7 @@ public class ColorConverter extends CompositeConverter<ILoggingEvent> {
if (element == null) { if (element == null) {
// Assume highlighting // Assume highlighting
element = LEVELS.get(event.getLevel().toInteger()); element = LEVELS.get(event.getLevel().toInteger());
element = (element == null ? AnsiElement.GREEN : element); element = (element == null ? AnsiColor.GREEN : element);
} }
return toAnsiString(in, element); return toAnsiString(in, element);
} }
......
...@@ -21,7 +21,10 @@ import java.io.PrintStream; ...@@ -21,7 +21,10 @@ import java.io.PrintStream;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import org.junit.After;
import org.junit.Test; 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.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MapPropertySource;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
...@@ -38,6 +41,11 @@ import static org.junit.Assert.assertThat; ...@@ -38,6 +41,11 @@ import static org.junit.Assert.assertThat;
*/ */
public class ResourceBannerTests { public class ResourceBannerTests {
@After
public void reset() {
AnsiOutput.setEnabled(Enabled.DETECT);
}
@Test @Test
public void renderVersions() throws Exception { public void renderVersions() throws Exception {
Resource resource = new ByteArrayResource( Resource resource = new ByteArrayResource(
...@@ -72,6 +80,24 @@ public class ResourceBannerTests { ...@@ -72,6 +80,24 @@ public class ResourceBannerTests {
assertThat(banner, startsWith("banner 1")); 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, private String printBanner(Resource resource, String bootVersion,
String applicationVersion) { String applicationVersion) {
ResourceBanner banner = new MockResourceBanner(resource, bootVersion, ResourceBanner banner = new MockResourceBanner(resource, bootVersion,
......
/*
* 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) ""));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment