diff --git a/pom.xml b/pom.xml index 02f6c447..6e912bed 100644 --- a/pom.xml +++ b/pom.xml @@ -154,6 +154,23 @@ + + pl.project13.maven + git-commit-id-plugin + + + + revision + + + + + true + yyyy-MM-dd'T'HH:mm:ssZ + true + ${project.build.outputDirectory}/git.properties + + diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java index 07e80f80..f54ba15d 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellProperties.java @@ -28,6 +28,7 @@ public class SpringShellProperties { private Script script = new Script(); private Interactive interactive = new Interactive(); private Noninteractive noninteractive = new Noninteractive(); + private Theme theme = new Theme(); private Command command = new Command(); public void setScript(Script script) { @@ -54,6 +55,14 @@ public class SpringShellProperties { this.noninteractive = noninteractive; } + public Theme getTheme() { + return theme; + } + + public void setTheme(Theme theme) { + this.theme = theme; + } + public Command getCommand() { return command; } @@ -101,6 +110,19 @@ public class SpringShellProperties { } } + public static class Theme { + + private String name = "default"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + public static class HelpCommand { private boolean enabled = true; @@ -210,6 +232,7 @@ public class SpringShellProperties { private ScriptCommand script = new ScriptCommand(); private HistoryCommand history = new HistoryCommand(); private CompletionCommand completion = new CompletionCommand(); + private VersionCommand version = new VersionCommand(); public void setHelp(HelpCommand help) { this.help = help; @@ -266,5 +289,116 @@ public class SpringShellProperties { public void setCompletion(CompletionCommand completion) { this.completion = completion; } + + public VersionCommand getVersion() { + return version; + } + + public void setVersion(VersionCommand version) { + this.version = version; + } + } + + public static class VersionCommand { + + private boolean enabled = true; + private String template = "classpath:template/version-default.st"; + private boolean showBuildGroup = false; + private boolean showBuildArtifact = false; + private boolean showBuildName = false; + private boolean showBuildVersion = true; + private boolean showBuildTime = false; + private boolean showGitBranch = false; + private boolean showGitCommitId = false; + private boolean showGitShortCommitId = false; + private boolean showGitCommitTime = false; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getTemplate() { + return template; + } + + public void setTemplate(String template) { + this.template = template; + } + + public boolean isShowBuildGroup() { + return showBuildGroup; + } + + public void setShowBuildGroup(boolean showBuildGroup) { + this.showBuildGroup = showBuildGroup; + } + + public boolean isShowBuildArtifact() { + return showBuildArtifact; + } + + public void setShowBuildArtifact(boolean showBuildArtifact) { + this.showBuildArtifact = showBuildArtifact; + } + + public boolean isShowBuildName() { + return showBuildName; + } + + public void setShowBuildName(boolean showBuildName) { + this.showBuildName = showBuildName; + } + + public boolean isShowBuildVersion() { + return showBuildVersion; + } + + public void setShowBuildVersion(boolean showBuildVersion) { + this.showBuildVersion = showBuildVersion; + } + + public boolean isShowBuildTime() { + return showBuildTime; + } + + public void setShowBuildTime(boolean showBuildTime) { + this.showBuildTime = showBuildTime; + } + + public boolean isShowGitBranch() { + return showGitBranch; + } + + public void setShowGitBranch(boolean showGitBranch) { + this.showGitBranch = showGitBranch; + } + + public boolean isShowGitCommitId() { + return showGitCommitId; + } + + public void setShowGitCommitId(boolean showGitCommitId) { + this.showGitCommitId = showGitCommitId; + } + + public boolean isShowGitShortCommitId() { + return showGitShortCommitId; + } + + public void setShowGitShortCommitId(boolean showGitShortCommitId) { + this.showGitShortCommitId = showGitShortCommitId; + } + + public boolean isShowGitCommitTime() { + return showGitCommitTime; + } + + public void setShowGitCommitTime(boolean showGitCommitTime) { + this.showGitCommitTime = showGitCommitTime; + } } } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java index 0885a0a1..d4cdfa8a 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/StandardCommandsAutoConfiguration.java @@ -23,9 +23,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.shell.boot.SpringShellProperties.VersionCommand; import org.springframework.shell.boot.condition.OnCompletionCommandCondition; import org.springframework.shell.result.ThrowableResultHandler; import org.springframework.shell.standard.commands.Clear; @@ -35,6 +38,8 @@ import org.springframework.shell.standard.commands.History; import org.springframework.shell.standard.commands.Quit; import org.springframework.shell.standard.commands.Script; import org.springframework.shell.standard.commands.Stacktrace; +import org.springframework.shell.standard.commands.Version; +import org.springframework.shell.style.TemplateExecutor; /** * Creates beans for standard commands. @@ -94,4 +99,26 @@ public class StandardCommandsAutoConfiguration { public Completion completion(SpringShellProperties properties) { return new Completion(properties.getCommand().getCompletion().getRootCommand()); } + + @Bean + @ConditionalOnMissingBean(Version.Command.class) + @ConditionalOnProperty(prefix = "spring.shell.command.version", value = "enabled", havingValue = "true", matchIfMissing = true) + public Version version(SpringShellProperties properties, ObjectProvider buildProperties, + ObjectProvider gitProperties, ObjectProvider templateExecutor) { + Version version = new Version(templateExecutor.getIfAvailable()); + version.setBuildProperties(buildProperties.getIfAvailable()); + version.setGitProperties(gitProperties.getIfAvailable()); + VersionCommand versionProperties = properties.getCommand().getVersion(); + version.setTemplate(versionProperties.getTemplate()); + version.setShowBuildArtifact(versionProperties.isShowBuildArtifact()); + version.setShowBuildGroup(versionProperties.isShowBuildGroup()); + version.setShowBuildName(versionProperties.isShowBuildName()); + version.setShowBuildTime(versionProperties.isShowBuildTime()); + version.setShowBuildVersion(versionProperties.isShowBuildVersion()); + version.setShowGitBranch(versionProperties.isShowGitBranch()); + version.setShowGitCommitId(versionProperties.isShowGitCommitId()); + version.setShowGitShortCommitId(versionProperties.isShowGitShortCommitId()); + version.setShowGitCommitTime(versionProperties.isShowGitCommitTime()); + return version; + } } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ThemingAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ThemingAutoConfiguration.java new file mode 100644 index 00000000..b371552b --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ThemingAutoConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 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.shell.boot; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.shell.style.TemplateExecutor; +import org.springframework.shell.style.Theme; +import org.springframework.shell.style.ThemeRegistry; +import org.springframework.shell.style.ThemeResolver; +import org.springframework.shell.style.ThemeSettings; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SpringShellProperties.class) +public class ThemingAutoConfiguration { + + @Bean + public ThemeRegistry themeRegistry(ObjectProvider themes) { + ThemeRegistry registry = new ThemeRegistry(); + registry.register(Theme.of("default", ThemeSettings.themeSettings())); + themes.orderedStream().forEachOrdered(theme -> registry.register(theme)); + return registry; + } + + @Bean + public ThemeResolver themeResolver(ThemeRegistry themeRegistry, SpringShellProperties properties) { + return new ThemeResolver(themeRegistry, properties.getTheme().getName()); + } + + @Bean + public TemplateExecutor templateExecutor(ThemeResolver themeResolver) { + return new TemplateExecutor(themeResolver); + } + +} diff --git a/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories index 4022caba..193d4ae8 100644 --- a/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories @@ -11,4 +11,5 @@ org.springframework.shell.boot.JLineShellAutoConfiguration,\ org.springframework.shell.boot.JCommanderParameterResolverAutoConfiguration,\ org.springframework.shell.boot.ParameterResolverAutoConfiguration,\ org.springframework.shell.boot.StandardAPIAutoConfiguration,\ +org.springframework.shell.boot.ThemingAutoConfiguration,\ org.springframework.shell.boot.StandardCommandsAutoConfiguration diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java index 234f8b28..9ef9577c 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/SpringShellPropertiesTests.java @@ -35,6 +35,7 @@ public class SpringShellPropertiesTests { assertThat(properties.getScript().isEnabled()).isTrue(); assertThat(properties.getInteractive().isEnabled()).isTrue(); assertThat(properties.getNoninteractive().isEnabled()).isTrue(); + assertThat(properties.getTheme().getName()).isEqualTo("default"); assertThat(properties.getCommand().getClear().isEnabled()).isTrue(); assertThat(properties.getCommand().getHelp().isEnabled()).isTrue(); assertThat(properties.getCommand().getHistory().isEnabled()).isTrue(); @@ -43,6 +44,17 @@ public class SpringShellPropertiesTests { assertThat(properties.getCommand().getStacktrace().isEnabled()).isTrue(); assertThat(properties.getCommand().getCompletion().isEnabled()).isTrue(); assertThat(properties.getCommand().getCompletion().getRootCommand()).isNull(); + assertThat(properties.getCommand().getVersion().isEnabled()).isTrue(); + assertThat(properties.getCommand().getVersion().getTemplate()).isNotNull(); + assertThat(properties.getCommand().getVersion().isShowBuildArtifact()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowBuildGroup()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowBuildName()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowBuildTime()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowBuildVersion()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowGitBranch()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowGitCommitId()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowGitShortCommitId()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowGitCommitTime()).isFalse(); }); } @@ -52,6 +64,7 @@ public class SpringShellPropertiesTests { .withPropertyValues("spring.shell.script.enabled=false") .withPropertyValues("spring.shell.interactive.enabled=false") .withPropertyValues("spring.shell.noninteractive.enabled=false") + .withPropertyValues("spring.shell.theme.name=fake") .withPropertyValues("spring.shell.command.clear.enabled=false") .withPropertyValues("spring.shell.command.help.enabled=false") .withPropertyValues("spring.shell.command.history.enabled=false") @@ -60,12 +73,24 @@ public class SpringShellPropertiesTests { .withPropertyValues("spring.shell.command.stacktrace.enabled=false") .withPropertyValues("spring.shell.command.completion.enabled=false") .withPropertyValues("spring.shell.command.completion.root-command=fake") + .withPropertyValues("spring.shell.command.version.enabled=false") + .withPropertyValues("spring.shell.command.version.template=fake") + .withPropertyValues("spring.shell.command.version.show-build-artifact=true") + .withPropertyValues("spring.shell.command.version.show-build-group=true") + .withPropertyValues("spring.shell.command.version.show-build-name=true") + .withPropertyValues("spring.shell.command.version.show-build-time=true") + .withPropertyValues("spring.shell.command.version.show-build-version=false") + .withPropertyValues("spring.shell.command.version.show-git-branch=true") + .withPropertyValues("spring.shell.command.version.show-git-commit-id=true") + .withPropertyValues("spring.shell.command.version.show-git-short-commit-id=true") + .withPropertyValues("spring.shell.command.version.show-git-commit-time=true") .withUserConfiguration(Config1.class) .run((context) -> { SpringShellProperties properties = context.getBean(SpringShellProperties.class); assertThat(properties.getScript().isEnabled()).isFalse(); assertThat(properties.getInteractive().isEnabled()).isFalse(); assertThat(properties.getNoninteractive().isEnabled()).isFalse(); + assertThat(properties.getTheme().getName()).isEqualTo("fake"); assertThat(properties.getCommand().getClear().isEnabled()).isFalse(); assertThat(properties.getCommand().getHelp().isEnabled()).isFalse(); assertThat(properties.getCommand().getHistory().isEnabled()).isFalse(); @@ -74,6 +99,17 @@ public class SpringShellPropertiesTests { assertThat(properties.getCommand().getStacktrace().isEnabled()).isFalse(); assertThat(properties.getCommand().getCompletion().isEnabled()).isFalse(); assertThat(properties.getCommand().getCompletion().getRootCommand()).isEqualTo("fake"); + assertThat(properties.getCommand().getVersion().isEnabled()).isFalse(); + assertThat(properties.getCommand().getVersion().getTemplate()).isEqualTo("fake"); + assertThat(properties.getCommand().getVersion().isShowBuildArtifact()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowBuildGroup()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowBuildName()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowBuildTime()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowBuildVersion()).isFalse(); + assertThat(properties.getCommand().getVersion().isShowGitBranch()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowGitCommitId()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowGitShortCommitId()).isTrue(); + assertThat(properties.getCommand().getVersion().isShowGitCommitTime()).isTrue(); }); } diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ThemingAutoConfigurationTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ThemingAutoConfigurationTests.java new file mode 100644 index 00000000..19cce0ba --- /dev/null +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/ThemingAutoConfigurationTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 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.shell.boot; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.shell.style.TemplateExecutor; +import org.springframework.shell.style.Theme; +import org.springframework.shell.style.ThemeRegistry; +import org.springframework.shell.style.ThemeResolver; +import org.springframework.shell.style.ThemeSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ThemingAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ThemingAutoConfiguration.class)); + + @Test + public void testDefaults() { + this.contextRunner + .run(context -> { + assertThat(context).hasSingleBean(TemplateExecutor.class); + assertThat(context).hasSingleBean(ThemeRegistry.class); + ThemeRegistry registry = context.getBean(ThemeRegistry.class); + assertThat(registry.get("default")).isNotNull(); + assertThat(context).hasSingleBean(ThemeResolver.class); + }); + } + + @Test + public void testRegistersCustomTheme() { + this.contextRunner + .withUserConfiguration(CustomThemeConfig.class) + .run(context -> { + assertThat(context).hasSingleBean(ThemeRegistry.class); + ThemeRegistry registry = context.getBean(ThemeRegistry.class); + assertThat(registry.get("default")).isNotNull(); + assertThat(registry.get("mytheme")).isNotNull(); + }); + } + + @Configuration + static class CustomThemeConfig { + + @Bean + public Theme myTheme() { + return new Theme() { + + @Override + public String getName() { + return "mytheme"; + } + + @Override + public ThemeSettings getSettings() { + return new MyThemeSettings(); + } + }; + } + } + + static class MyThemeSettings extends ThemeSettings { + } +} diff --git a/spring-shell-core/pom.xml b/spring-shell-core/pom.xml index 1ef9a806..eb7b78b0 100644 --- a/spring-shell-core/pom.xml +++ b/spring-shell-core/pom.xml @@ -11,7 +11,7 @@ spring-shell-parent 3.0.0-SNAPSHOT - + Core API and classes for Spring Shell 2 @@ -31,7 +31,10 @@ org.jline jline-terminal-jna - + + org.antlr + ST4 + org.springframework.shell spring-shell-table diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/StringToStyleExpressionRenderer.java b/spring-shell-core/src/main/java/org/springframework/shell/style/StringToStyleExpressionRenderer.java new file mode 100644 index 00000000..a024d099 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/StringToStyleExpressionRenderer.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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.shell.style; + +import java.util.Locale; + +import org.jline.style.StyleExpression; +import org.stringtemplate.v4.AttributeRenderer; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@code ST} {@link AttributeRenderer} which knows to use format string to + * render strings into {@code jline} {@link StyleExpression} based on theming + * settings. + * + * @author Janne Valkealahti + */ +public class StringToStyleExpressionRenderer implements AttributeRenderer { + + private final ThemeResolver themeResolver; + + public StringToStyleExpressionRenderer(ThemeResolver themeResolver) { + Assert.notNull(themeResolver, "themeResolver must be set"); + this.themeResolver = themeResolver; + } + + @Override + public String toString(String value, String formatString, Locale locale) { + if (!StringUtils.hasText(formatString)) { + return value; + } + else { + return String.format("@{%s %s}", themeResolver.resolveTag(formatString), value); + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/TemplateExecutor.java b/spring-shell-core/src/main/java/org/springframework/shell/style/TemplateExecutor.java new file mode 100644 index 00000000..40cd16fb --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/TemplateExecutor.java @@ -0,0 +1,88 @@ +/* + * Copyright 2022 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.shell.style; + +import java.util.Map; + +import org.jline.utils.AttributedString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.stringtemplate.v4.ST; +import org.stringtemplate.v4.STErrorListener; +import org.stringtemplate.v4.STGroup; +import org.stringtemplate.v4.misc.STMessage; + +/** + * Template executor which knows to use styling. + * + * @author Janne Valkealahti + */ +public class TemplateExecutor { + + private final static STErrorListener ERROR_LISTENER = new LoggingSTErrorListener(); + private final ThemeResolver themeResolver; + private StringToStyleExpressionRenderer renderer; + + public TemplateExecutor(ThemeResolver themeResolver) { + this.themeResolver = themeResolver; + renderer = new StringToStyleExpressionRenderer(themeResolver); + } + + /** + * Render template with a given attributes. + * + * @param template the ST template + * @param attributes the ST template attributes + * @return a rendered template + */ + public AttributedString render(String template, Map attributes) { + STGroup group = new STGroup(); + group.setListener(ERROR_LISTENER); + group.registerRenderer(String.class, renderer); + + ST st = new ST(group, template); + if (attributes != null) { + attributes.entrySet().stream().forEach(e -> st.add(e.getKey(), e.getValue())); + } + String templateRendered = st.render(); + return themeResolver.evaluateExpression(templateRendered); + } + + private static class LoggingSTErrorListener implements STErrorListener { + + private final static Logger log = LoggerFactory.getLogger(LoggingSTErrorListener.class); + + @Override + public void compileTimeError(STMessage msg) { + log.error("compileTimeError [{}]", msg); + } + + @Override + public void runTimeError(STMessage msg) { + log.error("runTimeError [{}]", msg); + } + + @Override + public void IOError(STMessage msg) { + log.error("IOError [{}]", msg); + } + + @Override + public void internalError(STMessage msg) { + log.error("internalError [{}]", msg); + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/Theme.java b/spring-shell-core/src/main/java/org/springframework/shell/style/Theme.java new file mode 100644 index 00000000..0aaa4412 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/Theme.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 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.shell.style; + +/** + * Contract representing a theme with its name and settings. + * + * {@link Theme} is a concept where you can request a {@code style} by using a theme {@code tag}. + * At this point an actual style is not known as it's going to get resolved from an + * enable {@code theme}. + * + * @author Janne Valkealahti + */ +public interface Theme { + + /** + * Gets a theme name. + * + * @return a theme name. + */ + String getName(); + + /** + * Gets a theme settings. + * + * @return a theme settings + */ + ThemeSettings getSettings(); + + /** + * Create a {@link Theme}. + * + * @param name the theme name + * @param themeSettings the theme settings + * @return a theme + */ + public static Theme of(String name, ThemeSettings themeSettings) { + return new Theme() { + + @Override + public String getName() { + return name; + } + + @Override + public ThemeSettings getSettings() { + return themeSettings; + } + }; + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java new file mode 100644 index 00000000..798c43c7 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeRegistry.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 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.shell.style; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * Registry which stores {@link Theme}'s with its name. + * + * @author Janne Valkealahti + */ +public class ThemeRegistry { + + private final Map themes = new HashMap<>(); + + /** + * Gets a theme from a registry. + * + * @param name the theme name + * @return a theme + */ + public Theme get(String name) { + return themes.get(name); + } + + /** + * Register a theme. + * + * @param theme the theme + */ + public void register(Theme theme) { + Assert.notNull(theme, "theme cannot be null"); + themes.put(theme.getName(), theme); + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java new file mode 100644 index 00000000..577c019e --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeResolver.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 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.shell.style; + +import org.jline.style.MemoryStyleSource; +import org.jline.style.StyleExpression; +import org.jline.style.StyleResolver; +import org.jline.style.StyleSource; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; + +/** + * Service which helps to do various things with styles. + * + * @author Janne Valkealahti + */ +public class ThemeResolver { + + private StyleSource styleSource = new MemoryStyleSource(); + private StyleResolver styleResolver = new StyleResolver(styleSource, "default"); + private StyleExpression styleExpression = new StyleExpression(styleResolver); + private final Theme theme; + + public ThemeResolver(ThemeRegistry themeRegistry, String themeName) { + this.theme = themeRegistry.get(themeName); + } + + /** + * Evaluate expression. + * + * @param expression the expression + * @return evaluated attributed string + */ + public AttributedString evaluateExpression(String expression) { + return styleExpression.evaluate(expression); + } + + /** + * Resolve style from a tag with activated theme. + * + * @param tag the tag + * @return a style + */ + public String resolveTag(String tag) { + return theme.getSettings().resolveTag(tag); + } + + /** + * Resolve {@link AttributedStyle} from a {@code spec}. + * + * @param spec the spec + * @return resolved attributed style + */ + public AttributedStyle resolveStyle(String spec) { + return styleResolver.resolve(spec); + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeSettings.java b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeSettings.java new file mode 100644 index 00000000..38a2e753 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/style/ThemeSettings.java @@ -0,0 +1,99 @@ +/* + * Copyright 2022 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.shell.style; + +/** + * {@link ThemeSettings} is storing {@link Theme} related settings together and + * is base class for further customizations. + * + * Settings in this base class are default styles. + * + * @author Janne Valkealahti + */ +public abstract class ThemeSettings { + + /** + * Represents some arbitrary {@code title}. + */ + public final static String TAG_TITLE = "title"; + + /** + * Represents some arbitrary {@code value}. + */ + public final static String TAG_VALUE = "value"; + + /** + * Styling for keys or names in a lists: + * : list value1 + * : list value2 + */ + public final static String TAG_LIST_KEY = "list-key"; + + /** + * Styling for keys or names in a lists: + * list key1 : + * list key2 : + */ + public final static String TAG_LIST_VALUE = "list-value"; + + public String title() { + return "bold"; + } + + public String value() { + return "fg:blue"; + } + + public String listKey() { + return null; + } + + public String listValue() { + return "bold,fg:green"; + } + + /** + * Resolve a theme setting from a given tag. + * + * @param tag the tag + * @return a theme setting + */ + public String resolveTag(String tag) { + switch (tag) { + case TAG_TITLE: + return title(); + case TAG_VALUE: + return value(); + case TAG_LIST_KEY: + return listKey(); + case TAG_LIST_VALUE: + return listValue(); + } + throw new IllegalArgumentException(String.format("Unknown tag '%s'", tag)); + } + + /** + * Creates an instance of a default settings. + * + * @return a default theme settings + */ + public static ThemeSettings themeSettings() { + return new DefaultThemeSettings(); + } + + private static class DefaultThemeSettings extends ThemeSettings { + } +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/style/TemplateExecutorTests.java b/spring-shell-core/src/test/java/org/springframework/shell/style/TemplateExecutorTests.java new file mode 100644 index 00000000..8587967d --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/style/TemplateExecutorTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 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.shell.style; + +import java.util.HashMap; +import java.util.Map; + +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.jline.utils.AttributedStyle.BOLD; + +public class TemplateExecutorTests { + + private ThemeResolver themeResolver; + + @BeforeEach + public void setup() { + ThemeRegistry themeRegistry = new ThemeRegistry(); + themeRegistry.register(new Theme() { + @Override + public String getName() { + return "default"; + } + + @Override + public ThemeSettings getSettings() { + return ThemeSettings.themeSettings(); + } + }); + themeResolver = new ThemeResolver(themeRegistry, "default"); + } + + @Test + public void testSimple() { + TemplateExecutor executor = new TemplateExecutor(themeResolver); + String template = "<(\"foo\")>"; + AttributedString result = executor.render(template, null); + AttributedString equalTo = new AttributedStringBuilder().append("foo").toAttributedString(); + assertThat(result).isEqualTo(equalTo); + } + + @Test + public void testWithExpression() { + TemplateExecutor executor = new TemplateExecutor(themeResolver); + String template = ""; + Map attributes = new HashMap<>(); + attributes.put("foo", "bar"); + AttributedString result = executor.render(template, attributes); + AttributedString equalTo = new AttributedStringBuilder().append("bar").toAttributedString(); + assertThat(result).isEqualTo(equalTo); + } + + @Test + public void testWithTheme() { + TemplateExecutor executor = new TemplateExecutor(themeResolver); + String template = ""; + Map attributes = new HashMap<>(); + attributes.put("foo", "bar"); + AttributedString result = executor.render(template, attributes); + AttributedString equalTo = new AttributedStringBuilder().append("bar", BOLD).toAttributedString(); + assertThat(result).isEqualTo(equalTo); + } + + @Test + public void testNullDontStyle() { + TemplateExecutor executor = new TemplateExecutor(themeResolver); + String template = ""; + Map attributes = new HashMap<>(); + attributes.put("foo", "bar"); + AttributedString result = executor.render(template, attributes); + AttributedString equalTo = new AttributedStringBuilder().append("bar").toAttributedString(); + assertThat(result).isEqualTo(equalTo); + } +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeRegistryTests.java b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeRegistryTests.java new file mode 100644 index 00000000..5e73412a --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeRegistryTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 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.shell.style; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ThemeRegistryTests { + + @Test + public void test() { + ThemeRegistry registry = new ThemeRegistry(); + Theme theme = Theme.of("name1", ThemeSettings.themeSettings()); + registry.register(theme); + assertThat(registry.get("name1")).isSameAs(theme); + assertThat(registry.get("name2")).isNull(); + } +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java new file mode 100644 index 00000000..0c02f93d --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeResolverTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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.shell.style; + +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStyle; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ThemeResolverTests { + + @Test + public void test() { + ThemeRegistry themeRegistry = new ThemeRegistry(); + themeRegistry.register(new Theme() { + @Override + public String getName() { + return "default"; + } + + @Override + public ThemeSettings getSettings() { + return ThemeSettings.themeSettings(); + } + }); + ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default"); + assertThat(themeResolver.resolveTag(ThemeSettings.TAG_TITLE)).isEqualTo("bold"); + assertThat(themeResolver.resolveStyle("bold")).isEqualTo(AttributedStyle.BOLD); + assertThat(themeResolver.evaluateExpression("@{bold foo}")) + .isEqualTo(new AttributedString("foo", AttributedStyle.BOLD)); + } +} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java new file mode 100644 index 00000000..3796d799 --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/style/ThemeSettingsTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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.shell.style; + +import org.jline.style.MemoryStyleSource; +import org.jline.style.StyleResolver; +import org.jline.style.StyleSource; +import org.jline.utils.AttributedStyle; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ThemeSettingsTests { + + @Test + public void test() { + ThemeSettings themeSettings = ThemeSettings.themeSettings(); + String resolveTag = themeSettings.resolveTag(ThemeSettings.TAG_TITLE); + assertThat(resolveTag).isEqualTo("bold"); + + StyleSource styleSource = new MemoryStyleSource(); + StyleResolver styleResolver = new StyleResolver(styleSource, "default"); + AttributedStyle style = styleResolver.resolve(resolveTag); + assertThat(style).isEqualTo(AttributedStyle.BOLD); + } + +} diff --git a/spring-shell-samples/pom.xml b/spring-shell-samples/pom.xml index 5ee588e5..c61d123c 100644 --- a/spring-shell-samples/pom.xml +++ b/spring-shell-samples/pom.xml @@ -19,6 +19,13 @@ org.springframework.boot spring-boot-maven-plugin + + + + build-info + + + diff --git a/spring-shell-samples/src/main/resources/application.yml b/spring-shell-samples/src/main/resources/application.yml index 41888623..ec0c27d2 100644 --- a/spring-shell-samples/src/main/resources/application.yml +++ b/spring-shell-samples/src/main/resources/application.yml @@ -1,6 +1,3 @@ -logging: - level: - root: error spring: main: banner-mode: off @@ -8,3 +5,15 @@ spring: command: completion: root-command: spring-shell-samples +## disable console logging +logging: + pattern: + console: +## log debug from a cli +# file: +# name: shell.log +# level: +# root: debug +# org: +# springframework: +# shell: debug diff --git a/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Version.java b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Version.java new file mode 100644 index 00000000..9e8455a7 --- /dev/null +++ b/spring-shell-standard-commands/src/main/java/org/springframework/shell/standard/commands/Version.java @@ -0,0 +1,173 @@ +/* + * Copyright 2022 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.shell.standard.commands; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.jline.utils.AttributedString; + +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.shell.standard.AbstractShellComponent; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.style.TemplateExecutor; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.StringUtils; + +/** + * Command to list version and other build related infos. + * + * @author Janne Valkealahti + */ +@ShellComponent +public class Version extends AbstractShellComponent implements ResourceLoaderAware { + + /** + * Marker interface used in auto-config. + */ + public interface Command { + } + + private BuildProperties buildProperties; + private GitProperties gitProperties; + private ResourceLoader resourceLoader; + private TemplateExecutor templateExecutor; + private String template; + private boolean showBuildGroup; + private boolean showBuildArtifact; + private boolean showBuildName; + private boolean showBuildVersion; + private boolean showBuildTime; + private boolean showGitBranch; + private boolean showGitCommitId; + private boolean showGitShortCommitId; + private boolean showGitCommitTime; + + public Version(TemplateExecutor templateExecutor) { + this.templateExecutor = templateExecutor; + } + + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @ShellMethod(key = "version", value = "Show version info") + public AttributedString version() { + String templateResource = resourceAsString(resourceLoader.getResource(template)); + + Map attributes = new HashMap<>(); + if (buildProperties != null) { + if (showBuildGroup && StringUtils.hasText(buildProperties.getGroup())) { + attributes.put("buildGroup", buildProperties.getGroup()); + } + if (showBuildArtifact && StringUtils.hasText(buildProperties.getArtifact())) { + attributes.put("buildArtifact", buildProperties.getArtifact()); + } + if (showBuildName && StringUtils.hasText(buildProperties.getName())) { + attributes.put("buildName", buildProperties.getName()); + } + if (showBuildVersion && StringUtils.hasText(buildProperties.getVersion())) { + attributes.put("buildVersion", buildProperties.getVersion()); + } + if (showBuildTime && buildProperties.getTime() != null) { + attributes.put("buildTime", buildProperties.getTime().toString()); + } + } + if (gitProperties != null) { + if (showGitBranch && StringUtils.hasText(gitProperties.getBranch())) { + attributes.put("gitBranch", gitProperties.getBranch()); + } + if (showGitCommitId && StringUtils.hasText(gitProperties.getCommitId())) { + attributes.put("gitCommitId", gitProperties.getCommitId()); + } + if (showGitShortCommitId && StringUtils.hasText(gitProperties.getShortCommitId())) { + attributes.put("gitShortCommitId", gitProperties.getShortCommitId()); + } + if (showGitCommitTime && gitProperties.getCommitTime() != null) { + attributes.put("gitCommitTime", gitProperties.getCommitTime().toString()); + } + } + AttributedString rendered = templateExecutor.render(templateResource, attributes); + return rendered; + } + + public void setBuildProperties(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } + + public void setGitProperties(GitProperties gitProperties) { + this.gitProperties = gitProperties; + } + + public void setTemplate(String template) { + this.template = template; + } + + public void setShowBuildGroup(boolean showBuildGroup) { + this.showBuildGroup = showBuildGroup; + } + + public void setShowBuildArtifact(boolean showBuildArtifact) { + this.showBuildArtifact = showBuildArtifact; + } + + public void setShowBuildName(boolean showBuildName) { + this.showBuildName = showBuildName; + } + + public void setShowBuildVersion(boolean showBuildVersion) { + this.showBuildVersion = showBuildVersion; + } + + public void setShowBuildTime(boolean showBuildTime) { + this.showBuildTime = showBuildTime; + } + + public void setShowGitBranch(boolean showGitBranch) { + this.showGitBranch = showGitBranch; + } + + public void setShowGitCommitId(boolean showGitCommitId) { + this.showGitCommitId = showGitCommitId; + } + + public void setShowGitShortCommitId(boolean showGitShortCommitId) { + this.showGitShortCommitId = showGitShortCommitId; + } + + public void setShowGitCommitTime(boolean showGitCommitTime) { + this.showGitCommitTime = showGitCommitTime; + } + + private static String resourceAsString(Resource resource) { + try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { + return FileCopyUtils.copyToString(reader); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json b/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json index 9f362800..573f1898 100644 --- a/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json +++ b/spring-shell-standard/src/main/resources/META-INF/native-image/resource-config.json @@ -3,7 +3,10 @@ "includes": [ { "pattern": "completion/.*" + }, + { + "pattern": "template/.*.st" } ] } -} \ No newline at end of file +} diff --git a/spring-shell-standard/src/main/resources/template/version-default.st b/spring-shell-standard/src/main/resources/template/version-default.st new file mode 100644 index 00000000..dfab31b3 --- /dev/null +++ b/spring-shell-standard/src/main/resources/template/version-default.st @@ -0,0 +1,27 @@ + +<("Build Version"); format="list-key">: + + +<("Build Group"); format="list-key">: + + +<("Build Artifact"); format="list-key">: + + +<("Build Name"); format="list-key">: + + +<("Build Time"); format="list-key">: + + +<("Git Short Commit Id"); format="list-key">: + + +<("Git Commit Id"); format="list-key">: + + +<("Git Branch"); format="list-key">: + + +<("Git Commit Time"); format="list-key">: +