From 12a6b787127862e62c0019b13f26dd0e5071b609 Mon Sep 17 00:00:00 2001 From: Piotr Olaszewski Date: Tue, 28 Oct 2025 11:03:35 +0100 Subject: [PATCH 1/5] Add JSpecify to spring-shell-autoconfigure Signed-off-by: Piotr Olaszewski --- .mvn/jvm.config | 10 ++++ pom.xml | 56 +++++++++++++++++++ spring-shell-autoconfigure/pom.xml | 5 ++ .../boot/CompleterAutoConfiguration.java | 11 +++- .../boot/LineReaderAutoConfiguration.java | 5 +- .../shell/boot/SpringShellProperties.java | 40 ++++++------- .../boot/UserConfigAutoConfiguration.java | 17 ++++-- .../shell/boot/condition/package-info.java | 4 ++ .../shell/boot/package-info.java | 4 ++ spring-shell-standard/pom.xml | 5 ++ spring-shell-table/pom.xml | 5 ++ spring-shell-test-autoconfigure/pom.xml | 5 ++ spring-shell-test/pom.xml | 6 +- 13 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 .mvn/jvm.config create mode 100644 spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/condition/package-info.java create mode 100644 spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/package-info.java diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 000000000..32599cefe --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index 19c49a7e2..877f13a6b 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,9 @@ 2.20.0 2.0.17 3.1.1 + 2.36.0 + 0.12.7 + 1.0.0 1.3.1 @@ -228,6 +231,59 @@ + + + nullaway + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + default-compile + none + + + java-compile + compile + + compile + + + + + com.google.errorprone + error_prone_core + ${errorprone.version} + + + com.uber.nullaway + nullaway + ${nullaway.version} + + + + -XDcompilePolicy=simple + --should-stop=ifError=FLOW + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR + -XepOpt:NullAway:OnlyNullMarked=true + -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract + + + + + + + + + + + maven-central diff --git a/spring-shell-autoconfigure/pom.xml b/spring-shell-autoconfigure/pom.xml index ba8bbd5ff..7a78a8312 100644 --- a/spring-shell-autoconfigure/pom.xml +++ b/spring-shell-autoconfigure/pom.xml @@ -47,6 +47,11 @@ ${spring-boot.version} true + + org.jspecify + jspecify + ${jspecify.version} + diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CompleterAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CompleterAutoConfiguration.java index 5b1a75365..23ba08183 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CompleterAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CompleterAutoConfiguration.java @@ -31,6 +31,9 @@ import org.springframework.shell.CompletionProposal; import org.springframework.shell.Shell; +/** + * @author Piotr Olaszewski + */ @AutoConfiguration public class CompleterAutoConfiguration { @@ -39,15 +42,17 @@ public class CompleterAutoConfiguration { @Bean public CompleterAdapter completer() { - CompleterAdapter completerAdapter = new CompleterAdapter(); - completerAdapter.setShell(shell); - return completerAdapter; + return new CompleterAdapter(shell); } public static class CompleterAdapter implements Completer { private Shell shell; + public CompleterAdapter(Shell shell) { + this.shell = shell; + } + @Override public void complete(LineReader reader, ParsedLine line, List candidates) { CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t; diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/LineReaderAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/LineReaderAutoConfiguration.java index bfba557d0..016bf31e6 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/LineReaderAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/LineReaderAutoConfiguration.java @@ -42,6 +42,9 @@ import org.springframework.shell.config.UserConfigPathProvider; import org.springframework.util.StringUtils; +/** + * @author Piotr Olaszewski + */ @AutoConfiguration @EnableConfigurationProperties(SpringShellProperties.class) public class LineReaderAutoConfiguration { @@ -59,7 +62,7 @@ public class LineReaderAutoConfiguration { private org.jline.reader.History jLineHistory; @Value("${spring.application.name:spring-shell}.log") - private String fallbackHistoryFileName; + private String fallbackHistoryFileName = "spring-shell.log"; private SpringShellProperties springShellProperties; private UserConfigPathProvider userConfigPathProvider; 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 50b63754d..1f64e28ca 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 @@ -15,12 +15,14 @@ */ package org.springframework.shell.boot; +import org.jspecify.annotations.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; /** * Configuration properties for shell. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ @ConfigurationProperties(prefix = "spring.shell") public class SpringShellProperties { @@ -118,36 +120,36 @@ public void setContext(Context context) { public static class Config { - private String env; - private String location; + private @Nullable String env; + private @Nullable String location; - public String getEnv() { + public @Nullable String getEnv() { return env; } - public void setEnv(String env) { + public void setEnv(@Nullable String env) { this.env = env; } - public String getLocation() { + public @Nullable String getLocation() { return location; } - public void setLocation(String location) { + public void setLocation(@Nullable String location) { this.location = location; } } public static class History { - private String name; + private @Nullable String name; private boolean enabled = true; - public String getName() { + public @Nullable String getName() { return name; } - public void setName(String name) { + public void setName(@Nullable String name) { this.name = name; } @@ -189,7 +191,7 @@ public void setEnabled(boolean enabled) { public static class Noninteractive { private boolean enabled = true; - private String primaryCommand; + private @Nullable String primaryCommand; public boolean isEnabled() { return enabled; @@ -199,24 +201,24 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public String getPrimaryCommand() { + public @Nullable String getPrimaryCommand() { return primaryCommand; } - public void setPrimaryCommand(String primaryCommand) { + public void setPrimaryCommand(@Nullable String primaryCommand) { this.primaryCommand = primaryCommand; } } public static class Theme { - private String name; + private @Nullable String name; - public String getName() { + public @Nullable String getName() { return name; } - public void setName(String name) { + public void setName(@Nullable String name) { this.name = name; } } @@ -334,7 +336,7 @@ public void setEnabled(boolean enabled) { public static class CompletionCommand { private boolean enabled = true; - private String rootCommand; + private @Nullable String rootCommand; public boolean isEnabled() { return enabled; @@ -344,11 +346,11 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public String getRootCommand() { + public @Nullable String getRootCommand() { return rootCommand; } - public void setRootCommand(String rootCommand) { + public void setRootCommand(@Nullable String rootCommand) { this.rootCommand = rootCommand; } } @@ -625,7 +627,7 @@ public void setClose(boolean close) { } } - public static enum OptionNamingCase { + public enum OptionNamingCase { NOOP, CAMEL, SNAKE, diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/UserConfigAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/UserConfigAutoConfiguration.java index 6fe68b615..06856a3a5 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/UserConfigAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/UserConfigAutoConfiguration.java @@ -19,6 +19,7 @@ import java.nio.file.Paths; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -26,6 +27,9 @@ import org.springframework.shell.config.UserConfigPathProvider; import org.springframework.util.StringUtils; +/** + * @author Piotr Olaszewski + */ @AutoConfiguration @EnableConfigurationProperties(SpringShellProperties.class) public class UserConfigAutoConfiguration { @@ -42,14 +46,15 @@ public UserConfigPathProvider userConfigPathProvider(SpringShellProperties sprin static class LocationResolver { - private final static String XDG_CONFIG_HOME = "XDG_CONFIG_HOME"; - private final static String APP_DATA = "APP_DATA"; + private static final String XDG_CONFIG_HOME = "XDG_CONFIG_HOME"; + private static final String APP_DATA = "APP_DATA"; private static final String USERCONFIG_PLACEHOLDER = "{userconfig}"; - private Function pathProvider = (path) -> Paths.get(path); - private final String configDirEnv; - private final String configDirLocation; - LocationResolver(String configDirEnv, String configDirLocation) { + private final Function pathProvider = Paths::get; + private final @Nullable String configDirEnv; + private final @Nullable String configDirLocation; + + LocationResolver(@Nullable String configDirEnv, @Nullable String configDirLocation) { this.configDirEnv = configDirEnv; this.configDirLocation = configDirLocation; } diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/condition/package-info.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/condition/package-info.java new file mode 100644 index 000000000..5113395c8 --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/condition/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.boot.condition; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/package-info.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/package-info.java new file mode 100644 index 000000000..0fe2e52b5 --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.boot; + +import org.jspecify.annotations.NullMarked; \ No newline at end of file diff --git a/spring-shell-standard/pom.xml b/spring-shell-standard/pom.xml index 26577df71..23949a8b1 100644 --- a/spring-shell-standard/pom.xml +++ b/spring-shell-standard/pom.xml @@ -41,6 +41,11 @@ spring-shell-tui ${project.parent.version} + + org.jspecify + jspecify + ${jspecify.version} + diff --git a/spring-shell-table/pom.xml b/spring-shell-table/pom.xml index d28c81298..ecab319bb 100644 --- a/spring-shell-table/pom.xml +++ b/spring-shell-table/pom.xml @@ -36,6 +36,11 @@ spring-beans ${spring-framework.version} + + org.jspecify + jspecify + ${jspecify.version} + diff --git a/spring-shell-test-autoconfigure/pom.xml b/spring-shell-test-autoconfigure/pom.xml index ed38fc14a..2c33c5f34 100644 --- a/spring-shell-test-autoconfigure/pom.xml +++ b/spring-shell-test-autoconfigure/pom.xml @@ -56,6 +56,11 @@ junit-jupiter ${junit-jupiter.version} + + org.jspecify + jspecify + ${jspecify.version} + diff --git a/spring-shell-test/pom.xml b/spring-shell-test/pom.xml index 5bd4509f1..fe2a39a8b 100644 --- a/spring-shell-test/pom.xml +++ b/spring-shell-test/pom.xml @@ -51,7 +51,11 @@ assertj-core ${assertj.version} - + + org.jspecify + jspecify + ${jspecify.version} + From 92a517a163b31a1d0495f3d46051acb358061988 Mon Sep 17 00:00:00 2001 From: Piotr Olaszewski Date: Thu, 30 Oct 2025 20:49:33 +0100 Subject: [PATCH 2/5] Add JSpecify to spring-shell-core Signed-off-by: Piotr Olaszewski --- spring-shell-core/pom.xml | 5 + .../springframework/shell/Availability.java | 8 +- .../org/springframework/shell/Command.java | 10 +- .../shell/CommandNotFound.java | 13 +- .../shell/CompletingParsedLine.java | 3 +- .../shell/CompletionContext.java | 35 ++-- .../shell/CompletionProposal.java | 13 +- .../shell/CoreResourcesRuntimeHints.java | 4 +- .../shell/DefaultShellApplicationRunner.java | 0 .../springframework/shell/InputProvider.java | 5 +- .../shell/JnaRuntimeHints.java | 6 +- .../shell/ResultHandlerService.java | 3 +- .../java/org/springframework/shell/Shell.java | 22 ++- .../springframework/shell/ValueResult.java | 5 +- .../shell/command/CommandAlias.java | 13 +- .../shell/command/CommandCatalog.java | 8 +- .../shell/command/CommandContext.java | 13 +- .../CommandContextMethodArgumentResolver.java | 4 +- .../command/CommandExceptionResolver.java | 4 +- .../shell/command/CommandExecution.java | 51 ++++-- .../shell/command/CommandHandlingResult.java | 20 +-- .../shell/command/CommandOption.java | 48 ++--- .../shell/command/CommandParser.java | 28 +-- .../CommandParserExceptionResolver.java | 4 +- .../shell/command/CommandRegistration.java | 168 +++++++++--------- .../ExceptionResolverMethodResolver.java | 14 +- .../MethodCommandExceptionResolver.java | 15 +- .../command/annotation/package-info.java | 4 + .../support/CommandAnnotationUtils.java | 3 +- .../CommandRegistrationBeanRegistrar.java | 7 +- .../CommandRegistrationFactoryBean.java | 35 ++-- .../support/CommandScanRegistrar.java | 11 +- .../support/OptionMethodArgumentResolver.java | 5 +- .../annotation/support/package-info.java | 4 + .../invocation/InvocableShellMethod.java | 48 +++-- .../ShellMethodArgumentResolverComposite.java | 9 +- .../command/invocation/package-info.java | 4 + .../shell/command/package-info.java | 4 + .../shell/command/parser/Ast.java | 9 +- .../command/parser/CommandArgumentNode.java | 9 +- .../shell/command/parser/CommandModel.java | 27 +-- .../shell/command/parser/DirectiveNode.java | 9 +- .../shell/command/parser/Lexer.java | 3 + .../shell/command/parser/MessageResult.java | 7 +- .../shell/command/parser/Parser.java | 26 ++- .../shell/command/parser/ParserMessage.java | 7 +- .../shell/command/parser/package-info.java | 4 + .../shell/command/support/package-info.java | 4 + ...RegistrationOptionsCompletionResolver.java | 9 +- .../shell/completion/package-info.java | 4 + .../shell/config/package-info.java | 4 + .../shell/context/package-info.java | 4 + .../shell/exit/package-info.java | 4 + .../shell/jline/ExtendedDefaultParser.java | 6 +- .../shell/jline/FileInputProvider.java | 4 +- .../jline/NonInteractiveShellRunner.java | 8 +- .../shell/jline/package-info.java | 5 +- .../springframework/shell/package-info.java | 5 +- .../CommandNotFoundMessageProvider.java | 12 +- .../result/CommandNotFoundResultHandler.java | 4 +- .../result/GenericResultHandlerService.java | 60 +++---- .../ResultHandlerNotFoundException.java | 11 +- .../shell/result/ThrowableResultHandler.java | 12 +- .../shell/result/package-info.java | 3 + ...bstractArgumentMethodArgumentResolver.java | 24 ++- .../shell/support/package-info.java | 4 + .../org/springframework/shell/ShellTests.java | 8 + ...CommandExecutionCustomConversionTests.java | 13 +- .../shell/command/CommandExecutionTests.java | 13 +- .../tui/component/ConfirmationInput.java | 30 ++-- .../tui/component/MultiItemSelector.java | 16 +- .../shell/tui/component/PathInput.java | 16 +- .../shell/tui/component/PathSearch.java | 28 +-- .../tui/component/SingleItemSelector.java | 16 +- .../shell/tui/component/StringInput.java | 56 +++--- .../tui/component/ViewComponentExecutor.java | 4 +- .../context/BaseComponentContext.java | 13 +- .../component/context/ComponentContext.java | 7 +- .../tui/component/context/package-info.java | 4 + .../component/flow/BaseConfirmationInput.java | 28 +-- .../component/flow/BaseMultiItemSelector.java | 30 ++-- .../tui/component/flow/BasePathInput.java | 30 ++-- .../flow/BaseSingleItemSelector.java | 38 ++-- .../tui/component/flow/BaseStringInput.java | 34 ++-- .../tui/component/flow/ComponentFlow.java | 114 ++++++++---- .../tui/component/flow/package-info.java | 4 + .../message/ShellMessageBuilder.java | 6 +- .../message/ShellMessageHeaderAccessor.java | 20 +-- .../StaticShellMessageHeaderAccessor.java | 20 +-- .../tui/component/message/package-info.java | 4 + .../shell/tui/component/package-info.java | 4 + .../component/support/AbstractComponent.java | 24 ++- .../support/AbstractSelectorComponent.java | 77 ++++---- .../support/AbstractTextComponent.java | 57 +++--- .../tui/component/support/Matchable.java | 7 +- .../tui/component/support/SelectorItem.java | 6 +- .../tui/component/support/SelectorList.java | 9 +- .../tui/component/support/package-info.java | 4 + .../shell/tui/component/view/TerminalUI.java | 45 +++-- .../tui/component/view/TerminalUIBuilder.java | 10 +- .../view/control/AbstractControl.java | 17 +- .../component/view/control/AbstractView.java | 52 +++--- .../tui/component/view/control/AppView.java | 94 +++++++--- .../tui/component/view/control/BoxView.java | 6 +- .../component/view/control/ButtonView.java | 12 +- .../tui/component/view/control/Control.java | 3 +- .../component/view/control/DialogView.java | 29 +-- .../tui/component/view/control/GridView.java | 15 +- .../tui/component/view/control/InputView.java | 9 +- .../tui/component/view/control/ListView.java | 13 +- .../component/view/control/MenuBarView.java | 30 ++-- .../tui/component/view/control/MenuView.java | 23 ++- .../component/view/control/ProgressView.java | 22 +-- .../component/view/control/StatusBarView.java | 32 ++-- .../tui/component/view/control/View.java | 6 +- .../component/view/control/ViewService.java | 6 +- .../view/control/cell/AbstractListCell.java | 8 +- .../view/control/cell/package-info.java | 4 + .../component/view/control/package-info.java | 4 + .../view/event/DefaultEventLoop.java | 9 +- .../tui/component/view/event/KeyEvent.java | 6 +- .../tui/component/view/event/KeyHandler.java | 7 +- .../component/view/event/MouseHandler.java | 7 +- .../component/view/event/package-info.java | 4 + .../view/event/processor/package-info.java | 4 + .../tui/component/view/package-info.java | 4 + .../component/view/screen/DefaultScreen.java | 18 +- .../tui/component/view/screen/Screen.java | 5 +- .../tui/component/view/screen/ScreenItem.java | 5 +- .../component/view/screen/package-info.java | 4 + .../shell/tui/geom/package-info.java | 4 + .../shell/tui/style/PartsTextRenderer.java | 18 +- .../StringToStyleExpressionRenderer.java | 15 +- .../shell/tui/style/StyleSettings.java | 11 +- .../shell/tui/style/TemplateExecutor.java | 6 +- .../shell/tui/style/ThemeRegistry.java | 4 +- .../shell/tui/style/ThemeResolver.java | 13 +- .../shell/tui/style/package-info.java | 4 + .../tui/support/search/SearchMatchResult.java | 13 +- .../tui/support/search/package-info.java | 4 + .../tui/style/PartsTextRendererTests.java | 72 ++++++++ .../StringToStyleExpressionRendererTests.java | 10 ++ 142 files changed, 1474 insertions(+), 897 deletions(-) create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/annotation/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/invocation/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/parser/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/command/support/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/completion/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/config/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/context/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/exit/package-info.java create mode 100644 spring-shell-core/src/main/java/org/springframework/shell/support/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java create mode 100644 spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java diff --git a/spring-shell-core/pom.xml b/spring-shell-core/pom.xml index 96f457043..fcab87209 100644 --- a/spring-shell-core/pom.xml +++ b/spring-shell-core/pom.xml @@ -61,6 +61,11 @@ jakarta.validation-api ${jakarta.validation-api.version} + + org.jspecify + jspecify + ${jspecify.version} + diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Availability.java b/spring-shell-core/src/main/java/org/springframework/shell/Availability.java index 0175eba61..475f87372 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Availability.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Availability.java @@ -16,6 +16,7 @@ package org.springframework.shell; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -23,12 +24,13 @@ * a reason. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class Availability { - private final String reason; + private final @Nullable String reason; - private Availability(String reason) { + private Availability(@Nullable String reason) { this.reason = reason; } @@ -45,7 +47,7 @@ public boolean isAvailable() { return reason == null; } - public String getReason() { + public @Nullable String getReason() { return reason; } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Command.java b/spring-shell-core/src/main/java/org/springframework/shell/Command.java index dbd5712e9..a5586953f 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Command.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Command.java @@ -18,9 +18,13 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +/** + * @author Piotr Olaszewski + */ public interface Command { /** @@ -45,10 +49,10 @@ public Help(String description) { this(description, null); } - public Help(String description, String group) { - this.group = StringUtils.hasText(group) ? group : ""; - Assert.isTrue(StringUtils.hasText(description), "Command description cannot be null or empty in group='" + this.group + "'"); + public Help(String description, @Nullable String group) { + Assert.isTrue(StringUtils.hasText(description), "Command description cannot be null or empty"); this.description = description; + this.group = StringUtils.hasText(group) ? group : ""; } public String getDescription() { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CommandNotFound.java b/spring-shell-core/src/main/java/org/springframework/shell/CommandNotFound.java index 61ba13cea..ff88c16c4 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/CommandNotFound.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/CommandNotFound.java @@ -20,22 +20,25 @@ import java.util.Map; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.shell.command.CommandRegistration; /** * A result to be handled by the {@link ResultHandler} when no command could be mapped to user input + * + * @author Piotr Olaszewski */ public class CommandNotFound extends RuntimeException { private final List words; - private final Map registrations; - private final String text; + private final @Nullable Map registrations; + private final @Nullable String text; public CommandNotFound(List words) { this(words, null, null); } - public CommandNotFound(List words, Map registrations, String text) { + public CommandNotFound(List words, @Nullable Map registrations, @Nullable String text) { this.words = words; this.registrations = registrations; this.text = text; @@ -60,7 +63,7 @@ public List getWords(){ * * @return known command registrations */ - public Map getRegistrations() { + public @Nullable Map getRegistrations() { return registrations; } @@ -69,7 +72,7 @@ public Map getRegistrations() { * * @return raw text input */ - public String getText() { + public @Nullable String getText() { return text; } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CompletingParsedLine.java b/spring-shell-core/src/main/java/org/springframework/shell/CompletingParsedLine.java index 30d5c0788..e841f454e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/CompletingParsedLine.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/CompletingParsedLine.java @@ -24,9 +24,10 @@ * should be escaped/quoted. * * @author Eric Bottard + * @author Piotr Olaszewski */ @FunctionalInterface public interface CompletingParsedLine { - public CharSequence emit(CharSequence candidate); + CharSequence emit(CharSequence candidate); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CompletionContext.java b/spring-shell-core/src/main/java/org/springframework/shell/CompletionContext.java index 5a42c1232..74ec22691 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/CompletionContext.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/CompletionContext.java @@ -16,17 +16,19 @@ package org.springframework.shell; +import org.jspecify.annotations.Nullable; +import org.springframework.shell.command.CommandOption; +import org.springframework.shell.command.CommandRegistration; + import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; -import org.springframework.shell.command.CommandOption; -import org.springframework.shell.command.CommandRegistration; - /** * Represents the buffer context in which completion was triggered. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class CompletionContext { @@ -36,17 +38,17 @@ public class CompletionContext { private final int position; - private final CommandOption commandOption; + private final @Nullable CommandOption commandOption; - private final CommandRegistration commandRegistration; + private final @Nullable CommandRegistration commandRegistration; /** * - * @param words words in the buffer, excluding words for the command name + * @param words words in the buffer, excluding words for the command name * @param wordIndex the index of the word the cursor is in - * @param position the position inside the current word where the cursor is + * @param position the position inside the current word where the cursor is */ - public CompletionContext(List words, int wordIndex, int position, CommandRegistration commandRegistration, CommandOption commandOption) { + public CompletionContext(List words, int wordIndex, int position, @Nullable CommandRegistration commandRegistration, @Nullable CommandOption commandOption) { this.words = words; this.wordIndex = wordIndex; this.position = position; @@ -66,11 +68,11 @@ public int getPosition() { return position; } - public CommandOption getCommandOption() { + public @Nullable CommandOption getCommandOption() { return commandOption; } - public CommandRegistration getCommandRegistration() { + public @Nullable CommandRegistration getCommandRegistration() { return commandRegistration; } @@ -80,7 +82,10 @@ public String upToCursor() { if (!start.isEmpty()) { start += " "; } - start += currentWord().substring(0, position); + String currentWord = currentWord(); + if (currentWord != null) { + start += currentWord.substring(0, position); + } } return start; } @@ -88,11 +93,11 @@ public String upToCursor() { /** * Return the whole word the cursor is in, or {@code null} if the cursor is past the last word. */ - public String currentWord() { + public @Nullable String currentWord() { return wordIndex >= 0 && wordIndex < words.size() ? words.get(wordIndex) : null; } - public String currentWordUpToCursor() { + public @Nullable String currentWordUpToCursor() { String currentWord = currentWord(); return currentWord != null ? currentWord.substring(0, getPosition()) : null; } @@ -101,7 +106,7 @@ public String currentWordUpToCursor() { * Return a copy of this context, as if the first {@literal nbWords} were not present */ public CompletionContext drop(int nbWords) { - return new CompletionContext(new ArrayList(words.subList(nbWords, words.size())), wordIndex - nbWords, + return new CompletionContext(new ArrayList<>(words.subList(nbWords, words.size())), wordIndex - nbWords, position, commandRegistration, commandOption); } @@ -115,7 +120,7 @@ public CompletionContext commandOption(CommandOption commandOption) { /** * Return a copy of this context with given command registration. */ - public CompletionContext commandRegistration(CommandRegistration commandRegistration) { + public CompletionContext commandRegistration(@Nullable CommandRegistration commandRegistration) { return new CompletionContext(words, wordIndex, position, commandRegistration, commandOption); } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CompletionProposal.java b/spring-shell-core/src/main/java/org/springframework/shell/CompletionProposal.java index d3c0fa99a..a6b8bf255 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/CompletionProposal.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/CompletionProposal.java @@ -16,12 +16,15 @@ package org.springframework.shell; +import org.jspecify.annotations.Nullable; + import java.util.Objects; /** * Represents a proposal for TAB completion, made not only of the text to append, but also metadata about the proposal. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class CompletionProposal { @@ -38,12 +41,12 @@ public class CompletionProposal { /** * The description for the proposal. */ - private String description; + private @Nullable String description; /** * The category of the proposal, which may be used to group proposals together. */ - private String category; + private @Nullable String category; /** * Whether the proposal should bypass escaping and quoting rules. This is useful for command proposals, which can @@ -79,16 +82,16 @@ public CompletionProposal displayText(String displayText) { return this; } - public String description() { + public @Nullable String description() { return description; } - public CompletionProposal description(String description) { + public CompletionProposal description(@Nullable String description) { this.description = description; return this; } - public String category() { + public @Nullable String category() { return category; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/CoreResourcesRuntimeHints.java b/spring-shell-core/src/main/java/org/springframework/shell/CoreResourcesRuntimeHints.java index b2107d9b9..a2a368a26 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/CoreResourcesRuntimeHints.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/CoreResourcesRuntimeHints.java @@ -15,6 +15,7 @@ */ package org.springframework.shell; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -22,11 +23,12 @@ * {@link RuntimeHintsRegistrar} for Shell Core resources. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class CoreResourcesRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.resources().registerPattern("org/springframework/shell/component/*.stg"); } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/DefaultShellApplicationRunner.java new file mode 100644 index 000000000..e69de29bb diff --git a/spring-shell-core/src/main/java/org/springframework/shell/InputProvider.java b/spring-shell-core/src/main/java/org/springframework/shell/InputProvider.java index aa888e07c..775798e5c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/InputProvider.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/InputProvider.java @@ -1,9 +1,12 @@ package org.springframework.shell; +import org.jspecify.annotations.Nullable; + /** * To be implemented by components able to provide a "line" of user input, whether interactively or by batch. * * @author Eric Bottard + * @author Piotr Olaszewski */ public interface InputProvider { @@ -12,5 +15,5 @@ public interface InputProvider { * *

Returning {@literal null} indicates end of input, requesting shell exit.

*/ - Input readInput(); + @Nullable Input readInput(); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/JnaRuntimeHints.java b/spring-shell-core/src/main/java/org/springframework/shell/JnaRuntimeHints.java index 8db5fa9f4..f7cd7b9ac 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/JnaRuntimeHints.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/JnaRuntimeHints.java @@ -19,6 +19,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ProxyHints; @@ -29,10 +30,13 @@ import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; +/** + * @author Piotr Olaszewski + */ public class JnaRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { ResourceHints resource = hints.resources(); ProxyHints proxy = hints.proxies(); ReflectionHints reflection = hints.reflection(); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/ResultHandlerService.java b/spring-shell-core/src/main/java/org/springframework/shell/ResultHandlerService.java index 4f3e91306..c94c9355c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/ResultHandlerService.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/ResultHandlerService.java @@ -15,13 +15,14 @@ */ package org.springframework.shell; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * A service interface for result handling. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface ResultHandlerService { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index c5c867cf2..9b88cd9d2 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -30,6 +30,7 @@ import jakarta.validation.ValidatorFactory; import org.jline.terminal.Terminal; import org.jline.utils.Signals; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,10 +59,11 @@ * * @author Eric Bottard * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class Shell { - private final static Logger log = LoggerFactory.getLogger(Shell.class); + private static final Logger log = LoggerFactory.getLogger(Shell.class); private final ResultHandlerService resultHandlerService; /** @@ -73,12 +75,12 @@ public class Shell { private final Terminal terminal; private final CommandCatalog commandRegistry; protected List completionResolvers = new ArrayList<>(); - private CommandExecutionHandlerMethodArgumentResolvers argumentResolvers; + private @Nullable CommandExecutionHandlerMethodArgumentResolvers argumentResolvers; private ConversionService conversionService = new DefaultConversionService(); private final ShellContext shellContext; private final ExitCodeMappings exitCodeMappings; - private Exception handlingResultNonInt = null; - private CommandHandlingResult processExceptionNonInt = null; + private @Nullable Exception handlingResultNonInt = null; + private @Nullable CommandHandlingResult processExceptionNonInt = null; /** * Marker object to distinguish unresolved arguments from {@code null}, which is a valid @@ -123,7 +125,7 @@ public void setExceptionResolvers(List exceptionResolv this.exceptionResolvers = exceptionResolvers; } - private ExitCodeExceptionProvider exitCodeExceptionProvider; + private @Nullable ExitCodeExceptionProvider exitCodeExceptionProvider; @Autowired(required = false) public void setExitCodeExceptionProvider(ExitCodeExceptionProvider exitCodeExceptionProvider) { @@ -191,7 +193,7 @@ else if (processExceptionNonInt != null && processExceptionNonInt.exitCode() != * result *

*/ - protected Object evaluate(Input input) { + protected @Nullable Object evaluate(Input input) { if (noInput(input)) { return NO_INPUT; } @@ -354,8 +356,10 @@ public List complete(CompletionContext context) { // Try to complete arguments List matchedArgOptions = new ArrayList<>(); - if (argsContext.getWords().size() > 0 && argsContext.getWordIndex() > 0 && argsContext.getWords().size() > argsContext.getWordIndex()) { - matchedArgOptions.addAll(matchOptions(registration.getOptions(), argsContext.getWords().get(argsContext.getWordIndex() - 1))); + if (!argsContext.getWords().isEmpty() && argsContext.getWordIndex() > 0 && argsContext.getWords().size() > argsContext.getWordIndex()) { + if (registration != null) { + matchedArgOptions.addAll(matchOptions(registration.getOptions(), argsContext.getWords().get(argsContext.getWordIndex() - 1))); + } } List argProposals = matchedArgOptions.stream() @@ -448,7 +452,7 @@ private CompletionProposal toCommandProposal(String command, CommandRegistration * * @return a valid command name, or {@literal null} if none matched */ - private String findLongestCommand(String prefix, boolean filterHidden) { + private @Nullable String findLongestCommand(String prefix, boolean filterHidden) { Map registrations = commandRegistry.getRegistrations(); if (filterHidden) { registrations = Utils.removeHiddenCommands(registrations); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/ValueResult.java b/spring-shell-core/src/main/java/org/springframework/shell/ValueResult.java index 7ae4fce47..ea434939f 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/ValueResult.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/ValueResult.java @@ -19,12 +19,14 @@ import java.util.List; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; /** * A result for a successful resolve operation. * * @author Camilo Gonzalez + * @author Piotr Olaszewski */ public class ValueResult { @@ -40,8 +42,7 @@ public ValueResult(MethodParameter methodParameter, Object resolvedValue) { this(methodParameter, resolvedValue, new BitSet(), new BitSet()); } - public ValueResult(MethodParameter methodParameter, Object resolvedValue, BitSet wordsUsed, - BitSet wordsUsedForValue) { + public ValueResult(MethodParameter methodParameter, Object resolvedValue, @Nullable BitSet wordsUsed, @Nullable BitSet wordsUsedForValue) { this.methodParameter = methodParameter; this.resolvedValue = resolvedValue; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java index 3895c37fc..9fec89637 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandAlias.java @@ -15,10 +15,13 @@ */ package org.springframework.shell.command; +import org.jspecify.annotations.Nullable; + /** * Interface representing an alias in a command. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandAlias { @@ -34,7 +37,7 @@ public interface CommandAlias { * * @return the group */ - String getGroup(); + @Nullable String getGroup(); /** * Gets an instance of a default {@link CommandAlias}. @@ -43,7 +46,7 @@ public interface CommandAlias { * @param group the group * @return default command alias */ - public static CommandAlias of(String command, String group) { + public static CommandAlias of(String command, @Nullable String group) { return new DefaultCommandAlias(command, group); } @@ -53,9 +56,9 @@ public static CommandAlias of(String command, String group) { public static class DefaultCommandAlias implements CommandAlias { private final String command; - private final String group; + private final @Nullable String group; - public DefaultCommandAlias(String command, String group) { + public DefaultCommandAlias(String command, @Nullable String group) { this.command = command; this.group = group; } @@ -66,7 +69,7 @@ public String getCommand() { } @Override - public String getGroup() { + public @Nullable String getGroup() { return group; } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java index 7e1c38a9c..0b1af893e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandCatalog.java @@ -23,6 +23,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.shell.context.InteractionMode; import org.springframework.shell.context.ShellContext; @@ -30,6 +31,7 @@ * Interface defining contract to handle existing {@link CommandRegistration}s. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandCatalog { @@ -89,9 +91,9 @@ static class DefaultCommandCatalog implements CommandCatalog { private final Map commandRegistrations = new HashMap<>(); private final Collection resolvers = new ArrayList<>(); - private final ShellContext shellContext; + private final @Nullable ShellContext shellContext; - DefaultCommandCatalog(Collection resolvers, ShellContext shellContext) { + DefaultCommandCatalog(@Nullable Collection resolvers, @Nullable ShellContext shellContext) { this.shellContext = shellContext; if (resolvers != null) { this.resolvers.addAll(resolvers); @@ -146,7 +148,7 @@ public Map getRegistrations() { * effectively disables filtering as as we only care if mode is set to interactive * or non-interactive. */ - private static Predicate> filterByInteractionMode(ShellContext shellContext) { + private static Predicate> filterByInteractionMode(@Nullable ShellContext shellContext) { return e -> { InteractionMode mim = e.getValue().getInteractionMode(); InteractionMode cim = shellContext != null ? shellContext.getInteractionMode() : InteractionMode.ALL; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java index 17daa5a32..b098667aa 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContext.java @@ -21,15 +21,18 @@ import org.jline.terminal.Terminal; +import org.jspecify.annotations.Nullable; import org.springframework.shell.command.CommandParser.CommandParserResult; import org.springframework.shell.command.CommandParser.CommandParserResults; import org.springframework.shell.context.ShellContext; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * Interface containing information about current command execution. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandContext { @@ -69,7 +72,7 @@ public interface CommandContext { * @param name the option name * @return mapped value */ - T getOptionValue(String name); + @Nullable T getOptionValue(String name); /** * Gets a terminal. @@ -94,8 +97,10 @@ public interface CommandContext { * @param commandRegistration the command registration * @return a command context */ - static CommandContext of(String[] args, CommandParserResults results, Terminal terminal, - CommandRegistration commandRegistration, ShellContext shellContext) { + static CommandContext of(String[] args, CommandParserResults results, @Nullable Terminal terminal, + CommandRegistration commandRegistration, @Nullable ShellContext shellContext) { + Assert.notNull(terminal, "'terminal' must not be null"); + Assert.notNull(shellContext, "'shellContext' must not be null"); return new DefaultCommandContext(args, results, terminal, commandRegistration, shellContext); } @@ -141,7 +146,7 @@ public CommandRegistration getCommandRegistration() { @Override @SuppressWarnings("unchecked") - public T getOptionValue(String name) { + public @Nullable T getOptionValue(String name) { Optional find = find(name); if (find.isPresent()) { return (T) find.get().value(); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContextMethodArgumentResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContextMethodArgumentResolver.java index b4a4957b7..81cfb8b89 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContextMethodArgumentResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandContextMethodArgumentResolver.java @@ -17,6 +17,7 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; @@ -26,6 +27,7 @@ * {@link CommandContext}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class CommandContextMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -39,7 +41,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, Message message){ + public @Nullable Object resolveArgument(MethodParameter parameter, Message message){ CommandContext commandContext = message.getHeaders().get(HEADER_COMMAND_CONTEXT, CommandContext.class); return parameter.isOptional() ? Optional.ofNullable(commandContext) : commandContext; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExceptionResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExceptionResolver.java index d29a54a2b..6a2e6f287 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExceptionResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExceptionResolver.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.command; +import org.jspecify.annotations.Nullable; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -25,6 +26,7 @@ * with command. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandExceptionResolver { @@ -40,5 +42,5 @@ public interface CommandExceptionResolver { * @return a corresponding {@code HandlingResult} framework to handle, or * {@code null} for default processing in the resolution chain */ - CommandHandlingResult resolve(Exception ex); + @Nullable CommandHandlingResult resolve(Exception ex); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java index 34bd7aa24..a6498a59f 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandExecution.java @@ -19,10 +19,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; import jakarta.validation.Validator; import org.jline.terminal.Terminal; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.Order; @@ -42,12 +45,14 @@ import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite; import org.springframework.shell.command.parser.ParserConfig; import org.springframework.shell.context.ShellContext; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * Interface to evaluate a result from a command with an arguments. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandExecution { @@ -57,7 +62,7 @@ public interface CommandExecution { * @param args the command args * @return evaluated execution */ - Object evaluate(String[] args); + @Nullable Object evaluate(String[] args); /** * Gets an instance of a default {@link CommandExecution}. @@ -92,7 +97,7 @@ public static CommandExecution of(List * @param conversionService the conversion services * @return default command execution */ - public static CommandExecution of(List resolvers, Validator validator, + public static CommandExecution of(@Nullable List resolvers, Validator validator, Terminal terminal, ShellContext shellContext, ConversionService conversionService, CommandCatalog commandCatalog) { return new DefaultCommandExecution(resolvers, validator, terminal, shellContext, conversionService, commandCatalog); } @@ -102,15 +107,15 @@ public static CommandExecution of(List */ static class DefaultCommandExecution implements CommandExecution { - private List resolvers; - private Validator validator; - private Terminal terminal; - private ShellContext shellContext; - private ConversionService conversionService; - private CommandCatalog commandCatalog; + private @Nullable List resolvers; + private @Nullable Validator validator; + private @Nullable Terminal terminal; + private @Nullable ShellContext shellContext; + private @Nullable ConversionService conversionService; + private @Nullable CommandCatalog commandCatalog; - public DefaultCommandExecution(List resolvers, Validator validator, - Terminal terminal, ShellContext shellContext, ConversionService conversionService, CommandCatalog commandCatalog) { + public DefaultCommandExecution(@Nullable List resolvers, @Nullable Validator validator, + @Nullable Terminal terminal, @Nullable ShellContext shellContext, @Nullable ConversionService conversionService, @Nullable CommandCatalog commandCatalog) { this.resolvers = resolvers; this.validator = validator; this.terminal = terminal; @@ -119,10 +124,12 @@ public DefaultCommandExecution(List res this.commandCatalog = commandCatalog; } - public Object evaluate(String[] args) { - CommandParser parser = CommandParser.of(conversionService, commandCatalog.getRegistrations(), new ParserConfig()); + public @Nullable Object evaluate(String[] args) { + Map registrations = commandCatalog == null ? Map.of() : commandCatalog.getRegistrations(); + CommandParser parser = CommandParser.of(conversionService, registrations, new ParserConfig()); CommandParserResults results = parser.parse(args); CommandRegistration registration = results.registration(); + Assert.state(registration != null, "'registration' must not be null"); // fast fail with availability before doing anything else Availability availability = registration.getAvailability(); @@ -162,9 +169,9 @@ public Object evaluate(String[] args) { CommandRegistration usedRegistration; if (handleHelpOption) { String command = registration.getCommand(); - CommandParser helpParser = CommandParser.of(conversionService, commandCatalog.getRegistrations(), + CommandParser helpParser = CommandParser.of(conversionService, registrations, new ParserConfig()); - CommandRegistration helpCommandRegistration = commandCatalog.getRegistrations() + CommandRegistration helpCommandRegistration = registrations .get(registration.getHelpOption().getCommand()); CommandParserResults helpResults = helpParser.parse(new String[] { "help", "--command", command }); results = helpResults; @@ -177,7 +184,7 @@ public Object evaluate(String[] args) { if (!results.errors().isEmpty()) { throw new CommandParserExceptionsException("Command parser resulted errors", results.errors()); } - + Assert.notNull(usedRegistration, "'usedRegistration' must not be null"); CommandContext ctx = CommandContext.of(args, results, terminal, usedRegistration, shellContext); Object res = null; @@ -186,10 +193,16 @@ public Object evaluate(String[] args) { // pick the target to execute if (targetInfo.getTargetType() == TargetType.FUNCTION) { - res = targetInfo.getFunction().apply(ctx); + Function function = targetInfo.getFunction(); + if (function != null) { + res = function.apply(ctx); + } } else if (targetInfo.getTargetType() == TargetType.CONSUMER) { - targetInfo.getConsumer().accept(ctx); + Consumer consumer = targetInfo.getConsumer(); + if (consumer != null) { + consumer.accept(ctx); + } } else if (targetInfo.getTargetType() == TargetType.METHOD) { try { @@ -222,7 +235,7 @@ else if (targetInfo.getTargetType() == TargetType.METHOD) { if (resolvers != null) { argumentResolvers.addResolvers(resolvers); } - if (!paramValues.isEmpty()) { + if (!paramValues.isEmpty() && conversionService != null) { argumentResolvers.addResolver(new ParamNameHandlerMethodArgumentResolver(paramValues, conversionService)); } invocableShellMethod.setMessageMethodArgumentResolvers(argumentResolvers); @@ -262,7 +275,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { Object source = paramValues.get(parameter.getParameterName()); if (source == null) { return null; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandHandlingResult.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandHandlingResult.java index 000565a06..140dc634c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandHandlingResult.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandHandlingResult.java @@ -15,12 +15,13 @@ */ package org.springframework.shell.command; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Holder for handling some processing, typically with {@link CommandExceptionResolver}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandHandlingResult { @@ -29,8 +30,7 @@ public interface CommandHandlingResult { * * @return a message */ - @Nullable - String message(); + @Nullable String message(); /** * Gets an exit code for this {@code CommandHandlingResult}. Exit code only has meaning @@ -38,7 +38,7 @@ public interface CommandHandlingResult { * * @return an exit code */ - Integer exitCode(); + @Nullable Integer exitCode(); /** * Indicate whether this {@code CommandHandlingResult} has a result. @@ -80,31 +80,31 @@ public static CommandHandlingResult of(@Nullable String message) { * @param exitCode the exit code * @return instance of {@code CommandHandlingResult} */ - public static CommandHandlingResult of(@Nullable String message, Integer exitCode) { + public static CommandHandlingResult of(@Nullable String message, @Nullable Integer exitCode) { return new DefaultHandlingResult(message, exitCode); } static class DefaultHandlingResult implements CommandHandlingResult { - private final String message; - private final Integer exitCode; + private final @Nullable String message; + private final @Nullable Integer exitCode; DefaultHandlingResult(String message) { this(message, null); } - DefaultHandlingResult(String message, Integer exitCode) { + DefaultHandlingResult(@Nullable String message, @Nullable Integer exitCode) { this.message = message; this.exitCode = exitCode; } @Override - public String message() { + public @Nullable String message() { return message; } @Override - public Integer exitCode() { + public @Nullable Integer exitCode() { return exitCode; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandOption.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandOption.java index 396db9526..98bc688f1 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandOption.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandOption.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.command; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.shell.completion.CompletionResolver; @@ -22,6 +23,7 @@ * Interface representing an option in a command. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandOption { @@ -52,14 +54,14 @@ public interface CommandOption { * * @return description of an option */ - String getDescription(); + @Nullable String getDescription(); /** * Gets a {@link ResolvableType} of an option. * * @return type of an option */ - ResolvableType getType(); + @Nullable ResolvableType getType(); /** * Gets a flag if option is required. @@ -73,7 +75,7 @@ public interface CommandOption { * * @return the default value */ - String getDefaultValue(); + @Nullable String getDefaultValue(); /** * Gets a positional value. @@ -101,14 +103,14 @@ public interface CommandOption { * * @return the label */ - String getLabel(); + @Nullable String getLabel(); /** * Gets a completion function. * * @return the completion function */ - CompletionResolver getCompletion(); + @Nullable CompletionResolver getCompletion(); /** * Gets an instance of a default {@link CommandOption}. @@ -131,7 +133,7 @@ public static CommandOption of(String[] longNames, Character[] shortNames, Strin * @param type the type * @return default command option */ - public static CommandOption of(String[] longNames, Character[] shortNames, String description, + public static CommandOption of(String @Nullable [] longNames, Character @Nullable [] shortNames, String description, ResolvableType type) { return of(longNames, null, shortNames, description, type, false, null, null, null, null, null, null); } @@ -153,9 +155,9 @@ public static CommandOption of(String[] longNames, Character[] shortNames, Strin * @param completion the completion * @return default command option */ - public static CommandOption of(String[] longNames, String[] longNamesModified, Character[] shortNames, String description, - ResolvableType type, boolean required, String defaultValue, Integer position, Integer arityMin, - Integer arityMax, String label, CompletionResolver completion) { + public static CommandOption of(String @Nullable [] longNames, String @Nullable [] longNamesModified, Character @Nullable [] shortNames, @Nullable String description, + @Nullable ResolvableType type, boolean required, @Nullable String defaultValue, @Nullable Integer position, @Nullable Integer arityMin, + @Nullable Integer arityMax, @Nullable String label, @Nullable CompletionResolver completion) { return new DefaultCommandOption(longNames, longNamesModified, shortNames, description, type, required, defaultValue, position, arityMin, arityMax, label, completion); } @@ -168,20 +170,20 @@ public static class DefaultCommandOption implements CommandOption { private String[] longNames; private String[] longNamesModified; private Character[] shortNames; - private String description; - private ResolvableType type; + private @Nullable String description; + private @Nullable ResolvableType type; private boolean required; - private String defaultValue; + private @Nullable String defaultValue; private int position; private int arityMin; private int arityMax; - private String label; - private CompletionResolver completion; + private @Nullable String label; + private @Nullable CompletionResolver completion; - public DefaultCommandOption(String[] longNames, String[] longNamesModified, Character[] shortNames, String description, - ResolvableType type, boolean required, String defaultValue, Integer position, - Integer arityMin, Integer arityMax, String label, - CompletionResolver completion) { + public DefaultCommandOption(String @Nullable [] longNames, String @Nullable [] longNamesModified, Character @Nullable [] shortNames, @Nullable String description, + @Nullable ResolvableType type, boolean required, @Nullable String defaultValue, @Nullable Integer position, + @Nullable Integer arityMin, @Nullable Integer arityMax, @Nullable String label, + @Nullable CompletionResolver completion) { this.longNames = longNames != null ? longNames : new String[0]; this.longNamesModified = longNamesModified != null ? longNamesModified : new String[0]; this.shortNames = shortNames != null ? shortNames : new Character[0]; @@ -212,12 +214,12 @@ public Character[] getShortNames() { } @Override - public String getDescription() { + public @Nullable String getDescription() { return description; } @Override - public ResolvableType getType() { + public @Nullable ResolvableType getType() { return type; } @@ -227,7 +229,7 @@ public boolean isRequired() { } @Override - public String getDefaultValue() { + public @Nullable String getDefaultValue() { return defaultValue; } @@ -247,12 +249,12 @@ public int getArityMax() { } @Override - public String getLabel() { + public @Nullable String getLabel() { return label; } @Override - public CompletionResolver getCompletion() { + public @Nullable CompletionResolver getCompletion() { return completion; } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParser.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParser.java index 58af7fa35..64a11e8cc 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParser.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParser.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionService; import org.springframework.shell.command.parser.Ast; import org.springframework.shell.command.parser.Ast.DefaultAst; @@ -36,6 +37,7 @@ * which this interface intercepts and translates into format we can understand. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandParser { @@ -56,7 +58,7 @@ interface CommandParserResult { * * @return the value */ - Object value(); + @Nullable Object value(); /** * Gets an instance of a default {@link CommandParserResult}. @@ -65,7 +67,7 @@ interface CommandParserResult { * @param value the value * @return a result */ - static CommandParserResult of(CommandOption option, Object value) { + static CommandParserResult of(CommandOption option, @Nullable Object value) { return new DefaultCommandParserResult(option, value); } } @@ -80,7 +82,7 @@ interface CommandParserResults { * * @return the registration */ - CommandRegistration registration(); + @Nullable CommandRegistration registration(); /** * Gets the results. @@ -137,8 +139,8 @@ static CommandParserResults of(CommandRegistration registration, List registrations, - ParserConfig config) { + static CommandParser of(@Nullable ConversionService conversionService, Map registrations, + ParserConfig config) { return new AstCommandParser(registrations, config, conversionService); } @@ -147,12 +149,12 @@ static CommandParser of(ConversionService conversionService, Map results; private List positional; private List errors; - DefaultCommandParserResults(CommandRegistration registration, List results, + DefaultCommandParserResults(@Nullable CommandRegistration registration, List results, List positional, List errors) { this.registration = registration; this.results = results; @@ -161,7 +163,7 @@ static class DefaultCommandParserResults implements CommandParserResults { } @Override - public CommandRegistration registration() { + public @Nullable CommandRegistration registration() { return registration; } @@ -187,9 +189,9 @@ public List errors() { static class DefaultCommandParserResult implements CommandParserResult { private CommandOption option; - private Object value; + private @Nullable Object value; - DefaultCommandParserResult(CommandOption option, Object value) { + DefaultCommandParserResult(CommandOption option, @Nullable Object value) { this.option = option; this.value = value; } @@ -200,7 +202,7 @@ public CommandOption option() { } @Override - public Object value() { + public @Nullable Object value() { return value; } } @@ -212,10 +214,10 @@ static class AstCommandParser implements CommandParser { private final Map registrations; private final ParserConfig configuration; - private final ConversionService conversionService; + private final @Nullable ConversionService conversionService; public AstCommandParser(Map registrations, ParserConfig configuration, - ConversionService conversionService) { + @Nullable ConversionService conversionService) { this.registrations = registrations; this.configuration = configuration; this.conversionService = conversionService; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParserExceptionResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParserExceptionResolver.java index ecc7fbdc2..8def46a83 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParserExceptionResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandParserExceptionResolver.java @@ -19,17 +19,19 @@ import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; +import org.jspecify.annotations.Nullable; import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException; /** * Handles {@link CommandParserExceptionsException}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class CommandParserExceptionResolver implements CommandExceptionResolver { @Override - public CommandHandlingResult resolve(Exception ex) { + public @Nullable CommandHandlingResult resolve(Exception ex) { if (ex instanceof CommandParserExceptionsException cpee) { AttributedStringBuilder builder = new AttributedStringBuilder(); cpee.getParserExceptions().stream().forEach(e -> { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java index 2f1b0edc1..9e0dc1545 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java @@ -27,8 +27,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.shell.Availability; import org.springframework.shell.completion.CompletionResolver; import org.springframework.shell.context.InteractionMode; @@ -41,6 +41,7 @@ * Interface defining a command registration endpoint. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface CommandRegistration { @@ -63,7 +64,7 @@ public interface CommandRegistration { * * @return the group */ - String getGroup(); + @Nullable String getGroup(); /** * Returns if command is hidden. @@ -77,7 +78,7 @@ public interface CommandRegistration { * * @return the description */ - String getDescription(); + @Nullable String getDescription(); /** * Get {@link Availability} for a command @@ -339,30 +340,30 @@ public interface TargetInfo { * * @return the bean */ - Object getBean(); + @Nullable Object getBean(); /** * Get the bean method * * @return the bean method */ - Method getMethod(); + @Nullable Method getMethod(); /** * Get the function * * @return the function */ - Function getFunction(); + @Nullable Function getFunction(); /** * Get the consumer * * @return the consumer */ - Consumer getConsumer(); + @Nullable Consumer getConsumer(); - static TargetInfo of(Object bean, Method method) { + static TargetInfo of(Object bean, @Nullable Method method) { return new DefaultTargetInfo(TargetType.METHOD, bean, method, null, null); } @@ -381,13 +382,13 @@ enum TargetType { static class DefaultTargetInfo implements TargetInfo { private final TargetType targetType; - private final Object bean; - private final Method method; - private final Function function; - private final Consumer consumer; + private final @Nullable Object bean; + private final @Nullable Method method; + private final @Nullable Function function; + private final @Nullable Consumer consumer; - public DefaultTargetInfo(TargetType targetType, Object bean, Method method, - Function function, Consumer consumer) { + public DefaultTargetInfo(TargetType targetType, @Nullable Object bean, @Nullable Method method, + @Nullable Function function, @Nullable Consumer consumer) { this.targetType = targetType; this.bean = bean; this.method = method; @@ -401,22 +402,22 @@ public TargetType getTargetType() { } @Override - public Object getBean() { + public @Nullable Object getBean() { return bean; } @Override - public Method getMethod() { + public @Nullable Method getMethod() { return method; } @Override - public Function getFunction() { + public @Nullable Function getFunction() { return function; } @Override - public Consumer getConsumer() { + public @Nullable Consumer getConsumer() { return consumer; } } @@ -563,38 +564,38 @@ public interface HelpOptionInfo { * * @return long names options for help */ - String[] getLongNames(); + String @Nullable [] getLongNames(); /** * Gets short names options for help. * * @return short names options for help */ - Character[] getShortNames(); + Character @Nullable [] getShortNames(); /** * Gets command for help. * * @return command for help */ - String getCommand(); + @Nullable String getCommand(); static HelpOptionInfo of() { return of(false, null, null, null); } - static HelpOptionInfo of(boolean enabled, String[] longNames, Character[] shortNames, String command) { + static HelpOptionInfo of(boolean enabled, String @Nullable[] longNames, Character @Nullable[] shortNames, @Nullable String command) { return new DefaultHelpOptionInfo(enabled, longNames, shortNames, command); } static class DefaultHelpOptionInfo implements HelpOptionInfo { - private final String command; - private final String[] longNames; - private final Character[] shortNames; + private final @Nullable String command; + private final String @Nullable [] longNames; + private final Character @Nullable [] shortNames; private final boolean enabled; - public DefaultHelpOptionInfo(boolean enabled, String[] longNames, Character[] shortNames, String command) { + public DefaultHelpOptionInfo(boolean enabled, String @Nullable [] longNames, Character @Nullable [] shortNames, @Nullable String command) { this.command = command; this.longNames = longNames; this.shortNames = shortNames; @@ -607,17 +608,17 @@ public boolean isEnabled() { } @Override - public String[] getLongNames() { + public String @Nullable [] getLongNames() { return longNames; } @Override - public Character[] getShortNames() { + public Character @Nullable [] getShortNames() { return shortNames; } @Override - public String getCommand() { + public @Nullable String getCommand() { return command; } } @@ -690,7 +691,7 @@ public interface Builder { * @param mode the interaction mode * @return builder for chaining */ - Builder interactionMode(InteractionMode mode); + Builder interactionMode(@Nullable InteractionMode mode); /** * Define a description text for a command. @@ -796,18 +797,18 @@ public interface Builder { static class DefaultOptionSpec implements OptionSpec { private BaseBuilder builder; - private String[] longNames; - private Character[] shortNames; - private ResolvableType type; - private String description; + private String[] longNames = new String[0]; + private Character[] shortNames = new Character[0]; + private @Nullable ResolvableType type; + private @Nullable String description; private boolean required; - private String defaultValue; - private Integer position; - private Integer arityMin; - private Integer arityMax; - private String label; - private CompletionResolver completion; - private Function optionNameModifier; + private @Nullable String defaultValue; + private @Nullable Integer position; + private @Nullable Integer arityMin; + private @Nullable Integer arityMax; + private @Nullable String label; + private @Nullable CompletionResolver completion; + private @Nullable Function optionNameModifier; DefaultOptionSpec(BaseBuilder builder) { this.builder = builder; @@ -941,11 +942,11 @@ public Character[] getShortNames() { return shortNames; } - public ResolvableType getType() { + public @Nullable ResolvableType getType() { return type; } - public String getDescription() { + public @Nullable String getDescription() { return description; } @@ -953,32 +954,31 @@ public boolean isRequired() { return required; } - public String getDefaultValue() { + public @Nullable String getDefaultValue() { return defaultValue; } - public Integer getPosition() { + public @Nullable Integer getPosition() { return position; } - public Integer getArityMin() { + public @Nullable Integer getArityMin() { return arityMin; } - public Integer getArityMax() { + public @Nullable Integer getArityMax() { return arityMax; } - public String getLabel() { + public @Nullable String getLabel() { return label; } - public CompletionResolver getCompletion() { + public @Nullable CompletionResolver getCompletion() { return completion; } - @Nullable - public Function getOptionNameModifier() { + public @Nullable Function getOptionNameModifier() { if (optionNameModifier != null) { return optionNameModifier; } @@ -992,10 +992,10 @@ public Function getOptionNameModifier() { static class DefaultTargetSpec implements TargetSpec { private BaseBuilder builder; - private Object bean; - private Method method; - private Function function; - private Consumer consumer; + private @Nullable Object bean; + private @Nullable Method method; + private @Nullable Function function; + private @Nullable Consumer consumer; DefaultTargetSpec(BaseBuilder builder) { this.builder = builder; @@ -1037,8 +1037,8 @@ public Builder and() { static class DefaultAliasSpec implements AliasSpec { private BaseBuilder builder; - private String[] commands; - private String group; + private String[] commands = new String[0]; + private @Nullable String group; DefaultAliasSpec(BaseBuilder builder) { this.builder = builder; @@ -1125,9 +1125,9 @@ public Builder and() { static class DefaultHelpOptionsSpec implements HelpOptionsSpec { private BaseBuilder builder; - private String command; - private String[] longNames; - private Character[] shortNames; + private @Nullable String command; + private String @Nullable [] longNames; + private Character @Nullable [] shortNames; private boolean enabled = true; DefaultHelpOptionsSpec(BaseBuilder builder) { @@ -1138,8 +1138,8 @@ static class DefaultHelpOptionsSpec implements HelpOptionsSpec { this.builder = otherBuilder; this.builder.helpOptionsSpec = this; this.command = otherSpec.command; - this.longNames = otherSpec.longNames.clone(); - this.shortNames = otherSpec.shortNames.clone(); + this.longNames = otherSpec.longNames != null ? otherSpec.longNames.clone() : null; + this.shortNames = otherSpec.shortNames != null ? otherSpec.shortNames.clone() : null; this.enabled = otherSpec.enabled; } @@ -1177,22 +1177,22 @@ static class DefaultCommandRegistration implements CommandRegistration { private String command; private InteractionMode interactionMode; - private String group; + private @Nullable String group; private boolean hidden; - private String description; - private Supplier availability; - private List options; + private @Nullable String description; + private @Nullable Supplier availability; + private @Nullable List options; private List optionSpecs; private DefaultTargetSpec targetSpec; private List aliasSpecs; - private DefaultExitCodeSpec exitCodeSpec; - private DefaultErrorHandlingSpec errorHandlingSpec; - private DefaultHelpOptionsSpec helpOptionsSpec; + private @Nullable DefaultExitCodeSpec exitCodeSpec; + private @Nullable DefaultErrorHandlingSpec errorHandlingSpec; + private @Nullable DefaultHelpOptionsSpec helpOptionsSpec; - public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, String group, - boolean hidden, String description, Supplier availability, + public DefaultCommandRegistration(String[] commands, InteractionMode interactionMode, @Nullable String group, + boolean hidden, @Nullable String description, @Nullable Supplier availability, List optionSpecs, DefaultTargetSpec targetSpec, List aliasSpecs, - DefaultExitCodeSpec exitCodeSpec, DefaultErrorHandlingSpec errorHandlingSpec, DefaultHelpOptionsSpec helpOptionsSpec) { + @Nullable DefaultExitCodeSpec exitCodeSpec, @Nullable DefaultErrorHandlingSpec errorHandlingSpec, @Nullable DefaultHelpOptionsSpec helpOptionsSpec) { this.command = commandArrayToName(commands); this.interactionMode = interactionMode; this.group = group; @@ -1218,7 +1218,7 @@ public InteractionMode getInteractionMode() { } @Override - public String getGroup() { + public @Nullable String getGroup() { return group; } @@ -1228,7 +1228,7 @@ public boolean isHidden() { } @Override - public String getDescription() { + public @Nullable String getDescription() { return description; } @@ -1333,19 +1333,19 @@ static class DefaultBuilder extends BaseBuilder { static abstract class BaseBuilder implements Builder { - private String[] commands; + private String @Nullable[] commands; private InteractionMode interactionMode = InteractionMode.ALL; - private String group; + private @Nullable String group; private boolean hidden; - private String description; - private Supplier availability; + private @Nullable String description; + private @Nullable Supplier availability; private List optionSpecs = new ArrayList<>(); private List aliasSpecs = new ArrayList<>(); - private DefaultTargetSpec targetSpec; - private DefaultExitCodeSpec exitCodeSpec; - private DefaultErrorHandlingSpec errorHandlingSpec; - private DefaultHelpOptionsSpec helpOptionsSpec; - private Function defaultOptionNameModifier; + private @Nullable DefaultTargetSpec targetSpec; + private @Nullable DefaultExitCodeSpec exitCodeSpec; + private @Nullable DefaultErrorHandlingSpec errorHandlingSpec; + private @Nullable DefaultHelpOptionsSpec helpOptionsSpec; + private @Nullable Function defaultOptionNameModifier; @Override public Builder command(String... commands) { @@ -1360,7 +1360,7 @@ public Builder command(String... commands) { } @Override - public Builder interactionMode(InteractionMode mode) { + public Builder interactionMode(@Nullable InteractionMode mode) { this.interactionMode = mode != null ? mode : InteractionMode.ALL; return this; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/ExceptionResolverMethodResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/ExceptionResolverMethodResolver.java index 7c8e60e90..e018f5b62 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/ExceptionResolverMethodResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/ExceptionResolverMethodResolver.java @@ -22,10 +22,10 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.core.ExceptionDepthComparator; import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils.MethodFilter; @@ -33,6 +33,7 @@ /** * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ExceptionResolverMethodResolver { @@ -113,8 +114,7 @@ public boolean hasExceptionMappings() { * @param exception the exception * @return a Method to handle the exception, or {@code null} if none found */ - @Nullable - public Method resolveMethod(Exception exception) { + public @Nullable Method resolveMethod(Exception exception) { return resolveMethodByThrowable(exception); } @@ -125,8 +125,7 @@ public Method resolveMethod(Exception exception) { * @param exception the exception * @return a Method to handle the exception, or {@code null} if none found */ - @Nullable - public Method resolveMethodByThrowable(Throwable exception) { + public @Nullable Method resolveMethodByThrowable(Throwable exception) { Method method = resolveMethodByExceptionType(exception.getClass()); if (method == null) { Throwable cause = exception.getCause(); @@ -145,8 +144,7 @@ public Method resolveMethodByThrowable(Throwable exception) { * @param exceptionType the exception type * @return a Method to handle the exception, or {@code null} if none found */ - @Nullable - public Method resolveMethodByExceptionType(Class exceptionType) { + public @Nullable Method resolveMethodByExceptionType(Class exceptionType) { Method method = this.exceptionLookupCache.get(exceptionType); if (method == null) { method = getMappedMethod(exceptionType); @@ -159,7 +157,7 @@ public Method resolveMethodByExceptionType(Class exceptionT * Return the {@link Method} mapped to the given exception type, or * {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none. */ - private Method getMappedMethod(Class exceptionType) { + private @Nullable Method getMappedMethod(Class exceptionType) { List> matches = new ArrayList<>(); for (Class mappedException : this.mappedMethods.keySet()) { if (mappedException.isAssignableFrom(exceptionType)) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/MethodCommandExceptionResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/MethodCommandExceptionResolver.java index f5d03f4c8..1f448fb1d 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/MethodCommandExceptionResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/MethodCommandExceptionResolver.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import org.jline.terminal.Terminal; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,24 +34,27 @@ import org.springframework.shell.command.invocation.ShellMethodArgumentResolverComposite; import org.springframework.util.Assert; +/** + * @author Piotr Olaszewski + */ public class MethodCommandExceptionResolver implements CommandExceptionResolver { private final static Logger log = LoggerFactory.getLogger(MethodCommandExceptionResolver.class); private final Object bean; - private final Terminal terminal; + private final @Nullable Terminal terminal; public MethodCommandExceptionResolver(Object bean) { this(bean, null); } - public MethodCommandExceptionResolver(Object bean, Terminal terminal) { + public MethodCommandExceptionResolver(Object bean, @Nullable Terminal terminal) { Assert.notNull(bean, "Target bean must be set"); this.bean = bean; this.terminal = terminal; } @Override - public CommandHandlingResult resolve(Exception ex) { + public @Nullable CommandHandlingResult resolve(Exception ex) { try { ExceptionResolverMethodResolver resolver = new ExceptionResolverMethodResolver(bean.getClass()); Method exceptionResolverMethod = resolver.resolveMethodByThrowable(ex); @@ -121,9 +125,8 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { - Terminal terminal = message.getHeaders().get("terminal", Terminal.class); - return terminal; + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + return message.getHeaders().get("terminal", Terminal.class); } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/package-info.java new file mode 100644 index 000000000..696b20117 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.command.annotation; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandAnnotationUtils.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandAnnotationUtils.java index f4f669fd7..f96fd6bda 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandAnnotationUtils.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandAnnotationUtils.java @@ -19,8 +19,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.lang.Nullable; import org.springframework.shell.command.annotation.Command; import org.springframework.shell.context.InteractionMode; import org.springframework.util.StringUtils; @@ -32,6 +32,7 @@ * defaults and every field may have its own logic. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class CommandAnnotationUtils { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationBeanRegistrar.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationBeanRegistrar.java index c71a0aa96..0092471dc 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationBeanRegistrar.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationBeanRegistrar.java @@ -20,6 +20,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; @@ -42,6 +43,7 @@ * {@link Command @Command} class. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public final class CommandRegistrationBeanRegistrar { @@ -100,8 +102,7 @@ private void registerCommandMethodBeanDefinition(Class commandBeanType, Strin } private BeanDefinition createCommandClassBeanDefinition(Class type) { - RootBeanDefinition definition = new RootBeanDefinition(type); - return definition; + return new RootBeanDefinition(type); } private BeanDefinition createCommandMethodBeanDefinition(Class commandBeanType, String commandBeanName, @@ -118,7 +119,7 @@ private boolean containsBeanDefinition(String name) { return containsBeanDefinition(this.beanFactory, name); } - private boolean containsBeanDefinition(BeanFactory beanFactory, String name) { + private boolean containsBeanDefinition(@Nullable BeanFactory beanFactory, String name) { if (beanFactory instanceof ListableBeanFactory listableBeanFactory && listableBeanFactory.containsBeanDefinition(name)) { return true; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java index 5426a4571..5a943b75e 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandRegistrationFactoryBean.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,10 +56,7 @@ import org.springframework.shell.command.invocation.InvocableShellMethod; import org.springframework.shell.completion.CompletionProvider; import org.springframework.shell.context.InteractionMode; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.StringUtils; +import org.springframework.util.*; /** * Factory bean used in {@link CommandRegistrationBeanRegistrar} to build @@ -75,6 +73,7 @@ * This is internal class and not meant for generic use. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class CommandRegistrationFactoryBean implements FactoryBean, ApplicationContextAware, InitializingBean { @@ -84,18 +83,20 @@ class CommandRegistrationFactoryBean implements FactoryBean public static final String COMMAND_METHOD_NAME = "commandMethodName"; public static final String COMMAND_METHOD_PARAMETERS = "commandMethodParameters"; + @SuppressWarnings("NullAway.Init") private ObjectProvider supplier; + @SuppressWarnings("NullAway.Init") private ApplicationContext applicationContext; + @SuppressWarnings("NullAway.Init") private Object commandBean; - private Class commandBeanType; - private String commandBeanName; - private String commandMethodName; - private Class[] commandMethodParameters; + private @Nullable Class commandBeanType; + private @Nullable String commandBeanName; + private @Nullable String commandMethodName; + private Class @Nullable [] commandMethodParameters; @Override public CommandRegistration getObject() throws Exception { - CommandRegistration registration = buildRegistration(); - return registration; + return buildRegistration(); } @Override @@ -110,6 +111,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws @Override public void afterPropertiesSet() throws Exception { + Assert.notNull(commandBeanName, "'commandBeanName' must not be null"); this.commandBean = applicationContext.getBean(commandBeanName); this.supplier = applicationContext.getBeanProvider(CommandRegistration.BuilderSupplier.class); } @@ -135,7 +137,13 @@ private CommandRegistration.Builder getBuilder() { } private CommandRegistration buildRegistration() { + Assert.notNull(commandBeanType, "'commandBeanType' must not be null"); + Assert.notNull(commandMethodName, "'commandMethodName' must not be null"); + Method method = ReflectionUtils.findMethod(commandBeanType, commandMethodName, commandMethodParameters); + + Assert.notNull(method, "'method' must not be null"); + MergedAnnotation classAnn = MergedAnnotations.from(commandBeanType, SearchStrategy.TYPE_HIERARCHY) .get(Command.class); MergedAnnotation methodAnn = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) @@ -211,8 +219,7 @@ private CommandRegistration buildRegistration() { methodCommandExceptionResolver.exceptionResolverMethodResolver = exceptionResolverMethodResolver; builder.withErrorHandling().resolver(methodCommandExceptionResolver); - CommandRegistration registration = builder.build(); - return registration; + return builder.build(); } private void onCommandParameter(MethodParameter mp, Builder builder) { @@ -351,11 +358,13 @@ else if (ClassUtils.isAssignable(Boolean.class, parameterType)) { private static class MethodCommandExceptionResolver implements CommandExceptionResolver { + @SuppressWarnings("NullAway.Init") Object bean; + @SuppressWarnings("NullAway.Init") ExceptionResolverMethodResolver exceptionResolverMethodResolver; @Override - public CommandHandlingResult resolve(Exception ex) { + public @Nullable CommandHandlingResult resolve(Exception ex) { Method exceptionHandlerMethod = exceptionResolverMethodResolver.resolveMethodByThrowable(ex); if (exceptionHandlerMethod == null) { return null; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandScanRegistrar.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandScanRegistrar.java index a06e329eb..fbd4333f2 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandScanRegistrar.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/CommandScanRegistrar.java @@ -19,6 +19,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; @@ -33,6 +35,7 @@ import org.springframework.shell.command.annotation.Command; import org.springframework.shell.command.annotation.CommandScan; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -41,6 +44,7 @@ * * @author Janne Valkealahti * @author Mahmoud Ben Hassine + * @author Piotr Olaszewski */ public class CommandScanRegistrar implements ImportBeanDefinitionRegistrar { @@ -61,6 +65,7 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B private Set getPackagesToScan(AnnotationMetadata metadata) { AnnotationAttributes attributes = AnnotationAttributes .fromMap(metadata.getAnnotationAttributes(CommandScan.class.getName())); + Assert.state(attributes != null, "'attributes' must not be null"); String[] basePackages = attributes.getStringArray("basePackages"); Class[] basePackageClasses = attributes.getClassArray("basePackageClasses"); Set packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages)); @@ -93,9 +98,11 @@ private ClassPathScanningCandidateComponentProvider getScanner(BeanDefinitionReg return scanner; } - private void register(CommandRegistrationBeanRegistrar registrar, String className) throws LinkageError { + private void register(CommandRegistrationBeanRegistrar registrar, @Nullable String className) throws LinkageError { try { - register(registrar, ClassUtils.forName(className, null)); + if (StringUtils.hasText(className)) { + register(registrar, ClassUtils.forName(className, null)); + } } catch (ClassNotFoundException ex) { // Ignore diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/OptionMethodArgumentResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/OptionMethodArgumentResolver.java index 54e943f2b..144eb1337 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/OptionMethodArgumentResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/OptionMethodArgumentResolver.java @@ -19,10 +19,10 @@ import java.util.List; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; import org.springframework.shell.command.annotation.Option; @@ -57,8 +57,7 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { } @Override - @Nullable - protected Object resolveArgumentInternal(MethodParameter parameter, Message message, List names) + protected @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, List names) throws Exception { for (String name : names) { if (message.getHeaders().containsKey(ARGUMENT_PREFIX + name)) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/package-info.java new file mode 100644 index 000000000..5ca39f10b --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/annotation/support/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.command.annotation.support; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/InvocableShellMethod.java b/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/InvocableShellMethod.java index 515095277..31249450a 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/InvocableShellMethod.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/InvocableShellMethod.java @@ -27,6 +27,7 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +41,6 @@ import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.shell.ParameterValidationException; @@ -62,6 +62,7 @@ * through the associated {@link BeanFactory}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class InvocableShellMethod { @@ -72,8 +73,7 @@ public class InvocableShellMethod { private final Object bean; - @Nullable - private final BeanFactory beanFactory; + private final @Nullable BeanFactory beanFactory; private final Class beanType; @@ -83,21 +83,20 @@ public class InvocableShellMethod { private final MethodParameter[] parameters; - @Nullable - private InvocableShellMethod resolvedFromHandlerMethod; + private @Nullable InvocableShellMethod resolvedFromHandlerMethod; private ShellMethodArgumentResolverComposite resolvers = new ShellMethodArgumentResolverComposite(); private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); - private Validator validator; + private @Nullable Validator validator; private ConversionService conversionService = new DefaultConversionService(); /** * Create an instance from a bean instance and a method. */ - public InvocableShellMethod(Object bean, Method method) { + public InvocableShellMethod(@Nullable Object bean, @Nullable Method method) { Assert.notNull(bean, "Bean is required"); Assert.notNull(method, "Method is required"); this.bean = bean; @@ -152,7 +151,7 @@ public InvocableShellMethod(String beanName, BeanFactory beanFactory, Method met * * @param conversionService the conversion service */ - public void setConversionService(ConversionService conversionService) { + public void setConversionService(@Nullable ConversionService conversionService) { if (conversionService != null) { this.conversionService = conversionService; } @@ -187,7 +186,7 @@ private InvocableShellMethod(InvocableShellMethod handlerMethod, Object handler) this.resolvedFromHandlerMethod = handlerMethod; } - public void setValidator(Validator validator) { + public void setValidator(@Nullable Validator validator) { this.validator = validator; } @@ -223,8 +222,7 @@ public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDisc * @see #getMethodArgumentValues * @see #doInvoke */ - @Nullable - public Object invoke(Message message, Object... providedArgs) throws Exception { + public @Nullable Object invoke(@Nullable Message message, Object @Nullable... providedArgs) throws Exception { Object[] args = getMethodArgumentValues(message, providedArgs); if (log.isTraceEnabled()) { log.trace("Arguments: " + Arrays.toString(args)); @@ -237,7 +235,7 @@ public Object invoke(Message message, Object... providedArgs) throws Exceptio * argument values and falling back to the configured argument resolvers. *

The resulting array will be passed into {@link #doInvoke}. */ - protected Object[] getMethodArgumentValues(Message message, Object... providedArgs) throws Exception { + protected Object[] getMethodArgumentValues(@Nullable Message message, Object @Nullable... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; @@ -251,7 +249,7 @@ protected Object[] getMethodArgumentValues(Message message, Object... provide parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); boolean supports = this.resolvers.supportsParameter(parameter); Object arg = null; - if (supports) { + if (supports && message != null) { arg = this.resolvers.resolveArgument(parameter, message); } else { @@ -280,9 +278,9 @@ protected Object[] getMethodArgumentValues(Message message, Object... provide private static class ResolvedHolder { boolean resolved; MethodParameter parameter; - Object arg; + @Nullable Object arg; - public ResolvedHolder(boolean resolved, MethodParameter parameter, Object arg) { + public ResolvedHolder(boolean resolved, MethodParameter parameter, @Nullable Object arg) { this.resolved = resolved; this.parameter = parameter; this.arg = arg; @@ -292,8 +290,7 @@ public ResolvedHolder(boolean resolved, MethodParameter parameter, Object arg) { /** * Invoke the handler method with the given argument values. */ - @Nullable - protected Object doInvoke(Object... args) throws Exception { + protected @Nullable Object doInvoke(Object... args) throws Exception { try { if (validator != null) { Method bridgedMethod = getBridgedMethod(); @@ -410,8 +407,7 @@ public boolean isVoid() { * @return the annotation, or {@code null} if none found * @see AnnotatedElementUtils#findMergedAnnotation */ - @Nullable - public A getMethodAnnotation(Class annotationType) { + public @Nullable A getMethodAnnotation(Class annotationType) { return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); } @@ -428,8 +424,7 @@ public boolean hasMethodAnnotation(Class annotationTyp * Return the HandlerMethod from which this HandlerMethod instance was * resolved via {@link #createWithResolvedBean()}. */ - @Nullable - public InvocableShellMethod getResolvedFromHandlerMethod() { + public @Nullable InvocableShellMethod getResolvedFromHandlerMethod() { return this.resolvedFromHandlerMethod; } @@ -482,8 +477,7 @@ public String toString() { // Support methods for use in "InvocableHandlerMethod" sub-class variants.. - @Nullable - protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { + protected static @Nullable Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { if (!ObjectUtils.isEmpty(providedArgs)) { for (Object providedArg : providedArgs) { if (parameter.getParameterType().isInstance(providedArg)) { @@ -552,7 +546,7 @@ public Class getContainingClass() { } @Override - public T getMethodAnnotation(Class annotationType) { + public @Nullable T getMethodAnnotation(Class annotationType) { return InvocableShellMethod.this.getMethodAnnotation(annotationType); } @@ -573,8 +567,7 @@ public HandlerMethodParameter clone() { */ private class ReturnValueMethodParameter extends HandlerMethodParameter { - @Nullable - private final Object returnValue; + private final @Nullable Object returnValue; public ReturnValueMethodParameter(@Nullable Object returnValue) { super(-1); @@ -599,8 +592,7 @@ public ReturnValueMethodParameter clone() { private class AsyncResultMethodParameter extends HandlerMethodParameter { - @Nullable - private final Object returnValue; + private final @Nullable Object returnValue; private final ResolvableType returnType; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/ShellMethodArgumentResolverComposite.java b/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/ShellMethodArgumentResolverComposite.java index ef3afcd35..91297e28d 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/ShellMethodArgumentResolverComposite.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/ShellMethodArgumentResolverComposite.java @@ -21,9 +21,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; @@ -34,6 +34,7 @@ * * @author Rossen Stoyanchev * @author Juergen Hoeller + * @author Piotr Olaszewski */ public class ShellMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { @@ -111,8 +112,7 @@ public boolean supportsParameter(MethodParameter parameter) { * @throws IllegalArgumentException if no suitable argument resolver is found */ @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + @@ -125,8 +125,7 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr * Find a registered {@link HandlerMethodArgumentResolver} that supports * the given method parameter. */ - @Nullable - private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + private @Nullable HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/package-info.java new file mode 100644 index 000000000..ae51493e9 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/invocation/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.command.invocation; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/command/package-info.java new file mode 100644 index 000000000..b0a2bc7d1 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.command; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Ast.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Ast.java index a9c108c44..8a5d18f26 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Ast.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Ast.java @@ -29,6 +29,7 @@ * arguments makes sense which happen later when ast tree is visited. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface Ast { @@ -82,7 +83,9 @@ public AstResult generate(List tokens) { break; case OPTION: optionNode = new OptionNode(token, token.getValue()); - commandNode.addChildNode(optionNode); + if (commandNode != null) { + commandNode.addChildNode(optionNode); + } break; case ARGUMENT: if (optionNode != null) { @@ -91,7 +94,9 @@ public AstResult generate(List tokens) { } else { CommandArgumentNode commandArgumentNode = new CommandArgumentNode(token, commandNode); - commandNode.addChildNode(commandArgumentNode); + if (commandNode != null) { + commandNode.addChildNode(commandArgumentNode); + } } break; case DOUBLEDASH: diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandArgumentNode.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandArgumentNode.java index 0f009ee28..43d2bca7c 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandArgumentNode.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandArgumentNode.java @@ -15,21 +15,24 @@ */ package org.springframework.shell.command.parser; +import org.jspecify.annotations.Nullable; + /** * Node representing a command argument. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public final class CommandArgumentNode extends TerminalAstNode { - private final CommandNode parent; + private final @Nullable CommandNode parent; - public CommandArgumentNode(Token token, CommandNode parent) { + public CommandArgumentNode(Token token, @Nullable CommandNode parent) { super(token); this.parent = parent; } - public CommandNode getParentCommandNode() { + public @Nullable CommandNode getParentCommandNode() { return parent; } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandModel.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandModel.java index 8a65e51e5..a29247eb5 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandModel.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/CommandModel.java @@ -21,7 +21,7 @@ import java.util.Map; import java.util.Optional; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.command.parser.ParserConfig.Feature; @@ -30,6 +30,7 @@ * those are used with parser model. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class CommandModel { @@ -41,12 +42,11 @@ public CommandModel(Map registrations, ParserConfig buildModel(registrations); } - @Nullable - CommandInfo getRootCommand(String command) { + @Nullable CommandInfo getRootCommand(String command) { return rootCommands.get(command); } - CommandInfo resolve(List commands) { + @Nullable CommandInfo resolve(List commands) { CommandInfo info = null; boolean onRoot = true; for (String commandx : commands) { @@ -56,6 +56,9 @@ CommandInfo resolve(List commands) { onRoot = false; } else { + if (info == null) { + continue; + } Optional nextInfo = info.getChildren().stream() .filter(i -> i.command.equals(command)) .findFirst(); @@ -103,7 +106,7 @@ void xxx(String command, CommandRegistration registration) { // root1 sub1 // root1 sub2 - private CommandInfo getOrCreate(String[] commands, CommandRegistration registration) { + private @Nullable CommandInfo getOrCreate(String[] commands, CommandRegistration registration) { CommandInfo ret = null; CommandInfo parent = null; @@ -122,6 +125,10 @@ private CommandInfo getOrCreate(String[] commands, CommandRegistration registrat continue; } + if (parent == null) { + continue; + } + CommandInfo children = parent.getChildren(command); if (children == null) { children = new CommandInfo(key, i < commands.length - 1 ? null : registration, parent); @@ -135,7 +142,7 @@ private CommandInfo getOrCreate(String[] commands, CommandRegistration registrat } - if (ret.registration == null) { + if (ret != null && ret.registration == null) { ret.registration = registration; } return ret; @@ -170,12 +177,12 @@ private void buildModel(Map registrations) { */ static class CommandInfo { String command; - CommandRegistration registration; - CommandInfo parent; + @Nullable CommandRegistration registration; + @Nullable CommandInfo parent; // private List children = new ArrayList<>(); private Map children = new HashMap<>(); - CommandInfo(String command, CommandRegistration registration, CommandInfo parent) { + CommandInfo(String command, @Nullable CommandRegistration registration, @Nullable CommandInfo parent) { this.registration = registration; this.parent = parent; this.command = command; @@ -212,7 +219,7 @@ public void addChildred(String command, CommandInfo children) { this.children.put(command, children); } - CommandInfo getChildren(String command) { + @Nullable CommandInfo getChildren(String command) { return children.get(command); // return children.stream() // .filter(c -> c.command.equals(command)) diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/DirectiveNode.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/DirectiveNode.java index 433e93f55..d9e40f2a6 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/DirectiveNode.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/DirectiveNode.java @@ -15,17 +15,20 @@ */ package org.springframework.shell.command.parser; +import org.jspecify.annotations.Nullable; + /** * Node representing * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public final class DirectiveNode extends TerminalAstNode { private final String name; - private final String value; + private final @Nullable String value; - public DirectiveNode(Token token, String name, String value) { + public DirectiveNode(Token token, String name, @Nullable String value) { super(token); this.name = name; this.value = value; @@ -35,7 +38,7 @@ public String getName() { return name; } - public String getValue() { + public @Nullable String getValue() { return value; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Lexer.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Lexer.java index 04a002cc4..9e5f3de63 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Lexer.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Lexer.java @@ -28,6 +28,7 @@ import org.springframework.shell.command.parser.CommandModel.CommandInfo; import org.springframework.shell.command.parser.ParserConfig.Feature; +import org.springframework.util.Assert; /** * Interface to tokenize arguments into tokens. Generic language parser usually @@ -40,6 +41,7 @@ * through parsing operation. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface Lexer { @@ -191,6 +193,7 @@ public LexerResult tokenize(List arguments) { currentCommand = currentCommand == null ? commandModel.getRootCommands().get(argumentToCheck) : currentCommand.getChildren(argument); tokenList.add(Token.of(argument, TokenType.COMMAND, i2)); + Assert.state(currentCommand != null, "'currentCommand' must not be null"); validTokens = currentCommand.getValidTokens(); break; case OPTION: diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/MessageResult.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/MessageResult.java index 1300efbe2..cba04f307 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/MessageResult.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/MessageResult.java @@ -15,12 +15,15 @@ */ package org.springframework.shell.command.parser; +import org.jspecify.annotations.Nullable; + /** * Encapsulating {@link ParserMessage} with position and {@code inserts}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ -public record MessageResult(ParserMessage parserMessage, int position, Object[] inserts) { +public record MessageResult(ParserMessage parserMessage, int position, Object @Nullable [] inserts) { /** * Constructs {@code MessageResult} with parser message, position and inserts. @@ -30,7 +33,7 @@ public record MessageResult(ParserMessage parserMessage, int position, Object[] * @param inserts the inserts * @return a message result */ - public static MessageResult of(ParserMessage parserMessage, int position, Object... inserts) { + public static MessageResult of(ParserMessage parserMessage, int position, @Nullable Object... inserts) { return new MessageResult(parserMessage, position, inserts); } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Parser.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Parser.java index 84284546e..66b1c0edb 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Parser.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/Parser.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -42,6 +43,7 @@ * Interface to parse command line arguments. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface Parser { @@ -65,13 +67,13 @@ public interface Parser { * @param messageResults message results * @param directiveResults directive result */ - public record ParseResult(CommandRegistration commandRegistration, List optionResults, + public record ParseResult(@Nullable CommandRegistration commandRegistration, List optionResults, List argumentResults, List messageResults, List directiveResults) { - public record OptionResult(CommandOption option, Object value) { + public record OptionResult(CommandOption option, @Nullable Object value) { - public static OptionResult of(CommandOption option, Object value) { + public static OptionResult of(CommandOption option, @Nullable Object value) { return new OptionResult(option, value); } } @@ -105,7 +107,7 @@ public DefaultParser(CommandModel commandModel, Lexer lexer, Ast ast, ParserConf } public DefaultParser(CommandModel commandModel, Lexer lexer, Ast ast, ParserConfig config, - ConversionService conversionService) { + @Nullable ConversionService conversionService) { this.commandModel = commandModel; this.lexer = lexer; this.ast = ast; @@ -276,9 +278,17 @@ protected void onEnterOptionNode(OptionNode node) { currentOptionArgument.clear(); CommandInfo info = commandModel.resolve(resolvedCommmand); + if (info == null) { + return; + } + CommandRegistration registration = info.registration; + if (registration == null) { + return; + } + String name = node.getName(); if (name.startsWith("--")) { - info.registration.getOptions().forEach(option -> { + registration.getOptions().forEach(option -> { Set longNames = Arrays.asList(option.getLongNames()).stream() .map(n -> "--" + n) .collect(Collectors.toSet()); @@ -291,7 +301,7 @@ protected void onEnterOptionNode(OptionNode node) { } else if (name.startsWith("-")) { if (name.length() == 2) { - info.registration.getOptions().forEach(option -> { + registration.getOptions().forEach(option -> { Set shortNames = Arrays.asList(option.getShortNames()).stream() .map(n -> "-" + Character.toString(n)) .collect(Collectors.toSet()); @@ -302,7 +312,7 @@ else if (name.startsWith("-")) { }); } else if (name.length() > 2) { - info.registration.getOptions().forEach(option -> { + registration.getOptions().forEach(option -> { Set shortNames = Arrays.asList(option.getShortNames()).stream() .map(n -> "-" + Character.toString(n)) .collect(Collectors.toSet()); @@ -399,7 +409,7 @@ protected void onEnterOptionArgumentNode(OptionArgumentNode node) { protected void onExitOptionArgumentNode(OptionArgumentNode node) { } - private Object convertOptionType(CommandOption option, Object value) { + private @Nullable Object convertOptionType(CommandOption option, @Nullable Object value) { ResolvableType type = option.getType(); if (value == null && type != null && type.isAssignableFrom(boolean.class)) { return true; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/ParserMessage.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/ParserMessage.java index 9eb22488c..2b1629143 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/ParserMessage.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/ParserMessage.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.command.parser; +import org.jspecify.annotations.Nullable; + import java.text.MessageFormat; /** @@ -35,6 +37,7 @@ * is beyond its control. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public enum ParserMessage { @@ -70,7 +73,7 @@ public Type getType() { * @param inserts the inserts * @return formatted message */ - public String formatMessage(Object... inserts) { + public String formatMessage(Object @Nullable ... inserts) { return formatMessage(false, -1, inserts); } @@ -84,7 +87,7 @@ public String formatMessage(Object... inserts) { * @param inserts the inserts * @return formatted message */ - public String formatMessage(boolean useCode, int position, Object... inserts) { + public String formatMessage(boolean useCode, int position, Object @Nullable... inserts) { StringBuilder msg = new StringBuilder(); if (useCode) { msg.append(code); diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/parser/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/package-info.java new file mode 100644 index 000000000..f9e156962 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/parser/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.command.parser; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/support/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/command/support/package-info.java new file mode 100644 index 000000000..f42dbf7b1 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/support/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.command.support; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java index fa33ba387..83a0d345b 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java @@ -22,27 +22,30 @@ import org.springframework.shell.CompletionContext; import org.springframework.shell.CompletionProposal; +import org.springframework.shell.command.CommandRegistration; /** * Default implementation of a {@link CompletionResolver}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class RegistrationOptionsCompletionResolver implements CompletionResolver { @Override public List apply(CompletionContext context) { - if (context.getCommandRegistration() == null) { + CommandRegistration commandRegistration = context.getCommandRegistration(); + if (commandRegistration == null) { return Collections.emptyList(); } List candidates = new ArrayList<>(); - context.getCommandRegistration().getOptions().stream() + commandRegistration.getOptions().stream() .flatMap(o -> Stream.of(o.getLongNames())) .map(ln -> "--" + ln) .filter(ln -> !context.getWords().contains(ln)) .map(CompletionProposal::new) .forEach(candidates::add); - context.getCommandRegistration().getOptions().stream() + commandRegistration.getOptions().stream() .flatMap(o -> Stream.of(o.getShortNames())) .map(ln -> "-" + ln) .filter(ln -> !context.getWords().contains(ln)) diff --git a/spring-shell-core/src/main/java/org/springframework/shell/completion/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/completion/package-info.java new file mode 100644 index 000000000..77c07db9e --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/completion/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.completion; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/config/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/config/package-info.java new file mode 100644 index 000000000..a4bcab227 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/config/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.config; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/context/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/context/package-info.java new file mode 100644 index 000000000..875413bd4 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/context/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.context; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/exit/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/exit/package-info.java new file mode 100644 index 000000000..8569bed49 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/exit/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.exit; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/ExtendedDefaultParser.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/ExtendedDefaultParser.java index cf4f1b726..a94149f43 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/ExtendedDefaultParser.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/ExtendedDefaultParser.java @@ -26,6 +26,7 @@ import org.jline.reader.ParsedLine; import org.jline.reader.Parser; +import org.jspecify.annotations.Nullable; import org.springframework.shell.CompletingParsedLine; /** @@ -34,6 +35,7 @@ * * @author Original JLine author * @author Eric Bottard + * @author Piotr Olaszewski */ public class ExtendedDefaultParser implements Parser { @@ -238,10 +240,10 @@ public class ExtendedArgumentList implements ParsedLine, CompletingParsedLine { private final int cursor; - private final String openingQuote; + private final @Nullable String openingQuote; public ExtendedArgumentList(final String line, final List words, final int wordIndex, - final int wordCursor, final int cursor, final String openingQuote) { + final int wordCursor, final int cursor, final @Nullable String openingQuote) { this.line = line; this.words = Collections.unmodifiableList(Objects.requireNonNull(words)); this.wordIndex = wordIndex; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java index 2b421eceb..027c02ea3 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/FileInputProvider.java @@ -18,6 +18,7 @@ import org.jline.reader.ParsedLine; import org.jline.reader.Parser; +import org.jspecify.annotations.Nullable; import org.springframework.shell.Input; import org.springframework.shell.InputProvider; @@ -32,6 +33,7 @@ * of line to signal line continuation.

* * @author Eric Bottard + * @author Piotr Olaszewski */ public class FileInputProvider implements InputProvider, Closeable { @@ -46,7 +48,7 @@ public FileInputProvider(Reader reader, Parser parser) { } @Override - public Input readInput() { + public @Nullable Input readInput() { StringBuilder sb = new StringBuilder(); boolean continued = false; String line; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java index 13399c7a2..c8e95a81d 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/NonInteractiveShellRunner.java @@ -25,6 +25,7 @@ import org.jline.reader.Parser; import org.jline.reader.impl.DefaultParser; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.Order; import org.springframework.shell.Input; import org.springframework.shell.InputProvider; @@ -44,6 +45,7 @@ * * @author Janne Valkealahti * @author Chris Bono + * @author Piotr Olaszewski */ @Order(NonInteractiveShellRunner.PRECEDENCE) public class NonInteractiveShellRunner implements ShellRunner { @@ -60,7 +62,7 @@ public class NonInteractiveShellRunner implements ShellRunner { private Parser lineParser; - private String primaryCommand; + private @Nullable String primaryCommand; private static final String SINGLE_QUOTE = "\'"; private static final String DOUBLE_QUOTE = "\""; @@ -103,7 +105,7 @@ public NonInteractiveShellRunner(Shell shell, ShellContext shellContext) { this(shell, shellContext, null); } - public NonInteractiveShellRunner(Shell shell, ShellContext shellContext, String primaryCommand) { + public NonInteractiveShellRunner(Shell shell, ShellContext shellContext, @Nullable String primaryCommand) { this.shell = shell; this.shellContext = shellContext; this.primaryCommand = primaryCommand; @@ -160,7 +162,7 @@ static class MultiParsedLineInputProvider implements InputProvider { } @Override - public Input readInput() { + public @Nullable Input readInput() { if (inputIdx == parsedLineInputs.size()) { return null; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/jline/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/jline/package-info.java index 743b2e970..ead160176 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/jline/package-info.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/jline/package-info.java @@ -19,4 +19,7 @@ * * @author Eric Bottard */ -package org.springframework.shell.jline; \ No newline at end of file +@NullMarked +package org.springframework.shell.jline; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/package-info.java index 66ef91fc4..6567dedcb 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/package-info.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/package-info.java @@ -19,4 +19,7 @@ * * @author Eric Bottard */ -package org.springframework.shell; \ No newline at end of file +@NullMarked +package org.springframework.shell; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundMessageProvider.java b/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundMessageProvider.java index 66c9e1efa..87c22433d 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundMessageProvider.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundMessageProvider.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.result.CommandNotFoundMessageProvider.ProviderContext; @@ -26,11 +27,12 @@ * Provider for a message used within {@link CommandNotFoundResultHandler}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ @FunctionalInterface public interface CommandNotFoundMessageProvider extends Function { - static ProviderContext contextOf(Throwable error, List commands, Map registrations, String text) { + static ProviderContext contextOf(Throwable error, List commands, @Nullable Map registrations, @Nullable String text) { return new ProviderContext() { @Override @@ -44,12 +46,12 @@ public List commands() { } @Override - public Map registrations() { + public @Nullable Map registrations() { return registrations; } @Override - public String text() { + public @Nullable String text() { return text; } }; @@ -79,14 +81,14 @@ interface ProviderContext { * * @return a command registrations */ - Map registrations(); + @Nullable Map registrations(); /** * Gets a raw input text. * * @return a raw input text */ - String text(); + @Nullable String text(); } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundResultHandler.java index bdf6a0bd8..b00c844f8 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundResultHandler.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/CommandNotFoundResultHandler.java @@ -19,6 +19,7 @@ * {@link CommandNotFoundMessageProvider} bean. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public final class CommandNotFoundResultHandler extends TerminalAwareResultHandler { @@ -44,9 +45,8 @@ private static class DefaultProvider implements CommandNotFoundMessageProvider { @Override public String apply(ProviderContext context) { - String message = new AttributedString(context.error().getMessage(), + return new AttributedString(context.error().getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi(); - return message; } } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandlerService.java b/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandlerService.java index e6c1c096e..1a1c24241 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandlerService.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandlerService.java @@ -15,43 +15,40 @@ */ package org.springframework.shell.result; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.CopyOnWriteArraySet; - +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.shell.ResultHandler; import org.springframework.shell.ResultHandlerService; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import java.lang.reflect.Array; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CopyOnWriteArraySet; + +import static java.util.Objects.requireNonNull; + /** * Base {@link ResultHandlerService} implementation suitable for use in most * environments. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class GenericResultHandlerService implements ResultHandlerService { private final ResultHandlers resultHandlers = new ResultHandlers(); @Override - public void handle(Object source) { + public void handle(@Nullable Object source) { handle(source, TypeDescriptor.forObject(source)); } @Override - public void handle(Object result, TypeDescriptor resultType) { + public void handle(@Nullable Object result, @Nullable TypeDescriptor resultType) { if (result == null) { return; } @@ -80,8 +77,8 @@ public void addResultHandler(ResultHandler resultHandler) { /** * Add a plain result handler to this registry. * - * @param the type of result handler - * @param resultType the class of a result type + * @param the type of result handler + * @param resultType the class of a result type * @param resultHandler the result handler */ public void addResultHandler(Class resultType, ResultHandler resultHandler) { @@ -97,12 +94,11 @@ public void addResultHandler(GenericResultHandler handler) { this.resultHandlers.add(handler); } - private GenericResultHandler getResultHandler(TypeDescriptor resultType) { + private @Nullable GenericResultHandler getResultHandler(@Nullable TypeDescriptor resultType) { return this.resultHandlers.find(resultType); } - @Nullable - private Object handleResultHandlerNotFound( + private @Nullable Object handleResultHandlerNotFound( @Nullable Object source, @Nullable TypeDescriptor sourceType) { if (source == null) { return null; @@ -113,8 +109,7 @@ private Object handleResultHandlerNotFound( throw new ResultHandlerNotFoundException(sourceType); } - @Nullable - private ResolvableType[] getRequiredTypeInfo(Class handlerClass, Class genericIfc) { + private ResolvableType @Nullable [] getRequiredTypeInfo(Class handlerClass, Class genericIfc) { ResolvableType resolvableType = ResolvableType.forClass(handlerClass).as(genericIfc); ResolvableType[] generics = resolvableType.getGenerics(); if (generics.length < 1) { @@ -191,8 +186,7 @@ public void add(GenericResultHandler handler) { Set> handlerTypes = handler.getHandlerTypes(); if (handlerTypes == null) { this.globalHandlers.add(handler); - } - else { + } else { for (Class handlerType : handlerTypes) { getMatchableConverters(handlerType).add(handler); } @@ -203,7 +197,11 @@ private ResultHandlersForType getMatchableConverters(Class handlerType) { return this.handlers.computeIfAbsent(handlerType, k -> new ResultHandlersForType()); } - public GenericResultHandler find(TypeDescriptor resultType) { + public @Nullable GenericResultHandler find(@Nullable TypeDescriptor resultType) { + if (resultType == null) { + return null; + } + List> resultCandidates = getClassHierarchy(resultType.getType()); for (Class resultCandidate : resultCandidates) { GenericResultHandler handler = getRegisteredHandler(resultType, resultCandidate); @@ -214,8 +212,7 @@ public GenericResultHandler find(TypeDescriptor resultType) { return null; } - @Nullable - private GenericResultHandler getRegisteredHandler(TypeDescriptor resultType, Class handlerType) { + private @Nullable GenericResultHandler getRegisteredHandler(TypeDescriptor resultType, Class handlerType) { ResultHandlersForType resultHandlersForType = this.handlers.get(handlerType); if (resultHandlersForType != null) { GenericResultHandler handler = resultHandlersForType.getHandler(resultType); @@ -261,14 +258,14 @@ private List> getClassHierarchy(Class type) { } private void addInterfacesToClassHierarchy(Class type, boolean asArray, - List> hierarchy, Set> visited) { + List> hierarchy, Set> visited) { for (Class implementedInterface : type.getInterfaces()) { addToClassHierarchy(hierarchy.size(), implementedInterface, asArray, hierarchy, visited); } } private void addToClassHierarchy(int index, Class type, boolean asArray, - List> hierarchy, Set> visited) { + List> hierarchy, Set> visited) { if (asArray) { type = Array.newInstance(type, 0).getClass(); } @@ -278,7 +275,8 @@ private void addToClassHierarchy(int index, Class type, boolean asArray, } } - private static void invokeHandler(GenericResultHandler handler, Object result, TypeDescriptor resultType) { - handler.handle(result, resultType);; + private static void invokeHandler(GenericResultHandler handler, Object result, @Nullable TypeDescriptor resultType) { + requireNonNull(resultType); + handler.handle(result, resultType); } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerNotFoundException.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerNotFoundException.java index c4df57baf..070f9f3f5 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerNotFoundException.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerNotFoundException.java @@ -15,13 +15,15 @@ */ package org.springframework.shell.result; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; +/** + * @author Piotr Olaszewski + */ public class ResultHandlerNotFoundException extends ResultHandlingException { - @Nullable - private final TypeDescriptor resultType; + private final @Nullable TypeDescriptor resultType; /** * Create a new handling executor not found exception. @@ -37,8 +39,7 @@ public ResultHandlerNotFoundException(@Nullable TypeDescriptor resultType) { /** * Return the source type that was requested to convert from. */ - @Nullable - public TypeDescriptor getResultType() { + public @Nullable TypeDescriptor getResultType() { return this.resultType; } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ThrowableResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ThrowableResultHandler.java index daa2486ab..4a7037a41 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/ThrowableResultHandler.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ThrowableResultHandler.java @@ -23,6 +23,7 @@ import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ObjectProvider; import org.springframework.shell.ResultHandler; import org.springframework.shell.command.CommandCatalog; @@ -40,6 +41,7 @@ * * @author Eric Bottard * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ThrowableResultHandler extends TerminalAwareResultHandler { @@ -48,13 +50,13 @@ public class ThrowableResultHandler extends TerminalAwareResultHandler interactiveRunner; + private final ObjectProvider interactiveRunner; - private ShellContext shellContext; + private final ShellContext shellContext; public ThrowableResultHandler(Terminal terminal, CommandCatalog commandCatalog, ShellContext shellContext, ObjectProvider interactiveRunner) { @@ -109,7 +111,7 @@ else if (result instanceof Error) { /** * Return the last error that was dealt with by this result handler. */ - public Throwable getLastError() { + public @Nullable Throwable getLastError() { return lastError; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/result/package-info.java index 414454cea..be8765e69 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/package-info.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/package-info.java @@ -19,4 +19,7 @@ * * @author Eric Bottard */ +@NullMarked package org.springframework.shell.result; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/support/AbstractArgumentMethodArgumentResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/support/AbstractArgumentMethodArgumentResolver.java index fa0d1c991..5ed2bbdda 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/support/AbstractArgumentMethodArgumentResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/support/AbstractArgumentMethodArgumentResolver.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; @@ -27,7 +28,6 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.ValueConstants; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; @@ -50,6 +50,7 @@ * value to the expected target method parameter type. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver { @@ -57,11 +58,10 @@ public abstract class AbstractArgumentMethodArgumentResolver implements HandlerM private final ConversionService conversionService; - @Nullable - private final ConfigurableBeanFactory configurableBeanFactory; - @Nullable - private final BeanExpressionContext expressionContext; + private final @Nullable ConfigurableBeanFactory configurableBeanFactory; + + private final @Nullable BeanExpressionContext expressionContext; private final Map namedValueInfoCache = new ConcurrentHashMap<>(256); @@ -87,7 +87,7 @@ protected AbstractArgumentMethodArgumentResolver(ConversionService conversionSer } @Override - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); @@ -164,8 +164,7 @@ private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValu * Resolve the given annotation-specified value, * potentially containing placeholders and expressions. */ - @Nullable - private Object resolveEmbeddedValuesAndExpressions(String value) { + private @Nullable Object resolveEmbeddedValuesAndExpressions(String value) { if (this.configurableBeanFactory == null || this.expressionContext == null) { return value; } @@ -186,8 +185,7 @@ private Object resolveEmbeddedValuesAndExpressions(String value) { * @return the resolved argument. May be {@code null} * @throws Exception in case of errors */ - @Nullable - protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message message, List names) + protected abstract @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, List names) throws Exception; /** @@ -206,8 +204,7 @@ protected abstract Object resolveArgumentInternal(MethodParameter parameter, Mes * Specifically for booleans method parameters, use {@link Boolean#FALSE}. * Also raise an ISE for primitive types. */ - @Nullable - private Object handleNullValue(List name, @Nullable Object value, Class paramType) { + private @Nullable Object handleNullValue(List name, @Nullable Object value, Class paramType) { if (value == null) { if (Boolean.TYPE.equals(paramType)) { return Boolean.FALSE; @@ -242,8 +239,7 @@ protected static class NamedValueInfo { private final boolean required; - @Nullable - private final String defaultValue; + private final @Nullable String defaultValue; protected NamedValueInfo(List names, boolean required, @Nullable String defaultValue) { this.names = names; diff --git a/spring-shell-core/src/main/java/org/springframework/shell/support/package-info.java b/spring-shell-core/src/main/java/org/springframework/shell/support/package-info.java new file mode 100644 index 000000000..9ccb67479 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/support/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.support; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java b/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java index bb113ec98..6abd0f5c6 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.stream.Stream; +import org.jline.terminal.Terminal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -30,6 +31,7 @@ import org.springframework.shell.command.CommandCatalog; import org.springframework.shell.command.CommandRegistration; import org.springframework.shell.completion.RegistrationOptionsCompletionResolver; +import org.springframework.shell.context.ShellContext; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -54,6 +56,12 @@ class ShellTests { @Mock CommandCatalog commandRegistry; + @Mock + Terminal terminal; + + @Mock + ShellContext shellContext; + @InjectMocks private Shell shell; diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java index 6b2d75eb9..afe8840e6 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionCustomConversionTests.java @@ -15,10 +15,15 @@ */ package org.springframework.shell.command; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Set; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +32,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.shell.command.CommandRegistration.OptionArity; +import org.springframework.shell.context.DefaultShellContext; import static org.assertj.core.api.Assertions.assertThat; @@ -36,14 +42,17 @@ class CommandExecutionCustomConversionTests { private CommandCatalog commandCatalog; @BeforeEach - void setupCommandExecutionTests() { + void setupCommandExecutionTests() throws IOException { commandCatalog = CommandCatalog.of(); DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new StringToMyPojo2Converter()); List resolvers = new ArrayList<>(); resolvers.add(new ArgumentHeaderMethodArgumentResolver(conversionService, null)); resolvers.add(new CommandContextMethodArgumentResolver()); - execution = CommandExecution.of(resolvers, null, null, null, conversionService, commandCatalog); + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal terminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + execution = CommandExecution.of(resolvers, null, terminal, new DefaultShellContext(), conversionService, commandCatalog); } @Test diff --git a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java index d35ae6521..2f4e73368 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/command/CommandExecutionTests.java @@ -15,9 +15,14 @@ */ package org.springframework.shell.command; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import org.jline.terminal.impl.DumbTerminal; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -31,6 +36,7 @@ import org.springframework.shell.CommandNotCurrentlyAvailable; import org.springframework.shell.command.CommandExecution.CommandParserExceptionsException; import org.springframework.shell.command.CommandRegistration.OptionArity; +import org.springframework.shell.context.DefaultShellContext; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -41,13 +47,16 @@ class CommandExecutionTests extends AbstractCommandTests { private CommandCatalog commandCatalog; @BeforeEach - void setupCommandExecutionTests() { + void setupCommandExecutionTests() throws IOException { commandCatalog = CommandCatalog.of(); ConversionService conversionService = new DefaultConversionService(); List resolvers = new ArrayList<>(); resolvers.add(new ArgumentHeaderMethodArgumentResolver(conversionService, null)); resolvers.add(new CommandContextMethodArgumentResolver()); - execution = CommandExecution.of(resolvers, null, null, null, conversionService, commandCatalog); + ByteArrayInputStream in = new ByteArrayInputStream(new byte[0]); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DumbTerminal terminal = new DumbTerminal("terminal", "ansi", in, out, StandardCharsets.UTF_8); + execution = CommandExecution.of(resolvers, null, terminal, new DefaultShellContext(), conversionService, commandCatalog); } @Test diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ConfirmationInput.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ConfirmationInput.java index 25a69dd90..81fd6402d 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ConfirmationInput.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ConfirmationInput.java @@ -24,6 +24,7 @@ import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,27 +38,28 @@ * Component for a confirmation question. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ConfirmationInput extends AbstractTextComponent { private final static Logger log = LoggerFactory.getLogger(ConfirmationInput.class); private final boolean defaultValue; - private ConfirmationInputContext currentContext; + private @Nullable ConfirmationInputContext currentContext; public ConfirmationInput(Terminal terminal) { this(terminal, null); } - public ConfirmationInput(Terminal terminal, String name) { + public ConfirmationInput(Terminal terminal, @Nullable String name) { this(terminal, name, true, null); } - public ConfirmationInput(Terminal terminal, String name, boolean defaultValue) { + public ConfirmationInput(Terminal terminal, @Nullable String name, boolean defaultValue) { this(terminal, name, defaultValue, null); } - public ConfirmationInput(Terminal terminal, String name, boolean defaultValue, - Function> renderer) { + public ConfirmationInput(Terminal terminal, @Nullable String name, boolean defaultValue, + @Nullable Function> renderer) { super(terminal, name, null); setRenderer(renderer != null ? renderer : new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/confirmation-input-default.stg"); @@ -65,7 +67,7 @@ public ConfirmationInput(Terminal terminal, String name, boolean defaultValue, } @Override - public ConfirmationInputContext getThisContext(ComponentContext context) { + public ConfirmationInputContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } @@ -122,7 +124,7 @@ else if (context.getDefaultValue() != null) { return false; } - private Boolean parseBoolean(String input) { + private @Nullable Boolean parseBoolean(@Nullable String input) { if (!StringUtils.hasText(input)) { return null; } @@ -141,7 +143,7 @@ private Boolean parseBoolean(String input) { } } - private void checkInput(String input, ConfirmationInputContext context) { + private void checkInput(@Nullable String input, ConfirmationInputContext context) { if (!StringUtils.hasText(input)) { context.setMessage(null); return; @@ -163,7 +165,7 @@ public interface ConfirmationInputContext extends TextComponentContext implements ConfirmationInputContext { - private Boolean defaultValue; + private @Nullable Boolean defaultValue; - public DefaultConfirmationInputContext(Boolean defaultValue) { + public DefaultConfirmationInputContext(@Nullable Boolean defaultValue) { this.defaultValue = defaultValue; } @Override - public Boolean getDefaultValue() { + public @Nullable Boolean getDefaultValue() { return defaultValue; } @@ -212,7 +214,7 @@ public void setDefaultValue(Boolean defaultValue) { @Override public Map toTemplateModel() { - Map attributes = super.toTemplateModel(); + Map attributes = super.toTemplateModel(); attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null); Map model = new HashMap<>(); model.put("model", attributes); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/MultiItemSelector.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/MultiItemSelector.java index 4a50ffa86..8dd239d97 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/MultiItemSelector.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/MultiItemSelector.java @@ -26,6 +26,7 @@ import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.context.ComponentContext; import org.springframework.shell.tui.component.support.AbstractSelectorComponent; import org.springframework.shell.tui.component.support.Enableable; @@ -34,30 +35,34 @@ import org.springframework.shell.tui.component.support.Nameable; import org.springframework.shell.tui.component.support.Selectable; import org.springframework.shell.tui.component.MultiItemSelector.MultiItemSelectorContext; +import org.springframework.util.StringUtils; /** * Component able to pick multiple items. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class MultiItemSelector> extends AbstractSelectorComponent, I> { - private MultiItemSelectorContext currentContext; + private @Nullable MultiItemSelectorContext currentContext; - public MultiItemSelector(Terminal terminal, List items, String name, Comparator comparator) { + public MultiItemSelector(Terminal terminal, List items, @Nullable String name, @Nullable Comparator comparator) { super(terminal, name, items, false, comparator); setRenderer(new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/multi-item-selector-default.stg"); } @Override - public MultiItemSelectorContext getThisContext(ComponentContext context) { + public MultiItemSelectorContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } currentContext = MultiItemSelectorContext.empty(getItemMapper()); - currentContext.setName(name); + if (StringUtils.hasText(name)) { + currentContext.setName(name); + } currentContext.setTerminalWidth(getTerminal().getWidth()); if (currentContext.getItems() == null) { currentContext.setItems(getItems()); @@ -144,7 +149,8 @@ public Map toTemplateModel() { Map map = new HashMap<>(); map.put("name", is.getName()); map.put("selected", is.isSelected()); - map.put("onrow", getCursorRow().intValue() == is.getIndex()); + Integer cursorRow = getCursorRow(); + map.put("onrow", cursorRow != null && cursorRow == is.getIndex()); map.put("enabled", is.isEnabled()); return map; }) diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathInput.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathInput.java index cb6a437b9..b45397783 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathInput.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathInput.java @@ -27,6 +27,7 @@ import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,35 +35,36 @@ import org.springframework.shell.tui.component.context.ComponentContext; import org.springframework.shell.tui.component.support.AbstractTextComponent; import org.springframework.shell.tui.component.support.AbstractTextComponent.TextComponentContext.MessageLevel; -import org.springframework.util.StringUtils;; +import org.springframework.util.StringUtils; /** * Component for a simple path input. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class PathInput extends AbstractTextComponent { private final static Logger log = LoggerFactory.getLogger(PathInput.class); - private PathInputContext currentContext; + private @Nullable PathInputContext currentContext; private Function pathProvider = (path) -> Paths.get(path); public PathInput(Terminal terminal) { this(terminal, null); } - public PathInput(Terminal terminal, String name) { + public PathInput(Terminal terminal, @Nullable String name) { this(terminal, name, null); } - public PathInput(Terminal terminal, String name, Function> renderer) { + public PathInput(Terminal terminal, @Nullable String name, @Nullable Function> renderer) { super(terminal, name, null); setRenderer(renderer != null ? renderer : new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/path-input-default.stg"); } @Override - public PathInputContext getThisContext(ComponentContext context) { + public PathInputContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } @@ -135,7 +137,7 @@ protected Path resolvePath(String path) { return this.pathProvider.apply(path); } - private void checkPath(String path, PathInputContext context) { + private void checkPath(@Nullable String path, PathInputContext context) { if (!StringUtils.hasText(path)) { context.setMessage(null); return; @@ -167,7 +169,7 @@ private static class DefaultPathInputContext extends BaseTextComponentContext toTemplateModel() { - Map attributes = super.toTemplateModel(); + Map attributes = super.toTemplateModel(); Map model = new HashMap<>(); model.put("model", attributes); return model; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathSearch.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathSearch.java index 0a9069583..c53552eaf 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathSearch.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/PathSearch.java @@ -44,6 +44,7 @@ import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; import org.jline.utils.InfoCmp.Capability; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,13 +74,14 @@ * sources. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class PathSearch extends AbstractTextComponent { private final static Logger log = LoggerFactory.getLogger(PathSearch.class); private final static String DEFAULT_TEMPLATE_LOCATION = "classpath:org/springframework/shell/component/path-search-default.stg"; private final PathSearchConfig config; - private PathSearchContext currentContext; + private @Nullable PathSearchContext currentContext; private Function pathProvider = (path) -> Paths.get(path); private final SelectorList selectorList; @@ -87,16 +89,16 @@ public PathSearch(Terminal terminal) { this(terminal, null); } - public PathSearch(Terminal terminal, String name) { + public PathSearch(Terminal terminal, @Nullable String name) { this(terminal, name, null); } - public PathSearch(Terminal terminal, String name, PathSearchConfig config) { + public PathSearch(Terminal terminal, @Nullable String name, @Nullable PathSearchConfig config) { this(terminal, name, config, null); } - public PathSearch(Terminal terminal, String name, PathSearchConfig config, - Function> renderer) { + public PathSearch(Terminal terminal, @Nullable String name, @Nullable PathSearchConfig config, + @Nullable Function> renderer) { super(terminal, name, null); setRenderer(renderer != null ? renderer : new DefaultRenderer()); setTemplateLocation(DEFAULT_TEMPLATE_LOCATION); @@ -114,7 +116,7 @@ protected void bindKeyMap(KeyMap keyMap) { } @Override - public PathSearchContext getThisContext(ComponentContext context) { + public PathSearchContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } @@ -199,7 +201,7 @@ protected Path resolvePath(String path) { return this.pathProvider.apply(path); } - private void inputUpdated(PathSearchContext context, String input) { + private void inputUpdated(PathSearchContext context, @Nullable String input) { context.setMessage("Type ' ' to search", MessageLevel.INFO); updateSelectorList(input, context); selectorListUpdated(context); @@ -214,7 +216,7 @@ private void selectorListUpdated(PathSearchContext context) { context.setPathViewItems(pathViews); } - private void updateSelectorList(String path, PathSearchContext context) { + private void updateSelectorList(@Nullable String path, PathSearchContext context) { if (path == null) { // when user removes all input this.selectorList.reset(Collections.emptyList()); @@ -365,7 +367,7 @@ public interface PathSearchContext extends TextComponentContext getPathViewItems(); + @Nullable List getPathViewItems(); /** * Sets a path view items. @@ -501,11 +503,11 @@ else if (position == text.length() - 1) { private static class DefaultPathSearchContext extends BaseTextComponentContext implements PathSearchContext { - private List pathViewItems; - private PathSearchConfig pathSearchConfig; + private @Nullable List pathViewItems; + private PathSearchConfig pathSearchConfig = new PathSearchConfig(); @Override - public List getPathViewItems() { + public @Nullable List getPathViewItems() { return this.pathViewItems; } @@ -526,7 +528,7 @@ public void setPathSearchConfig(PathSearchConfig config) { @Override public Map toTemplateModel() { - Map attributes = super.toTemplateModel(); + Map attributes = super.toTemplateModel(); attributes.put("pathViewItems", getPathViewItems()); Map model = new HashMap<>(); model.put("model", attributes); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/SingleItemSelector.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/SingleItemSelector.java index 933813e5e..16b67a6e7 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/SingleItemSelector.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/SingleItemSelector.java @@ -26,6 +26,7 @@ import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.context.ComponentContext; import org.springframework.shell.tui.component.support.AbstractSelectorComponent; import org.springframework.shell.tui.component.support.Enableable; @@ -34,30 +35,34 @@ import org.springframework.shell.tui.component.support.Nameable; import org.springframework.shell.tui.component.support.Selectable; import org.springframework.shell.tui.component.SingleItemSelector.SingleItemSelectorContext; +import org.springframework.util.StringUtils; /** * Component able to pick single item. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class SingleItemSelector> extends AbstractSelectorComponent, I> { - private SingleItemSelectorContext currentContext; + private @Nullable SingleItemSelectorContext currentContext; - public SingleItemSelector(Terminal terminal, List items, String name, Comparator comparator) { + public SingleItemSelector(Terminal terminal, List items, @Nullable String name, @Nullable Comparator comparator) { super(terminal, name, items, true, comparator); setRenderer(new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/single-item-selector-default.stg"); } @Override - public SingleItemSelectorContext getThisContext(ComponentContext context) { + public SingleItemSelectorContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } currentContext = SingleItemSelectorContext.empty(getItemMapper()); - currentContext.setName(name); + if (StringUtils.hasText(name)) { + currentContext.setName(name); + } currentContext.setTerminalWidth(getTerminal().getWidth()); if (currentContext.getItems() == null) { currentContext.setItems(getItems()); @@ -154,7 +159,8 @@ public Map toTemplateModel() { .map(is -> { Map map = new HashMap<>(); map.put("name", is.getName()); - map.put("selected", getCursorRow().intValue() == is.getIndex()); + Integer cursorRow = getCursorRow(); + map.put("selected", cursorRow != null && cursorRow == is.getIndex()); return map; }) .collect(Collectors.toList()); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/StringInput.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/StringInput.java index 1f31f09fa..2eab8e034 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/StringInput.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/StringInput.java @@ -24,6 +24,7 @@ import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,24 +37,25 @@ * Component for a simple string input. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class StringInput extends AbstractTextComponent { private final static Logger log = LoggerFactory.getLogger(StringInput.class); - private final String defaultValue; - private StringInputContext currentContext; - private Character maskCharacter; + private final @Nullable String defaultValue; + private @Nullable StringInputContext currentContext; + private @Nullable Character maskCharacter; public StringInput(Terminal terminal) { this(terminal, null, null, null); } - public StringInput(Terminal terminal, String name, String defaultValue) { + public StringInput(Terminal terminal, @Nullable String name, @Nullable String defaultValue) { this(terminal, name, defaultValue, null); } - public StringInput(Terminal terminal, String name, String defaultValue, - Function> renderer) { + public StringInput(Terminal terminal, @Nullable String name, @Nullable String defaultValue, + @Nullable Function> renderer) { super(terminal, name, null); setRenderer(renderer != null ? renderer : new DefaultRenderer()); setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg"); @@ -65,12 +67,12 @@ public StringInput(Terminal terminal, String name, String defaultValue, * * @param maskCharacter a mask character */ - public void setMaskCharacter(Character maskCharacter) { + public void setMaskCharacter(@Nullable Character maskCharacter) { this.maskCharacter = maskCharacter; } @Override - public StringInputContext getThisContext(ComponentContext context) { + public StringInputContext getThisContext(@Nullable ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } @@ -110,7 +112,9 @@ protected boolean read(BindingReader bindingReader, KeyMap keyMap, Strin if (StringUtils.hasLength(input)) { input = input.length() > 1 ? input.substring(0, input.length() - 1) : null; } - context.setInput(input); + if (input != null) { + context.setInput(input); + } break; case OPERATION_EXIT: if (StringUtils.hasText(context.getInput())) { @@ -133,14 +137,14 @@ public interface StringInputContext extends TextComponentContext implements StringInputContext { - private String defaultValue; - private Character maskCharacter; + private @Nullable String defaultValue; + private @Nullable Character maskCharacter; - public DefaultStringInputContext(String defaultValue, Character maskCharacter) { + public DefaultStringInputContext(@Nullable String defaultValue, @Nullable Character maskCharacter) { this.defaultValue = defaultValue; this.maskCharacter = maskCharacter; } @Override - public String getDefaultValue() { + public @Nullable String getDefaultValue() { return defaultValue; } @Override - public void setDefaultValue(String defaultValue) { + public void setDefaultValue(@Nullable String defaultValue) { this.defaultValue = defaultValue; } @@ -223,12 +227,12 @@ public void setMaskCharacter(Character maskCharacter) { } @Override - public String getMaskedInput() { + public @Nullable String getMaskedInput() { return maybeMask(getInput()); } @Override - public String getMaskedResultValue() { + public @Nullable String getMaskedResultValue() { return maybeMask(getResultValue()); } @@ -238,13 +242,13 @@ public boolean hasMaskCharacter() { } @Override - public Character getMaskCharacter() { + public @Nullable Character getMaskCharacter() { return maskCharacter; } @Override - public Map toTemplateModel() { - Map attributes = super.toTemplateModel(); + public Map toTemplateModel() { + Map attributes = super.toTemplateModel(); attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null); attributes.put("maskedInput", getMaskedInput()); attributes.put("maskedResultValue", getMaskedResultValue()); @@ -255,7 +259,7 @@ public Map toTemplateModel() { return model; } - private String maybeMask(String str) { + private @Nullable String maybeMask(@Nullable String str) { if (StringUtils.hasLength(str) && maskCharacter != null) { return new String(new char[str.length()]).replace('\0', maskCharacter); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ViewComponentExecutor.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ViewComponentExecutor.java index 2d75477fd..2f131f4be 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ViewComponentExecutor.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/ViewComponentExecutor.java @@ -18,6 +18,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,12 +30,13 @@ * component in a thread so that it doesn't need to block from a command. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ViewComponentExecutor implements AutoCloseable { private final Logger log = LoggerFactory.getLogger(ViewComponentExecutor.class); private final SimpleAsyncTaskExecutor executor; - private Future future; + private @Nullable Future future; public ViewComponentExecutor() { this.executor = new SimpleAsyncTaskExecutor(); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/BaseComponentContext.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/BaseComponentContext.java index f5d02716b..2c3427e6b 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/BaseComponentContext.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/BaseComponentContext.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.tui.component.context; +import org.jspecify.annotations.Nullable; + import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -25,12 +27,13 @@ * Base implementation of a {@link ComponentContext}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ @SuppressWarnings("unchecked") public class BaseComponentContext> extends LinkedHashMap implements ComponentContext { - private Integer terminalWidth; + private @Nullable Integer terminalWidth; @Override public Object get(Object key) { @@ -63,18 +66,18 @@ public Stream> stream() { } @Override - public Integer getTerminalWidth() { + public @Nullable Integer getTerminalWidth() { return terminalWidth; } @Override - public void setTerminalWidth(Integer terminalWidth) { + public void setTerminalWidth(@Nullable Integer terminalWidth) { this.terminalWidth = terminalWidth; } @Override - public Map toTemplateModel() { - Map attributes = new HashMap<>(); + public Map toTemplateModel() { + Map attributes = new HashMap<>(); // hardcoding enclosed map values into 'rawValues' // as it may contain anything attributes.put("rawValues", this); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/ComponentContext.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/ComponentContext.java index dac569307..8a55d363b 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/ComponentContext.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/ComponentContext.java @@ -15,6 +15,8 @@ */ package org.springframework.shell.tui.component.context; +import org.jspecify.annotations.Nullable; + import java.util.Map; import java.util.stream.Stream; @@ -24,6 +26,7 @@ * component spesific contexts. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface ComponentContext> { @@ -85,7 +88,7 @@ static > ComponentContext empty() { * * @return a terminal width */ - Integer getTerminalWidth(); + @Nullable Integer getTerminalWidth(); /** * Set terminal width. @@ -102,5 +105,5 @@ static > ComponentContext empty() { * * @return map of context values */ - Map toTemplateModel(); + Map toTemplateModel(); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java new file mode 100644 index 000000000..fe455127c --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.context; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseConfirmationInput.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseConfirmationInput.java index c60058208..7cf81e049 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseConfirmationInput.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseConfirmationInput.java @@ -22,6 +22,7 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.ConfirmationInput.ConfirmationInputContext; import org.springframework.shell.tui.component.flow.ComponentFlow.BaseBuilder; import org.springframework.shell.tui.component.flow.ComponentFlow.Builder; @@ -30,19 +31,20 @@ * Base impl for {@link ConfirmationInputSpec}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class BaseConfirmationInput extends BaseInput implements ConfirmationInputSpec { - private String name; - private Boolean defaultValue; - private Boolean resultValue; - private ResultMode resultMode; - private Function> renderer; + private @Nullable String name; + private @Nullable Boolean defaultValue; + private @Nullable Boolean resultValue; + private @Nullable ResultMode resultMode; + private @Nullable Function> renderer; private List> preHandlers = new ArrayList<>(); private List> postHandlers = new ArrayList<>(); private boolean storeResult = true; - private String templateLocation; - private Function next; + private @Nullable String templateLocation; + private @Nullable Function next; public BaseConfirmationInput(BaseBuilder builder, String id) { super(builder, id); @@ -119,7 +121,7 @@ public ConfirmationInputSpec getThis() { return this; } - public String getName() { + public @Nullable String getName() { return name; } @@ -127,19 +129,19 @@ public boolean getDefaultValue() { return defaultValue != null ? defaultValue : true; } - public Boolean getResultValue() { + public @Nullable Boolean getResultValue() { return resultValue; } - public ResultMode getResultMode() { + public @Nullable ResultMode getResultMode() { return resultMode; } - public Function> getRenderer() { + public @Nullable Function> getRenderer() { return renderer; } - public String getTemplateLocation() { + public @Nullable String getTemplateLocation() { return templateLocation; } @@ -155,7 +157,7 @@ public boolean isStoreResult() { return storeResult; } - public Function getNext() { + public @Nullable Function getNext() { return next; } } \ No newline at end of file diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseMultiItemSelector.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseMultiItemSelector.java index 7f7de2699..da85f1cd1 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseMultiItemSelector.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseMultiItemSelector.java @@ -23,6 +23,7 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.MultiItemSelector.MultiItemSelectorContext; import org.springframework.shell.tui.component.flow.ComponentFlow.BaseBuilder; import org.springframework.shell.tui.component.flow.ComponentFlow.Builder; @@ -32,21 +33,22 @@ * Base impl for {@link MultiItemSelectorSpec}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class BaseMultiItemSelector extends BaseInput implements MultiItemSelectorSpec { - private String name; + private @Nullable String name; private List resultValues = new ArrayList<>(); - private ResultMode resultMode; + private @Nullable ResultMode resultMode; private List selectItems = new ArrayList<>(); - private Comparator> comparator; - private Function>, List> renderer; - private Integer maxItems; + private @Nullable Comparator> comparator; + private @Nullable Function>, List> renderer; + private @Nullable Integer maxItems; private List>>> preHandlers = new ArrayList<>(); private List>>> postHandlers = new ArrayList<>(); private boolean storeResult = true; - private String templateLocation; - private Function>, String> next; + private @Nullable String templateLocation; + private @Nullable Function>, String> next; public BaseMultiItemSelector(BaseBuilder builder, String id) { super(builder, id); @@ -136,7 +138,7 @@ public MultiItemSelectorSpec getThis() { return this; } - public String getName() { + public @Nullable String getName() { return name; } @@ -144,7 +146,7 @@ public List getResultValues() { return resultValues; } - public ResultMode getResultMode() { + public @Nullable ResultMode getResultMode() { return resultMode; } @@ -152,19 +154,19 @@ public List getSelectItems() { return selectItems; } - public Comparator> getComparator() { + public @Nullable Comparator> getComparator() { return comparator; } - public Function>, List> getRenderer() { + public @Nullable Function>, List> getRenderer() { return renderer; } - public String getTemplateLocation() { + public @Nullable String getTemplateLocation() { return templateLocation; } - public Integer getMaxItems() { + public @Nullable Integer getMaxItems() { return maxItems; } @@ -180,7 +182,7 @@ public boolean isStoreResult() { return storeResult; } - public Function>, String> getNext() { + public @Nullable Function>, String> getNext() { return next; } } \ No newline at end of file diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BasePathInput.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BasePathInput.java index 1e9f69056..c4023ef08 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BasePathInput.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BasePathInput.java @@ -22,6 +22,7 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.PathInput.PathInputContext; import org.springframework.shell.tui.component.flow.ComponentFlow.BaseBuilder; import org.springframework.shell.tui.component.flow.ComponentFlow.Builder; @@ -30,19 +31,20 @@ * Base impl for {@link PathInputSpec}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class BasePathInput extends BaseInput implements PathInputSpec { - private String name; - private String resultValue; - private ResultMode resultMode; - private String defaultValue; - private Function> renderer; + private @Nullable String name; + private @Nullable String resultValue; + private @Nullable ResultMode resultMode; + private @Nullable String defaultValue; + private @Nullable Function> renderer; private List> preHandlers = new ArrayList<>(); private List> postHandlers = new ArrayList<>(); private boolean storeResult = true; - private String templateLocation; - private Function next; + private @Nullable String templateLocation; + private @Nullable Function next; public BasePathInput(BaseBuilder builder, String id) { super(builder, id); @@ -119,27 +121,27 @@ public PathInputSpec getThis() { return this; } - public String getName() { + public @Nullable String getName() { return name; } - public String getResultValue() { + public @Nullable String getResultValue() { return resultValue; } - public ResultMode getResultMode() { + public @Nullable ResultMode getResultMode() { return resultMode; } - public String getDefaultValue() { + public @Nullable String getDefaultValue() { return defaultValue; } - public Function> getRenderer() { + public @Nullable Function> getRenderer() { return renderer; } - public String getTemplateLocation() { + public @Nullable String getTemplateLocation() { return templateLocation; } @@ -155,7 +157,7 @@ public boolean isStoreResult() { return storeResult; } - public Function getNext() { + public @Nullable Function getNext() { return next; } } \ No newline at end of file diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseSingleItemSelector.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseSingleItemSelector.java index faed18023..5f773a11c 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseSingleItemSelector.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseSingleItemSelector.java @@ -26,6 +26,7 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.SingleItemSelector.SingleItemSelectorContext; import org.springframework.shell.tui.component.flow.ComponentFlow.BaseBuilder; import org.springframework.shell.tui.component.flow.ComponentFlow.Builder; @@ -35,22 +36,23 @@ * Base impl for {@link SingleItemSelectorSpec}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class BaseSingleItemSelector extends BaseInput implements SingleItemSelectorSpec { - private String name; - private String resultValue; - private ResultMode resultMode; + private @Nullable String name; + private @Nullable String resultValue; + private @Nullable ResultMode resultMode; private List selectItems = new ArrayList<>(); - private String defaultSelect; - private Comparator> comparator; - private Function>, List> renderer; - private Integer maxItems; + private @Nullable String defaultSelect; + private @Nullable Comparator> comparator; + private @Nullable Function>, List> renderer; + private @Nullable Integer maxItems; private List>>> preHandlers = new ArrayList<>(); private List>>> postHandlers = new ArrayList<>(); private boolean storeResult = true; - private String templateLocation; - private Function>, String> next; + private @Nullable String templateLocation; + private @Nullable Function>, String> next; public BaseSingleItemSelector(BaseBuilder builder, String id) { super(builder, id); @@ -165,15 +167,15 @@ public SingleItemSelectorSpec getThis() { return this; } - public String getName() { + public @Nullable String getName() { return name; } - public String getResultValue() { + public @Nullable String getResultValue() { return resultValue; } - public ResultMode getResultMode() { + public @Nullable ResultMode getResultMode() { return resultMode; } @@ -181,23 +183,23 @@ public List getSelectItems() { return selectItems; } - public String getDefaultSelect() { + public @Nullable String getDefaultSelect() { return defaultSelect; } - public Comparator> getComparator() { + public @Nullable Comparator> getComparator() { return comparator; } - public Function>, List> getRenderer() { + public @Nullable Function>, List> getRenderer() { return renderer; } - public String getTemplateLocation() { + public @Nullable String getTemplateLocation() { return templateLocation; } - public Integer getMaxItems() { + public @Nullable Integer getMaxItems() { return maxItems; } @@ -213,7 +215,7 @@ public boolean isStoreResult() { return storeResult; } - public Function>, String> getNext() { + public @Nullable Function>, String> getNext() { return next; } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseStringInput.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseStringInput.java index 18262fca9..dba859ead 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseStringInput.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/BaseStringInput.java @@ -22,6 +22,7 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.StringInput.StringInputContext; import org.springframework.shell.tui.component.flow.ComponentFlow.BaseBuilder; import org.springframework.shell.tui.component.flow.ComponentFlow.Builder; @@ -30,20 +31,21 @@ * Base impl for {@link StringInputSpec}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class BaseStringInput extends BaseInput implements StringInputSpec { - private String name; - private String resultValue; - private ResultMode resultMode; - private String defaultValue; - private Character maskCharacter; - private Function> renderer; + private @Nullable String name; + private @Nullable String resultValue; + private @Nullable ResultMode resultMode; + private @Nullable String defaultValue; + private @Nullable Character maskCharacter; + private @Nullable Function> renderer; private List> preHandlers = new ArrayList<>(); private List> postHandlers = new ArrayList<>(); private boolean storeResult = true; - private String templateLocation; - private Function next; + private @Nullable String templateLocation; + private @Nullable Function next; public BaseStringInput(BaseBuilder builder, String id) { super(builder, id); @@ -126,31 +128,31 @@ public StringInputSpec getThis() { return this; } - public String getName() { + public @Nullable String getName() { return name; } - public String getResultValue() { + public @Nullable String getResultValue() { return resultValue; } - public ResultMode getResultMode() { + public @Nullable ResultMode getResultMode() { return resultMode; } - public String getDefaultValue() { + public @Nullable String getDefaultValue() { return defaultValue; } - public Character getMaskCharacter() { + public @Nullable Character getMaskCharacter() { return maskCharacter; } - public Function> getRenderer() { + public @Nullable Function> getRenderer() { return renderer; } - public String getTemplateLocation() { + public @Nullable String getTemplateLocation() { return templateLocation; } @@ -166,7 +168,7 @@ public boolean isStoreResult() { return storeResult; } - public Function getNext() { + public @Nullable Function getNext() { return next; } } \ No newline at end of file diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/ComponentFlow.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/ComponentFlow.java index 63c0b023f..290f5ffbf 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/ComponentFlow.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/ComponentFlow.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.tui.component.flow; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.HashMap; @@ -29,6 +30,7 @@ import java.util.stream.Stream; import org.jline.terminal.Terminal; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,6 +50,7 @@ import org.springframework.shell.tui.component.context.ComponentContext; import org.springframework.shell.tui.component.support.SelectorItem; import org.springframework.shell.tui.style.TemplateExecutor; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -57,6 +60,7 @@ * multi-select. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface ComponentFlow { @@ -140,7 +144,7 @@ interface Builder { * @param terminal the terminal * @return a builder */ - Builder terminal(Terminal terminal); + Builder terminal(@Nullable Terminal terminal); /** * Sets a {@link ResourceLoader}. @@ -148,7 +152,7 @@ interface Builder { * @param resourceLoader the resource loader * @return a builder */ - Builder resourceLoader(ResourceLoader resourceLoader); + Builder resourceLoader(@Nullable ResourceLoader resourceLoader); /** * Sets a {@link TemplateExecutor}. @@ -156,7 +160,7 @@ interface Builder { * @param templateExecutor the template executor * @return a builder */ - Builder templateExecutor(TemplateExecutor templateExecutor); + Builder templateExecutor(@Nullable TemplateExecutor templateExecutor); /** * Clones existing builder. @@ -189,9 +193,9 @@ static abstract class BaseBuilder implements Builder { private final List multiItemSelectors = new ArrayList<>(); private final AtomicInteger order = new AtomicInteger(); private final HashSet uniqueIds = new HashSet<>(); - private Terminal terminal; - private ResourceLoader resourceLoader; - private TemplateExecutor templateExecutor; + private @Nullable Terminal terminal; + private @Nullable ResourceLoader resourceLoader; + private @Nullable TemplateExecutor templateExecutor; BaseBuilder() { } @@ -228,19 +232,19 @@ public MultiItemSelectorSpec withMultiItemSelector(String id) { } @Override - public Builder terminal(Terminal terminal) { + public Builder terminal(@Nullable Terminal terminal) { this.terminal = terminal; return this; } @Override - public Builder resourceLoader(ResourceLoader resourceLoader) { + public Builder resourceLoader(@Nullable ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; return this; } @Override - public Builder templateExecutor(TemplateExecutor templateExecutor) { + public Builder templateExecutor(@Nullable TemplateExecutor templateExecutor) { this.templateExecutor = templateExecutor; return this; } @@ -292,15 +296,15 @@ void addMultiItemSelector(BaseMultiItemSelector input) { multiItemSelectors.add(input); } - Terminal getTerminal() { + @Nullable Terminal getTerminal() { return terminal; } - ResourceLoader getResourceLoader() { + @Nullable ResourceLoader getResourceLoader() { return resourceLoader; } - TemplateExecutor getTemplateExecutor() { + @Nullable TemplateExecutor getTemplateExecutor() { return templateExecutor; } @@ -347,12 +351,13 @@ static class DefaultComponentFlow implements ComponentFlow { private final List confirmationInputs; private final List singleInputs; private final List multiInputs; - private final ResourceLoader resourceLoader; - private final TemplateExecutor templateExecutor; + private final @Nullable ResourceLoader resourceLoader; + private final @Nullable TemplateExecutor templateExecutor; - DefaultComponentFlow(Terminal terminal, ResourceLoader resourceLoader, TemplateExecutor templateExecutor, + DefaultComponentFlow(@Nullable Terminal terminal, @Nullable ResourceLoader resourceLoader, @Nullable TemplateExecutor templateExecutor, List stringInputs, List pathInputs, List confirmationInputs, List singleInputs, List multiInputs) { + Assert.state(terminal != null, "'terminal' must not be null"); this.terminal = terminal; this.resourceLoader = resourceLoader; this.templateExecutor = templateExecutor; @@ -371,7 +376,7 @@ public ComponentFlowResult run() { private static class OrderedInputOperationList { private final Map map = new HashMap<>(); - private Node first; + private @Nullable Node first; OrderedInputOperationList(List values) { Node ref = null; @@ -388,17 +393,17 @@ private static class OrderedInputOperationList { } } - Node get(String id) { + @Nullable Node get(String id) { return map.get(id); } - Node getFirst() { + @Nullable Node getFirst() { return first; } static class Node { OrderedInputOperation data; - Node next; + @Nullable Node next; Node(OrderedInputOperation data) { this.data = data; } @@ -418,7 +423,10 @@ private DefaultComponentFlowResult runGetResults() { OrderedInputOperationList.Node node = oiol.getFirst(); while (node != null) { log.debug("Calling apply for {}", node.data.id); - context = node.data.getOperation().apply(context); + Function, ComponentContext> operation = node.data.getOperation(); + if (operation != null) { + context = operation.apply(context); + } if (node.data.next != null) { Optional n = node.data.next.apply(context); if (n == null) { @@ -452,8 +460,12 @@ private Stream stringInputsStream() { context.put(input.getId(), input.getResultValue()); return context; } - selector.setResourceLoader(resourceLoader); - selector.setTemplateExecutor(templateExecutor); + if (resourceLoader != null) { + selector.setResourceLoader(resourceLoader); + } + if (templateExecutor != null) { + selector.setTemplateExecutor(templateExecutor); + } selector.setMaskCharacter(input.getMaskCharacter()); if (StringUtils.hasText(input.getTemplateLocation())) { selector.setTemplateLocation(input.getTemplateLocation()); @@ -468,7 +480,10 @@ private Stream stringInputsStream() { }); } selector.addPostRunHandler(c -> { - c.put(input.getId(), c.getResultValue()); + String resultValue = c.getResultValue(); + if (resultValue != null) { + c.put(input.getId(), resultValue); + } }); } for (Consumer handler : input.getPreHandlers()) { @@ -496,8 +511,12 @@ private Stream pathInputsStream() { context.put(input.getId(), Paths.get(input.getResultValue())); return context; } - selector.setResourceLoader(resourceLoader); - selector.setTemplateExecutor(templateExecutor); + if (resourceLoader != null) { + selector.setResourceLoader(resourceLoader); + } + if (templateExecutor != null) { + selector.setTemplateExecutor(templateExecutor); + } if (StringUtils.hasText(input.getTemplateLocation())) { selector.setTemplateLocation(input.getTemplateLocation()); } @@ -506,7 +525,10 @@ private Stream pathInputsStream() { } if (input.isStoreResult()) { selector.addPostRunHandler(c -> { - c.put(input.getId(), c.getResultValue()); + Path resultValue = c.getResultValue(); + if (resultValue != null) { + c.put(input.getId(), resultValue); + } }); } for (Consumer handler : input.getPreHandlers()) { @@ -534,8 +556,12 @@ private Stream confirmationInputsStream() { context.put(input.getId(), input.getResultValue()); return context; } - selector.setResourceLoader(resourceLoader); - selector.setTemplateExecutor(templateExecutor); + if (resourceLoader != null) { + selector.setResourceLoader(resourceLoader); + } + if (templateExecutor != null) { + selector.setTemplateExecutor(templateExecutor); + } if (StringUtils.hasText(input.getTemplateLocation())) { selector.setTemplateLocation(input.getTemplateLocation()); } @@ -544,7 +570,9 @@ private Stream confirmationInputsStream() { } if (input.isStoreResult()) { selector.addPostRunHandler(c -> { - c.put(input.getId(), c.getResultValue()); + if (c.getResultValue() != null) { + c.put(input.getId(), c.getResultValue()); + } }); } for (Consumer handler : input.getPreHandlers()) { @@ -588,8 +616,12 @@ private Stream singleItemSelectorsStream() { context.put(input.getId(), input.getResultValue()); return context; } - selector.setResourceLoader(resourceLoader); - selector.setTemplateExecutor(templateExecutor); + if (resourceLoader != null) { + selector.setResourceLoader(resourceLoader); + } + if (templateExecutor != null) { + selector.setTemplateExecutor(templateExecutor); + } if (StringUtils.hasText(input.getTemplateLocation())) { selector.setTemplateLocation(input.getTemplateLocation()); } @@ -635,8 +667,12 @@ private Stream multiItemSelectorsStream() { context.put(input.getId(), input.getResultValues()); return context; } - selector.setResourceLoader(resourceLoader); - selector.setTemplateExecutor(templateExecutor); + if (resourceLoader != null) { + selector.setResourceLoader(resourceLoader); + } + if (templateExecutor != null) { + selector.setTemplateExecutor(templateExecutor); + } if (StringUtils.hasText(input.getTemplateLocation())) { selector.setTemplateLocation(input.getTemplateLocation()); } @@ -670,25 +706,25 @@ private Stream multiItemSelectorsStream() { static class OrderedInputOperation implements Ordered { - private String id; + private @Nullable String id; private int order; - private Function, ComponentContext> operation; - private Function, Optional> next; + private @Nullable Function, ComponentContext> operation; + private @Nullable Function, Optional> next; @Override public int getOrder() { return order; } - public String getId() { + public @Nullable String getId() { return id; } - public Function, ComponentContext> getOperation() { + public @Nullable Function, ComponentContext> getOperation() { return operation; } - public Function, Optional> getNext() { + public @Nullable Function, Optional> getNext() { return next; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java new file mode 100644 index 000000000..623eafd75 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.flow; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageBuilder.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageBuilder.java index 196e605a9..4adbbcf54 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageBuilder.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageBuilder.java @@ -15,7 +15,7 @@ */ package org.springframework.shell.tui.component.message; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; import org.springframework.shell.tui.component.view.control.View; @@ -30,13 +30,13 @@ * @param the payload type. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public final class ShellMessageBuilder { private final T payload; private final ShellMessageHeaderAccessor headerAccessor; - @Nullable - private final Message originalMessage; + private final @Nullable Message originalMessage; private ShellMessageBuilder(T payload, @Nullable Message originalMessage) { diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageHeaderAccessor.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageHeaderAccessor.java index e8aaac754..3530e72b9 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageHeaderAccessor.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/ShellMessageHeaderAccessor.java @@ -21,9 +21,9 @@ import java.util.Set; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; import reactor.util.context.ContextView; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.shell.tui.component.view.control.View; @@ -35,6 +35,7 @@ * Adds standard shell Headers. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ShellMessageHeaderAccessor extends MessageHeaderAccessor { @@ -77,14 +78,12 @@ public void setReadOnlyHeaders(String... readOnlyHeaders) { } } - @Nullable - public Integer getPriority() { + public @Nullable Integer getPriority() { Number priority = getHeader(PRIORITY, Number.class); return (priority != null ? priority.intValue() : null); } - @Nullable - public View getView() { + public @Nullable View getView() { View view = getHeader(VIEW, View.class); return view; } @@ -94,8 +93,7 @@ public View getView() { * * @return the {@link ContextView} header if present. */ - @Nullable - public ContextView getReactorContext() { + public @Nullable ContextView getReactorContext() { return getHeader(REACTOR_CONTEXT, ContextView.class); } @@ -104,14 +102,12 @@ public ContextView getReactorContext() { * * @return the {@link EventLoop.Type} header if present. */ - @Nullable - public EventLoop.Type getEventType() { + public EventLoop.@Nullable Type getEventType() { return getHeader(EVENT_TYPE, EventLoop.Type.class); } @SuppressWarnings("unchecked") - @Nullable - public T getHeader(String key, Class type) { + public @Nullable T getHeader(String key, Class type) { Object value = getHeader(key); if (value == null) { return null; @@ -124,7 +120,7 @@ public T getHeader(String key, Class type) { } @Override - protected void verifyType(String headerName, Object headerValue) { + protected void verifyType(@Nullable String headerName, @Nullable Object headerValue) { if (headerName != null && headerValue != null) { super.verifyType(headerName, headerValue); if (ShellMessageHeaderAccessor.PRIORITY.equals(headerName)) { diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/StaticShellMessageHeaderAccessor.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/StaticShellMessageHeaderAccessor.java index da5e3ae3f..d644134d7 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/StaticShellMessageHeaderAccessor.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/StaticShellMessageHeaderAccessor.java @@ -17,10 +17,10 @@ import java.util.UUID; +import org.jspecify.annotations.Nullable; import reactor.util.context.Context; import reactor.util.context.ContextView; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.shell.tui.component.view.control.View; @@ -31,6 +31,7 @@ * a header. * * @author Janne Valkealahti + * @author Piotr Olaszewski * * @see ShellMessageHeaderAccessor */ @@ -39,8 +40,7 @@ public final class StaticShellMessageHeaderAccessor { private StaticShellMessageHeaderAccessor() { } - @Nullable - public static UUID getId(Message message) { + public static @Nullable UUID getId(Message message) { Object value = message.getHeaders().get(MessageHeaders.ID); if (value == null) { return null; @@ -48,8 +48,7 @@ public static UUID getId(Message message) { return (value instanceof UUID ? (UUID) value : UUID.fromString(value.toString())); } - @Nullable - public static Long getTimestamp(Message message) { + public static @Nullable Long getTimestamp(Message message) { Object value = message.getHeaders().get(MessageHeaders.TIMESTAMP); if (value == null) { return null; @@ -57,14 +56,12 @@ public static Long getTimestamp(Message message) { return (value instanceof Long ? (Long) value : Long.parseLong(value.toString())); } - @Nullable - public static Integer getPriority(Message message) { + public static @Nullable Integer getPriority(Message message) { Number priority = message.getHeaders().get(ShellMessageHeaderAccessor.PRIORITY, Number.class); return (priority != null ? priority.intValue() : null); } - @Nullable - public static View getView(Message message) { + public static @Nullable View getView(Message message) { View view = message.getHeaders().get(ShellMessageHeaderAccessor.VIEW, View.class); return view; } @@ -90,9 +87,8 @@ public static ContextView getReactorContext(Message message) { * @param message the message to get a header from. * @return the {@link EventLoop.Type} header if present. */ - public static EventLoop.Type getEventType(Message message) { - EventLoop.Type eventType = message.getHeaders() + public static EventLoop.@Nullable Type getEventType(Message message) { + return message.getHeaders() .get(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.class); - return eventType; } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java new file mode 100644 index 000000000..4cff09cb2 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.message; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java new file mode 100644 index 000000000..0999b1547 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractComponent.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractComponent.java index 4432da493..6c9b7562c 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractComponent.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractComponent.java @@ -37,6 +37,7 @@ import org.jline.utils.AttributedString; import org.jline.utils.Display; import org.jline.utils.InfoCmp.Capability; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,6 +54,7 @@ * Base class for components. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractComponent> implements ResourceLoaderAware { @@ -70,11 +72,11 @@ public abstract class AbstractComponent> implement private final KeyMap keyMap = new KeyMap<>(); private final List> preRunHandlers = new ArrayList<>(); private final List> postRunHandlers = new ArrayList<>(); - private Function> renderer; + private @Nullable Function> renderer; private boolean printResults = true; - private String templateLocation; - private TemplateExecutor templateExecutor; - private ResourceLoader resourceLoader; + private @Nullable String templateLocation; + private @Nullable TemplateExecutor templateExecutor; + private @Nullable ResourceLoader resourceLoader; public AbstractComponent(Terminal terminal) { Assert.notNull(terminal, "terminal must be set"); @@ -101,7 +103,7 @@ public Terminal getTerminal() { * * @param renderer the display renderer function */ - public void setRenderer(Function> renderer) { + public void setRenderer(@Nullable Function> renderer) { this.renderer = renderer; } @@ -114,7 +116,7 @@ public void setRenderer(Function> renderer) { */ public List render(T context) { log.debug("Rendering with context [{}] as class [{}] in [{}]", context, context.getClass(), this); - return renderer.apply(context); + return renderer == null ? List.of() : renderer.apply(context); } /** @@ -167,7 +169,7 @@ public final T run(ComponentContext context) { * * @return a template executor */ - public TemplateExecutor getTemplateExecutor() { + public @Nullable TemplateExecutor getTemplateExecutor() { return templateExecutor; } @@ -211,7 +213,11 @@ protected boolean hasTty() { * @param attributes the attributes * @return rendered content as attributed strings */ - protected List renderTemplateResource(Map attributes) { + protected List renderTemplateResource(Map attributes) { + Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); + Assert.notNull(templateLocation, "'templateLocation' must not be null"); + Assert.notNull(templateExecutor, "'templateExecutor' must not be null"); + String templateResource = resourceAsString(resourceLoader.getResource(templateLocation)); log.debug("Rendering template: {}", templateResource); log.debug("Rendering template attributes: {}", attributes); @@ -238,7 +244,7 @@ protected List renderTemplateResource(Map attr * @param context the context * @return a component context */ - public abstract T getThisContext(ComponentContext context); + public abstract T getThisContext(@Nullable ComponentContext context); /** * Read input. diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java index 98715929c..341a8dc5b 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java @@ -26,6 +26,7 @@ import org.jline.keymap.KeyMap; import org.jline.terminal.Terminal; import org.jline.utils.InfoCmp.Capability; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +37,7 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import static java.util.Objects.requireNonNullElse; import static org.jline.keymap.KeyMap.ctrl; import static org.jline.keymap.KeyMap.del; import static org.jline.keymap.KeyMap.key; @@ -44,12 +46,13 @@ * Base component for selectors which provide selectable lists. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractSelectorComponent, I extends Nameable & Matchable & Enableable & Selectable & Itemable> extends AbstractComponent { private final static Logger log = LoggerFactory.getLogger(AbstractSelectorComponent.class); - protected final String name; + protected final @Nullable String name; private final List items; private Comparator comparator = (o1, o2) -> 0; private boolean exitSelects; @@ -58,11 +61,11 @@ public abstract class AbstractSelectorComponent items, boolean exitSelects, - Comparator comparator) { + public AbstractSelectorComponent(Terminal terminal, @Nullable String name, List items, boolean exitSelects, + @Nullable Comparator comparator) { super(terminal); this.name = name; this.items = items; @@ -106,7 +109,7 @@ public Function getItemMapper() { * * @param defaultExpose the default item */ - public void setDefaultExpose(I defaultExpose) { + public void setDefaultExpose(@Nullable I defaultExpose) { this.defaultExpose = defaultExpose; if (defaultExpose != null) { expose = true; @@ -232,7 +235,7 @@ else if (start.get() + pos.get() <= 0) { } }); } - List values = thisContext.getItemStates().stream() + List values = requireNonNullElse(thisContext.getItemStates(), new ArrayList>()).stream() .filter(i -> i.selected) .map(i -> i.item) .collect(Collectors.toList()); @@ -255,13 +258,13 @@ private void initialExpose(C context) { List> itemStates = context.getItemStates(); if (itemStates == null) { AtomicInteger index = new AtomicInteger(0); - itemStates = context.getItems().stream() + itemStates = requireNonNullElse(context.getItems(), new ArrayList()).stream() .sorted(comparator) .map(item -> ItemState.of(item, item.getName(), index.getAndIncrement(), item.isEnabled(), item.isSelected())) .collect(Collectors.toList()); } for (int i = 0; i < itemStates.size(); i++) { - if (ObjectUtils.nullSafeEquals(itemStates.get(i).getName(), defaultExpose.getName())) { + if (defaultExpose != null && ObjectUtils.nullSafeEquals(itemStates.get(i).getName(), defaultExpose.getName())) { if (i < maxItems) { this.pos.set(i); } @@ -278,7 +281,7 @@ private ItemStateViewProjection buildItemStateView(int skip, SelectorComponentCo List> itemStates = context.getItemStates(); if (itemStates == null) { AtomicInteger index = new AtomicInteger(0); - itemStates = context.getItems().stream() + itemStates = requireNonNullElse(context.getItems(), new ArrayList()).stream() .sorted(comparator) .map(item -> ItemState.of(item, item.getName(), index.getAndIncrement(), item.isEnabled(), item.isSelected())) .collect(Collectors.toList()); @@ -321,7 +324,7 @@ public interface SelectorComponentContext> getItemStates(); + @Nullable List> getItemStates(); /** * Sets an item states. @@ -363,7 +366,7 @@ public interface SelectorComponentContext> getItemStateView(); + @Nullable List> getItemStateView(); /** * Sets an item state view @@ -384,7 +387,7 @@ public interface SelectorComponentContext getItems(); + @Nullable List getItems(); /** * Sets an items. @@ -412,7 +415,7 @@ public interface SelectorComponentContext getResultItems(); + @Nullable List getResultItems(); /** * Sets a result items. @@ -437,16 +440,16 @@ static , C extends SelectorCompo protected static class BaseSelectorComponentContext, C extends SelectorComponentContext> extends BaseComponentContext implements SelectorComponentContext { - private String name; - private String input; - private List> itemStates; - private List> itemStateView; - private Integer cursorRow; - private List items; - private List resultItems; + private @Nullable String name; + private @Nullable String input; + private @Nullable List> itemStates; + private List> itemStateView = new ArrayList<>(); + private @Nullable Integer cursorRow; + private @Nullable List items; + private @Nullable List resultItems; @Override - public String getName() { + public @Nullable String getName() { return name; } @@ -456,17 +459,17 @@ public void setName(String name) { } @Override - public String getInput() { + public @Nullable String getInput() { return input; } @Override - public void setInput(String input) { + public void setInput(@Nullable String input) { this.input = input; } @Override - public List> getItemStates() { + public @Nullable List> getItemStates() { return itemStates; } @@ -491,13 +494,13 @@ public boolean isResult() { } @Override - public Integer getCursorRow() { + public @Nullable Integer getCursorRow() { return cursorRow; } @Override - public java.util.Map toTemplateModel() { - Map attributes = super.toTemplateModel(); + public Map toTemplateModel() { + Map attributes = super.toTemplateModel(); attributes.put("name", getName()); attributes.put("input", getInput()); attributes.put("itemStates", getItemStates()); @@ -505,14 +508,14 @@ public java.util.Map toTemplateModel() { attributes.put("isResult", isResult()); attributes.put("cursorRow", getCursorRow()); return attributes; - }; + } public void setCursorRow(Integer cursorRow) { this.cursorRow = cursorRow; }; @Override - public List getItems() { + public @Nullable List getItems() { return items; } @@ -522,7 +525,7 @@ public void setItems(List items) { } @Override - public List getResultItems() { + public @Nullable List getResultItems() { return resultItems; } @@ -555,9 +558,9 @@ public static class ItemState implements Matchable { this.selected = selected; } - public boolean matches(String match) { + public boolean matches(@Nullable String match) { return item.matches(match); - }; + } public int getIndex() { return index; @@ -576,7 +579,7 @@ public boolean isEnabled() { } static ItemState of(I item, String name, int index, boolean enabled, boolean selected) { - return new ItemState(item, name, index, enabled, selected); + return new ItemState<>(item, name, index, enabled, selected); } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractTextComponent.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractTextComponent.java index 0f3846197..e47c03a1f 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractTextComponent.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractTextComponent.java @@ -24,6 +24,7 @@ import org.jline.utils.AttributedString; import org.jline.utils.InfoCmp.Capability; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.context.BaseComponentContext; import org.springframework.shell.tui.component.context.ComponentContext; import org.springframework.shell.tui.component.support.AbstractTextComponent.TextComponentContext; @@ -35,20 +36,21 @@ * Base class for components which work on a simple text input. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractTextComponent> extends AbstractComponent { - private final String name; + private final @Nullable String name; public AbstractTextComponent(Terminal terminal) { this(terminal, null); } - public AbstractTextComponent(Terminal terminal, String name) { + public AbstractTextComponent(Terminal terminal, @Nullable String name) { this(terminal, name, null); } - public AbstractTextComponent(Terminal terminal, String name, Function> renderer) { + public AbstractTextComponent(Terminal terminal, @Nullable String name, @Nullable Function> renderer) { super(terminal); this.name = name; setRenderer(renderer); @@ -79,7 +81,7 @@ protected C runInternal(C context) { * * @return a name */ - protected String getName() { + protected @Nullable String getName() { return name; } @@ -90,56 +92,56 @@ public interface TextComponentContext> e * * @return a name */ - String getName(); + @Nullable String getName(); /** * Sets a name. * * @param name the name */ - void setName(String name); + void setName(@Nullable String name); /** * Gets an input. * * @return an input */ - String getInput(); + @Nullable String getInput(); /** * Sets an input. * * @param input the input */ - void setInput(String input); + void setInput(@Nullable String input); /** * Sets a result value. * * @return a result value */ - T getResultValue(); + @Nullable T getResultValue(); /** * Sets a result value. * * @param resultValue the result value */ - void setResultValue(T resultValue); + void setResultValue(@Nullable T resultValue); /** * Sets a message. * * @return a message */ - String getMessage(); + @Nullable String getMessage(); /** * Sets a message. * * @param message the message */ - void setMessage(String message); + void setMessage(@Nullable String message); /** * Sets a message with level. @@ -176,49 +178,49 @@ public enum MessageLevel { public static class BaseTextComponentContext> extends BaseComponentContext implements TextComponentContext { - private String name; - private String input; - private T resultValue; - private String message; + private @Nullable String name; + private @Nullable String input; + private @Nullable T resultValue; + private @Nullable String message; private MessageLevel messageLevel = MessageLevel.INFO; @Override - public String getName() { + public @Nullable String getName() { return name; } @Override - public void setName(String name) { + public void setName(@Nullable String name) { this.name = name; } @Override - public String getInput() { + public @Nullable String getInput() { return input; } @Override - public void setInput(String input) { + public void setInput(@Nullable String input) { this.input = input; } @Override - public T getResultValue() { + public @Nullable T getResultValue() { return resultValue; } @Override - public void setResultValue(T resultValue) { + public void setResultValue(@Nullable T resultValue) { this.resultValue = resultValue; } @Override - public String getMessage() { + public @Nullable String getMessage() { return message; } @Override - public void setMessage(String message) { + public void setMessage(@Nullable String message) { this.message = message; } @@ -239,9 +241,10 @@ public void setMessageLevel(MessageLevel messageLevel) { } @Override - public Map toTemplateModel() { - Map attributes = super.toTemplateModel(); - attributes.put("resultValue", getResultValue() != null ? getResultValue().toString() : null); + public Map toTemplateModel() { + Map attributes = super.toTemplateModel(); + T value = getResultValue(); + attributes.put("resultValue", value != null ? value.toString() : null); attributes.put("name", getName()); attributes.put("message", getMessage()); attributes.put("messageLevel", getMessageLevel()); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/Matchable.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/Matchable.java index a30a47ecf..55277f2e8 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/Matchable.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/Matchable.java @@ -15,7 +15,12 @@ */ package org.springframework.shell.tui.component.support; +import org.jspecify.annotations.Nullable; + +/** + * @author Piotr Olaszewski + */ public interface Matchable { - boolean matches(String match); + boolean matches(@Nullable String match); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorItem.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorItem.java index d3e05bd1e..432696215 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorItem.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorItem.java @@ -15,8 +15,12 @@ */ package org.springframework.shell.tui.component.support; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; +/** + * @author Piotr Olaszewski + */ public interface SelectorItem extends Nameable, Matchable, Enableable, Selectable, Itemable { static SelectorItem of(String name, T item) { @@ -50,7 +54,7 @@ public String getName() { } @Override - public boolean matches(String match) { + public boolean matches(@Nullable String match) { if (!StringUtils.hasText(match)) { return true; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorList.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorList.java index 8f6fba7e3..61fd46fae 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorList.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/SelectorList.java @@ -15,13 +15,18 @@ */ package org.springframework.shell.tui.component.support; +import org.jspecify.annotations.Nullable; + import java.util.ArrayList; import java.util.List; +/** + * @author Piotr Olaszewski + */ public interface SelectorList { void reset(List items); - T getSelected(); + @Nullable T getSelected(); void scrollUp(); void scrollDown(); List> getProjection(); @@ -54,7 +59,7 @@ public void reset(List items) { } @Override - public T getSelected() { + public @Nullable T getSelected() { int index = start + position; if (this.items.isEmpty()) { return null; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java new file mode 100644 index 000000000..47e0ace0c --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.support; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUI.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUI.java index 1cdca0cdd..980929f54 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUI.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUI.java @@ -29,10 +29,10 @@ import org.jline.utils.AttributedString; import org.jline.utils.Display; import org.jline.utils.InfoCmp.Capability; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.lang.Nullable; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.control.View; import org.springframework.shell.tui.component.view.control.ViewService; @@ -51,6 +51,8 @@ import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import static java.util.Objects.requireNonNull; + /** * {@link TerminalUI} is a main component orchestrating terminal, eventloop, * key/mouse events and view structure to work together. In many ways it can @@ -58,6 +60,7 @@ * a screen. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class TerminalUI implements ViewService { @@ -66,15 +69,15 @@ public class TerminalUI implements ViewService { private final BindingReader bindingReader; private final KeyMap keyMap = new KeyMap<>(); private final DefaultScreen virtualDisplay = new DefaultScreen(); - private Display display; - private Size size; - private View rootView; - private View modalView; + private @Nullable Display display; + private @Nullable Size size; + private @Nullable View rootView; + private @Nullable View modalView; private boolean fullScreen; private final KeyBinder keyBinder; private DefaultEventLoop eventLoop = new DefaultEventLoop(); - private View focus = null; - private ThemeResolver themeResolver; + private @Nullable View focus = null; + private @Nullable ThemeResolver themeResolver; private String themeName = "default"; /** @@ -90,12 +93,12 @@ public TerminalUI(Terminal terminal) { } @Override - public View getModal() { + public @Nullable View getModal() { return modalView; } @Override - public void setModal(View view) { + public void setModal(@Nullable View view) { this.modalView = view; } @@ -152,7 +155,7 @@ public void setThemeResolver(ThemeResolver themeResolver) { * * @return a theme resolver */ - public ThemeResolver getThemeResolver() { + public @Nullable ThemeResolver getThemeResolver() { return themeResolver; } @@ -220,16 +223,18 @@ private void render(Rectangle rect) { } } - private BiFunction fullScreenViewRect = (terminal, view) -> { + private BiFunction fullScreenViewRect = (terminal, view) -> { Size s = terminal.getSize(); return new Rectangle(0, 0, s.getColumns(), s.getRows()); }; - private BiFunction nonfullScreenViewRect = (terminal, view) -> { + private BiFunction nonfullScreenViewRect = (terminal, view) -> { Size s = terminal.getSize(); - Rectangle rect = view.getRect(); - if (!rect.isEmpty()) { - return rect; + if (view != null) { + Rectangle rect = view.getRect(); + if (!rect.isEmpty()) { + return rect; + } } return new Rectangle(0, 0, s.getColumns(), 5); }; @@ -259,13 +264,18 @@ public void setNonfullScreenViewRect(BiFunction nonfu private synchronized void display() { log.trace("display() start"); + requireNonNull(display); + requireNonNull(size); + size.copy(terminal.getSize()); if (fullScreen) { display.clear(); display.reset(); display.resize(size.getRows(), size.getColumns()); Rectangle rect = fullScreenViewRect.apply(terminal, rootView); - rootView.setRect(rect.x(), rect.y(), rect.width(), rect.height()); + if (rootView != null) { + rootView.setRect(rect.x(), rect.y(), rect.width(), rect.height()); + } virtualDisplay.resize(size.getRows(), size.getColumns()); virtualDisplay.setShowCursor(false); render(rect); @@ -370,6 +380,9 @@ private void handleMouseEvent(MouseEvent event) { } private void loop() { + requireNonNull(display); + requireNonNull(size); + Attributes attr = terminal.enterRawMode(); registerEventHandling(); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUIBuilder.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUIBuilder.java index 513c14819..ad4101431 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUIBuilder.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/TerminalUIBuilder.java @@ -23,6 +23,7 @@ import org.jline.terminal.Terminal; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.style.ThemeResolver; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -32,13 +33,14 @@ * Builder that can be used to configure and create a {@link TerminalUI}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class TerminalUIBuilder { private final Terminal terminal; private final Set customizers; - private final ThemeResolver themeResolver; - private final String themeName; + private final @Nullable ThemeResolver themeResolver; + private final @Nullable String themeName; /** * Create a new {@link TerminalUIBuilder} instance. @@ -65,8 +67,8 @@ public TerminalUIBuilder(Terminal terminal, TerminalUICustomizer... customizers) * @param themeResolver the theme resolver * @param themeName the theme name */ - public TerminalUIBuilder(Terminal terminal, Set customizers, ThemeResolver themeResolver, - String themeName) { + public TerminalUIBuilder(Terminal terminal, Set customizers, @Nullable ThemeResolver themeResolver, + @Nullable String themeName) { this.terminal = terminal; this.customizers = customizers; this.themeResolver = themeResolver; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractControl.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractControl.java index 3c5576d66..11924c215 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractControl.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractControl.java @@ -19,7 +19,7 @@ import org.jline.utils.AttributedStyle; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.geom.Rectangle; import org.springframework.shell.tui.style.ThemeResolver; import org.springframework.shell.tui.style.ThemeResolver.ResolvedValues; @@ -28,6 +28,7 @@ * Base implementation of a {@link Control}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractControl implements Control { @@ -35,8 +36,8 @@ public abstract class AbstractControl implements Control { private int y = 0; private int width = 0; private int height = 0; - private ThemeResolver themeResolver; - private String themeName; + private @Nullable ThemeResolver themeResolver; + private @Nullable String themeName; @Override public void setRect(int x, int y, int width, int height) { @@ -65,8 +66,7 @@ public void setThemeResolver(@Nullable ThemeResolver themeResolver) { * * @return a theme resolver */ - @Nullable - protected ThemeResolver getThemeResolver() { + protected @Nullable ThemeResolver getThemeResolver() { return themeResolver; } @@ -84,8 +84,7 @@ public void setThemeName(@Nullable String themeName) { * * @return a theme name */ - @Nullable - protected String getThemeName() { + protected @Nullable String getThemeName() { return themeName; } @@ -155,14 +154,14 @@ protected int resolveThemeBackground(String tag, int defaultColor, int fallbackC * @param fallbackSpinner the fallback spinner to use * @return resolved spinner */ - protected Spinner resolveThemeSpinner(String tag, Spinner defaultSpinner, Spinner fallbackSpinner) { + protected Spinner resolveThemeSpinner(String tag, @Nullable Spinner defaultSpinner, Spinner fallbackSpinner) { if (defaultSpinner != null) { return defaultSpinner; } Spinner spinner = null; ThemeResolver themeResolver = getThemeResolver(); if (themeResolver != null) { - spinner = getThemeResolver().resolveSpinnerTag(tag); + spinner = themeResolver.resolveSpinnerTag(tag); } if (spinner == null) { spinner = fallbackSpinner; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractView.java index 254a2ed9c..dd01fa5e9 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AbstractView.java @@ -21,12 +21,12 @@ import java.util.function.BiFunction; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Disposable; import reactor.core.Disposables; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.event.EventLoop; @@ -46,16 +46,17 @@ * {@link Control} providing some common functionality for implementations. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractView extends AbstractControl implements View { private final static Logger log = LoggerFactory.getLogger(AbstractView.class); private final Disposable.Composite disposables = Disposables.composite(); - private BiFunction drawFunction; + private @Nullable BiFunction drawFunction; private boolean hasFocus; private int layer; - private EventLoop eventLoop; - private ViewService viewService; + private @Nullable EventLoop eventLoop; + private @Nullable ViewService viewService; private final Map commands = new HashMap<>(); private Map keyBindings = new HashMap<>(); private Map hotKeyBindings = new HashMap<>(); @@ -94,8 +95,8 @@ public final void init() { init = true; } - private Integer shortcutKey; - private Runnable shortcutAction; + private @Nullable Integer shortcutKey; + private @Nullable Runnable shortcutAction; public void shortcut(Integer key, Runnable runnable) { this.shortcutKey = key; this.shortcutAction = runnable; @@ -171,7 +172,7 @@ public boolean hasFocus() { @Override public MouseHandler getMouseHandler() { log.trace("getMouseHandler() {}", this); - MouseHandler handler = args -> { + return args -> { MouseEvent event = args.event(); int mouse = event.mouse(); View view = null; @@ -179,7 +180,8 @@ public MouseHandler getMouseHandler() { // mouse binding may consume and focus MouseBindingValue mouseBindingValue = getMouseBindings().get(mouse); if (mouseBindingValue != null) { - if (mouseBindingValue.mousePredicate().test(event)) { + Predicate mouseEventPredicate = mouseBindingValue.mousePredicate(); + if (mouseEventPredicate != null && mouseEventPredicate.test(event)) { view = this; consumed = dispatchMouseRunCommand(event, mouseBindingValue); } @@ -190,7 +192,6 @@ public MouseHandler getMouseHandler() { } return MouseHandler.resultOf(args.event(), consumed, view, this); }; - return handler; } /** @@ -219,7 +220,7 @@ public KeyHandler getKeyHandler() { @Override public KeyHandler getHotKeyHandler() { log.trace("getHotKeyHandler() {}", this); - KeyHandler handler = args -> { + return args -> { KeyEvent event = args.event(); View view = null; boolean consumed = false; @@ -234,7 +235,6 @@ public KeyHandler getHotKeyHandler() { } return KeyHandler.resultOf(event, consumed, view); }; - return handler; } /** @@ -253,7 +253,7 @@ public void setDrawFunction(BiFunction drawFunctio * @return null if function is not set * @see #setDrawFunction(BiFunction) */ - public BiFunction getDrawFunction() { + public @Nullable BiFunction getDrawFunction() { return drawFunction; } @@ -271,7 +271,7 @@ public void setEventLoop(@Nullable EventLoop eventLoop) { * * @return event loop */ - protected EventLoop getEventLoop() { + protected @Nullable EventLoop getEventLoop() { return eventLoop; } @@ -281,7 +281,7 @@ protected EventLoop getEventLoop() { * @param viewService the view service */ @Override - public void setViewService(ViewService viewService) { + public void setViewService(@Nullable ViewService viewService) { this.viewService = viewService; } @@ -290,7 +290,7 @@ public void setViewService(ViewService viewService) { * * @return view service */ - protected ViewService getViewService() { + protected @Nullable ViewService getViewService() { return viewService; } @@ -315,7 +315,7 @@ protected void registerKeyBinding(Integer keyType, Runnable keyRunnable) { registerKeyBinding(keyType, null, null, keyRunnable); } - private void registerKeyBinding(Integer keyType, String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) { + private void registerKeyBinding(Integer keyType, @Nullable String keyCommand, @Nullable KeyBindingConsumer keyConsumer, @Nullable Runnable keyRunnable) { keyBindings.compute(keyType, (key, old) -> { return KeyBindingValue.of(old, keyCommand, keyConsumer, keyRunnable); }); @@ -333,15 +333,15 @@ protected void registerHotKeyBinding(Integer keyType, Runnable keyRunnable) { registerHotKeyBinding(keyType, null, null, keyRunnable); } - private void registerHotKeyBinding(Integer keyType, String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) { + private void registerHotKeyBinding(Integer keyType, @Nullable String keyCommand, @Nullable KeyBindingConsumer keyConsumer, @Nullable Runnable keyRunnable) { hotKeyBindings.compute(keyType, (key, old) -> { return KeyBindingValue.of(old, keyCommand, keyConsumer, keyRunnable); }); } - record KeyBindingValue(String keyCommand, KeyBindingConsumer keyConsumer, Runnable keyRunnable) { - static KeyBindingValue of(KeyBindingValue old, String keyCommand, KeyBindingConsumer keyConsumer, - Runnable keyRunnable) { + record KeyBindingValue(@Nullable String keyCommand, @Nullable KeyBindingConsumer keyConsumer, @Nullable Runnable keyRunnable) { + static KeyBindingValue of(@Nullable KeyBindingValue old, @Nullable String keyCommand, @Nullable KeyBindingConsumer keyConsumer, + @Nullable Runnable keyRunnable) { if (old == null) { return new KeyBindingValue(keyCommand, keyConsumer, keyRunnable); } @@ -369,10 +369,10 @@ protected Map getHotKeyBindings() { return hotKeyBindings; } - record MouseBindingValue(String mouseCommand, MouseBindingConsumer mouseConsumer, Runnable mouseRunnable, - Predicate mousePredicate) { - static MouseBindingValue of(MouseBindingValue old, String mouseCommand, MouseBindingConsumer mouseConsumer, - Runnable mouseRunnable, Predicate mousePredicate) { + record MouseBindingValue(@Nullable String mouseCommand, @Nullable MouseBindingConsumer mouseConsumer, @Nullable Runnable mouseRunnable, + @Nullable Predicate mousePredicate) { + static MouseBindingValue of(@Nullable MouseBindingValue old, @Nullable String mouseCommand, @Nullable MouseBindingConsumer mouseConsumer, + @Nullable Runnable mouseRunnable, @Nullable Predicate mousePredicate) { if (old == null) { return new MouseBindingValue(mouseCommand, mouseConsumer, mouseRunnable, mousePredicate); } @@ -404,7 +404,7 @@ protected void registerMouseBinding(Integer keyType, Runnable mouseRunnable) { registerMouseBinding(keyType, null, null, mouseRunnable); } - private void registerMouseBinding(Integer mouseType, String mouseCommand, MouseBindingConsumer mouseConsumer, Runnable mouseRunnable) { + private void registerMouseBinding(Integer mouseType, @Nullable String mouseCommand, @Nullable MouseBindingConsumer mouseConsumer,@Nullable Runnable mouseRunnable) { Predicate mousePredicate = event -> { int x = event.x(); int y = event.y(); @@ -442,7 +442,7 @@ protected boolean dispatchRunnable(Runnable runnable) { } @Override - public boolean runViewCommand(String command) { + public boolean runViewCommand(@Nullable String command) { if (eventLoop == null) { return false; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AppView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AppView.java index 0c490a40c..437f9668d 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AppView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/AppView.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.tui.component.view.control; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.event.KeyEvent; import org.springframework.shell.tui.component.view.event.KeyEvent.Key; @@ -30,13 +31,14 @@ * controlling main viewing area, menubar, statusbar and modal window system. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class AppView extends BoxView { - private GridView grid; - private View main; - private View menu; - private View status; + private @Nullable GridView grid; + private @Nullable View main; + private @Nullable View menu; + private @Nullable View status; private boolean menuVisible = true; private boolean statusVisible = true; @@ -51,19 +53,36 @@ public AppView(View main, View menuBar, View statusBar) { } @Override - public void setThemeName(String themeName) { + public void setThemeName(@Nullable String themeName) { + if (themeName == null) { + return; + } + super.setThemeName(themeName); - main.setThemeName(themeName); - menu.setThemeName(themeName); - status.setThemeName(themeName); + if (main != null) { + main.setThemeName(themeName); + } + if (menu != null) { + menu.setThemeName(themeName); + } + if (status != null) { + status.setThemeName(themeName); + } } @Override - public void setThemeResolver(ThemeResolver themeResolver) { + public void setThemeResolver(@Nullable ThemeResolver themeResolver) { super.setThemeResolver(themeResolver); - main.setThemeResolver(themeResolver); - menu.setThemeResolver(themeResolver); - status.setThemeResolver(themeResolver); + + if (main != null) { + main.setThemeResolver(themeResolver); + } + if (menu != null) { + menu.setThemeResolver(themeResolver); + } + if (status != null) { + status.setThemeResolver(themeResolver); + } } private void initLayout() { @@ -72,24 +91,48 @@ private void initLayout() { grid.setColumnSize(0); grid.clearItems(); if (menuVisible && statusVisible) { - grid.addItem(menu, 0, 0, 1, 1, 0, 0); - grid.addItem(main, 1, 0, 1, 1, 0, 0); - grid.addItem(status, 2, 0, 1, 1, 0, 0); + if (menu != null) { + grid.addItem(menu, 0, 0, 1, 1, 0, 0); + } + if (main != null) { + grid.addItem(main, 1, 0, 1, 1, 0, 0); + } + if (status != null) { + grid.addItem(status, 2, 0, 1, 1, 0, 0); + } } else if (!menuVisible && !statusVisible) { - grid.addItem(menu, 0, 0, 0, 0, 0, 0); - grid.addItem(main, 0, 0, 3, 1, 0, 0); - grid.addItem(status, 2, 0, 0, 0, 0, 0); + if (menu != null) { + grid.addItem(menu, 0, 0, 0, 0, 0, 0); + } + if (main != null) { + grid.addItem(main, 0, 0, 3, 1, 0, 0); + } + if (status != null) { + grid.addItem(status, 2, 0, 0, 0, 0, 0); + } } else if (menuVisible && !statusVisible) { - grid.addItem(menu, 0, 0, 1, 1, 0, 0); - grid.addItem(main, 1, 0, 2, 1, 0, 0); - grid.addItem(status, 2, 0, 0, 1, 0, 0); + if (menu != null) { + grid.addItem(menu, 0, 0, 1, 1, 0, 0); + } + if (main != null) { + grid.addItem(main, 1, 0, 2, 1, 0, 0); + } + if (status != null) { + grid.addItem(status, 2, 0, 0, 1, 0, 0); + } } else if (!menuVisible && statusVisible) { - grid.addItem(menu, 0, 0, 0, 1, 0, 0); - grid.addItem(main, 0, 0, 2, 1, 0, 0); - grid.addItem(status, 2, 0, 1, 1, 0, 0); + if (menu != null) { + grid.addItem(menu, 0, 0, 0, 1, 0, 0); + } + if (main != null) { + grid.addItem(main, 0, 0, 2, 1, 0, 0); + } + if (status != null) { + grid.addItem(status, 2, 0, 1, 1, 0, 0); + } } } @@ -105,6 +148,9 @@ protected void drawInternal(Screen screen) { @Override public MouseHandler getMouseHandler() { + if (grid == null) { + return super.getMouseHandler(); + } MouseHandler handler = grid.getMouseHandler(); return handler.thenIfNotConsumed(super.getMouseHandler()); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/BoxView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/BoxView.java index c906e4639..25065033c 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/BoxView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/BoxView.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.tui.component.view.control; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,11 +32,12 @@ * implementation by either subclassing or wrapping. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class BoxView extends AbstractView { private final static Logger log = LoggerFactory.getLogger(BoxView.class); - private String title = null; + private @Nullable String title = null; private boolean showBorder = false; private int innerX = -1; private int innerY; @@ -50,7 +52,7 @@ public class BoxView extends AbstractView { private int titleStyle = -1; private int focusedTitleColor = -1; private int focusedTitleStyle = -1; - private HorizontalAlign titleAlign; + private @Nullable HorizontalAlign titleAlign; @Override public void setRect(int x, int y, int width, int height) { diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ButtonView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ButtonView.java index fb80689cf..8a3dd8298 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ButtonView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ButtonView.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.tui.component.view.control; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.event.KeyEvent.Key; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.event.KeyHandler; @@ -32,21 +33,22 @@ * {@code ButtonView} is a {@link View} with border and text acting as a button. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ButtonView extends BoxView { - private String text; - private Runnable action; + private @Nullable String text; + private @Nullable Runnable action; public ButtonView() { this(null, null); } - public ButtonView(String text) { + public ButtonView(@Nullable String text) { this(text, null); } - public ButtonView(String text, Runnable action) { + public ButtonView(@Nullable String text, @Nullable Runnable action) { this.text = text; this.action = action; } @@ -84,7 +86,7 @@ protected void drawInternal(Screen screen) { super.drawInternal(screen); } - public Dimension getPreferredDimension() { + public @Nullable Dimension getPreferredDimension() { return null; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/Control.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/Control.java index 2b23f98d3..4b1fa6b73 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/Control.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/Control.java @@ -15,7 +15,7 @@ */ package org.springframework.shell.tui.component.view.control; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.control.cell.Cell; import org.springframework.shell.tui.component.view.screen.Screen; import org.springframework.shell.tui.geom.Rectangle; @@ -26,6 +26,7 @@ * {@link Rectangle} it is bound to and draw into a {@link Screen}. * * @author Janne Valkealahti + * @author Piotr Olaszewski * @see View * @see Cell */ diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/DialogView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/DialogView.java index 8e036d6e6..b3484da1b 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/DialogView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/DialogView.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.ListIterator; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.control.ButtonView.ButtonViewSelectEvent; import org.springframework.shell.tui.component.view.event.EventLoop; @@ -34,27 +35,28 @@ * for a generic content. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class DialogView extends WindowView { - private View content; + private @Nullable View content; private List buttons; public DialogView() { this(null); } - public DialogView(View content, ButtonView... buttons) { + public DialogView(@Nullable View content, ButtonView... buttons) { this(content, Arrays.asList(buttons)); } - public DialogView(View content, List buttons) { + public DialogView(@Nullable View content, List buttons) { this.content = content; this.buttons = buttons; } @Override - public void setEventLoop(EventLoop eventLoop) { + public void setEventLoop(@Nullable EventLoop eventLoop) { // TODO: should find better way to hook into eventloop super.setEventLoop(eventLoop); hookButtonEvents(); @@ -76,14 +78,17 @@ public void setLayer(int index) { private void hookButtonEvents() { buttons.forEach(b -> { - onDestroy(getEventLoop().viewEvents(ButtonViewSelectEvent.class, b) - .subscribe(event -> { - dispatch(); - ViewService viewService = getViewService(); - if (viewService != null) { - viewService.setModal(null); - } - })); + EventLoop eventLoop = getEventLoop(); + if (eventLoop != null) { + onDestroy(eventLoop.viewEvents(ButtonViewSelectEvent.class, b) + .subscribe(event -> { + dispatch(); + ViewService viewService = getViewService(); + if (viewService != null) { + viewService.setModal(null); + } + })); + } }); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/GridView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/GridView.java index 3e2a01358..d98111fe1 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/GridView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/GridView.java @@ -45,8 +45,8 @@ public class GridView extends BoxView { private final static Logger log = LoggerFactory.getLogger(GridView.class); private List gridItems = new ArrayList<>(); - private int[] columnSize; - private int[] rowSize; + private int[] columnSize = new int[0]; + private int[] rowSize = new int[0]; private int minWidth; private int minHeight; private int gapRows; @@ -205,7 +205,11 @@ public MouseHandler getMouseHandler() { return args -> { View focus = null; for (GridItem i : gridItems) { - MouseHandlerResult r = i.view.getMouseHandler().handle(args); + MouseHandler mouseHandler = i.view.getMouseHandler(); + if (mouseHandler == null) { + continue; + } + MouseHandlerResult r = mouseHandler.handle(args); if (r.focus() != null) { focus = r.focus(); break; @@ -233,8 +237,9 @@ private void nextView() { found = true; } } - if (toFocus != null) { - getViewService().setFocus(toFocus); + ViewService viewService = getViewService(); + if (toFocus != null && viewService != null) { + viewService.setFocus(toFocus); } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/InputView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/InputView.java index ad590246a..1dcdc6edd 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/InputView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/InputView.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,11 +29,13 @@ import org.springframework.shell.tui.component.view.screen.Screen; import org.springframework.shell.tui.geom.Position; import org.springframework.shell.tui.geom.Rectangle; +import org.springframework.util.StringUtils; /** * {@code InputView} is used as a text input. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class InputView extends BoxView { @@ -104,9 +107,11 @@ private void dispatchTextChange(String oldText, String newText) { dispatch(ShellMessageBuilder.ofView(this, InputViewTextChangeEvent.of(this, oldText, newText))); } - private void add(String data) { + private void add(@Nullable String data) { String oldText = text.stream().collect(Collectors.joining()); - text.add(cursorIndex, data); + if (StringUtils.hasText(data)) { + text.add(cursorIndex, data); + } moveCursor(1); String newText = text.stream().collect(Collectors.joining()); dispatchTextChange(oldText, newText); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ListView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ListView.java index 5da596441..acc098788 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ListView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ListView.java @@ -24,7 +24,7 @@ import java.util.Set; import java.util.function.BiFunction; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.control.cell.ListCell; import org.springframework.shell.tui.component.view.event.KeyEvent.Key; @@ -45,6 +45,7 @@ * * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ListView extends BoxView { @@ -254,7 +255,7 @@ else if (step > 0) { } } - private T selectedItem() { + private @Nullable T selectedItem() { T selectedItem = null; int active = start + pos; if (active >= 0 && active < items.size()) { @@ -301,9 +302,9 @@ private void click(MouseEvent event) { * * @param item the list view item */ - public record ListViewItemEventArgs(T item) implements ViewEventArgs { + public record ListViewItemEventArgs(@Nullable T item) implements ViewEventArgs { - public static ListViewItemEventArgs of(T item) { + public static ListViewItemEventArgs of(@Nullable T item) { return new ListViewItemEventArgs(item); } } @@ -316,7 +317,7 @@ public static ListViewItemEventArgs of(T item) { */ public record ListViewOpenSelectedItemEvent(View view, ListViewItemEventArgs args) implements ViewEvent { - public static ListViewOpenSelectedItemEvent of(View view, T item) { + public static ListViewOpenSelectedItemEvent of(View view, @Nullable T item) { return new ListViewOpenSelectedItemEvent(view, ListViewItemEventArgs.of(item)); } } @@ -329,7 +330,7 @@ public static ListViewOpenSelectedItemEvent of(View view, T item) { */ public record ListViewSelectedItemChangedEvent(View view, ListViewItemEventArgs args) implements ViewEvent { - public static ListViewSelectedItemChangedEvent of(View view, T item) { + public static ListViewSelectedItemChangedEvent of(View view, @Nullable T item) { return new ListViewSelectedItemChangedEvent(view, ListViewItemEventArgs.of(item)); } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java index 88a81d851..a04f7bb46 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java @@ -22,6 +22,7 @@ import java.util.ListIterator; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,12 +47,13 @@ * Internally {@link MenuView} is used to show the menus. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class MenuBarView extends BoxView { private final Logger log = LoggerFactory.getLogger(MenuBarView.class); private final List items = new ArrayList<>(); - private MenuView currentMenuView; + private @Nullable MenuView currentMenuView; private int activeItemIndex = -1; // Need to keep menuviews alive not to lose their states @@ -220,13 +222,13 @@ private void select(MouseEvent event) { } @Override - public void setThemeName(String themeName) { + public void setThemeName(@Nullable String themeName) { super.setThemeName(themeName); menuViews.values().forEach(view -> view.setThemeName(themeName)); } @Override - public void setThemeResolver(ThemeResolver themeResolver) { + public void setThemeResolver(@Nullable ThemeResolver themeResolver) { super.setThemeResolver(themeResolver); menuViews.values().forEach(view -> view.setThemeResolver(themeResolver)); } @@ -246,9 +248,11 @@ private void closeCurrentMenuView() { } private MenuView buildMenuView(MenuBarItem item) { + EventLoop eventLoop = getEventLoop(); + MenuView menuView = new MenuView(item.getItems()); menuView.init(); - menuView.setEventLoop(getEventLoop()); + menuView.setEventLoop(eventLoop); menuView.setThemeResolver(getThemeResolver()); menuView.setThemeName(getThemeName()); menuView.setViewService(getViewService()); @@ -258,10 +262,12 @@ private MenuView buildMenuView(MenuBarItem item) { int x = positionAtIndex(activeItemIndex); Dimension dim = menuView.getPreferredDimension(); menuView.setRect(rect.x() + x, rect.y() + 1, dim.width(), dim.height()); - menuView.onDestroy(getEventLoop().viewEvents(MenuViewOpenSelectedItemEvent.class, menuView) - .subscribe(event -> { - closeCurrentMenuView(); - })); + if (eventLoop != null) { + menuView.onDestroy(eventLoop.viewEvents(MenuViewOpenSelectedItemEvent.class, menuView) + .subscribe(event -> { + closeCurrentMenuView(); + })); + } return menuView; } @@ -300,15 +306,15 @@ public static class MenuBarItem { private String title; private List items; - private Integer hotKey; + private @Nullable Integer hotKey; public MenuBarItem(String title) { this(title, null); } - public MenuBarItem(String title, MenuItem[] items) { + public MenuBarItem(String title, MenuItem @Nullable [] items) { this.title = title; - this.items = Arrays.asList(items); + this.items = items == null ? List.of() : Arrays.asList(items); } public static MenuBarItem of(String title, MenuItem... items) { @@ -323,7 +329,7 @@ public List getItems() { return items; } - public Integer getHotKey() { + public @Nullable Integer getHotKey() { return hotKey; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuView.java index d4808f6f2..86c168d91 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuView.java @@ -22,10 +22,10 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.lang.Nullable; import org.springframework.shell.tui.component.view.event.KeyEvent.Key; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.event.MouseEvent; @@ -43,6 +43,7 @@ * typically used in layouts which builds complete terminal UI's. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class MenuView extends BoxView { @@ -51,7 +52,7 @@ public class MenuView extends BoxView { private int activeItemIndex = -1; // we support only one radio group - private MenuItem radioActive; + private @Nullable MenuItem radioActive; // keep checked states outside of items itself private Set checkedActive = new HashSet<>(); @@ -338,9 +339,9 @@ public enum MenuItemCheckStyle { public static class MenuItem { private final String title; - private final MenuItemCheckStyle checkStyle; - private final List items; - private Runnable action; + private final @Nullable MenuItemCheckStyle checkStyle; + private final @Nullable List items; + private @Nullable Runnable action; private boolean initialCheckState = false; /** @@ -369,7 +370,7 @@ public MenuItem(String title, MenuItemCheckStyle checkStyle) { * @param checkStyle the check style * @param action the action to run when item is chosen */ - public MenuItem(String title, MenuItemCheckStyle checkStyle, Runnable action) { + public MenuItem(String title, MenuItemCheckStyle checkStyle, @Nullable Runnable action) { this(title, checkStyle, action, false); } @@ -382,7 +383,7 @@ public MenuItem(String title, MenuItemCheckStyle checkStyle, Runnable action) { * @param action the action to run when item is chosen * @param initialCheckState initial checked state */ - public MenuItem(String title, MenuItemCheckStyle checkStyle, Runnable action, boolean initialCheckState) { + public MenuItem(String title, MenuItemCheckStyle checkStyle, @Nullable Runnable action, boolean initialCheckState) { Assert.state(StringUtils.hasText(title), "title must have text"); Assert.notNull(checkStyle, "check style cannot be null"); this.title = title; @@ -434,7 +435,7 @@ public static MenuItem of(String title, MenuItemCheckStyle checkStyle, Runnable return new MenuItem(title, checkStyle, action, initialCheckState); } - public Runnable getAction() { + public @Nullable Runnable getAction() { return action; } @@ -466,8 +467,7 @@ public String getTitle() { * * @return a check style */ - @Nullable - public MenuItemCheckStyle getCheckStyle() { + public @Nullable MenuItemCheckStyle getCheckStyle() { return checkStyle; } @@ -477,8 +477,7 @@ public MenuItemCheckStyle getCheckStyle() { * * @return a menu items */ - @Nullable - public List getItems() { + public @Nullable List getItems() { return items; } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ProgressView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ProgressView.java index 4cbf1bd4f..b82439dad 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ProgressView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ProgressView.java @@ -22,6 +22,7 @@ import java.util.UUID; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.Disposable; @@ -49,6 +50,7 @@ * Defaults to textItem, spinnerItem, percentItem * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ProgressView extends BoxView { @@ -57,8 +59,8 @@ public class ProgressView extends BoxView { private final int tickEnd; private int tickValue; private boolean running = false; - private String description; - private Spinner spinner; + private @Nullable String description; + private @Nullable Spinner spinner; private List items; private GridView grid; private long startTime; @@ -206,7 +208,7 @@ public static ProgressViewItem ofPercent(int size, HorizontalAlign hAligh) { * * @return a progress description */ - public String getDescription() { + public @Nullable String getDescription() { return description; } @@ -242,7 +244,7 @@ public void start() { dispatch(ShellMessageBuilder.ofView(this, ProgressViewStartEvent.of(this, state))); } - private Disposable.Composite disposables; + private Disposable.@Nullable Composite disposables; private final String TAG_KEY = "ProgressView"; private final String TAG_VALUE = UUID.randomUUID().toString(); @@ -398,7 +400,7 @@ private ProgressContext buildContext() { return new ProgressContext() { @Override - public String getDescription() { + public @Nullable String getDescription() { return ProgressView.this.getDescription(); } @@ -418,12 +420,12 @@ public int resolveThemeStyle(String tag, int defaultStyle) { } @Override - public Spinner resolveThemeSpinner(String tag, Spinner defaultSpinner, Spinner fallbackSpinner) { + public Spinner resolveThemeSpinner(String tag, @Nullable Spinner defaultSpinner, Spinner fallbackSpinner) { return ProgressView.this.resolveThemeSpinner(tag, defaultSpinner, fallbackSpinner); } @Override - public Spinner getSpinner() { + public @Nullable Spinner getSpinner() { return ProgressView.this.spinner; } }; @@ -439,7 +441,7 @@ public interface ProgressContext { * * @return a progress description */ - String getDescription(); + @Nullable String getDescription(); /** * Get a state of a {@link ProgressView}. @@ -460,7 +462,7 @@ public interface ProgressContext { * * @return spinner frames */ - Spinner getSpinner(); + @Nullable Spinner getSpinner(); /** * Resolve style using existing {@link ThemeResolver} and {@code theme name}. @@ -472,7 +474,7 @@ public interface ProgressContext { */ int resolveThemeStyle(String tag, int defaultStyle); - Spinner resolveThemeSpinner(String tag, Spinner defaultSpinner, Spinner fallbackSpinner); + Spinner resolveThemeSpinner(String tag, @Nullable Spinner defaultSpinner, Spinner fallbackSpinner); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/StatusBarView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/StatusBarView.java index 655b1d47a..5aa0ef99f 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/StatusBarView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/StatusBarView.java @@ -21,10 +21,10 @@ import java.util.List; import java.util.ListIterator; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.lang.Nullable; import org.springframework.shell.tui.component.message.ShellMessageBuilder; import org.springframework.shell.tui.component.view.event.MouseEvent; import org.springframework.shell.tui.component.view.event.MouseHandler; @@ -44,12 +44,13 @@ * is {@code 0}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class StatusBarView extends BoxView { private final Logger log = LoggerFactory.getLogger(StatusBarView.class); private final List items = new ArrayList<>(); - private String itemSeparator = " | "; + private @Nullable String itemSeparator = " | "; public StatusBarView() { this(new StatusItem[0]); @@ -73,8 +74,7 @@ protected String getBackgroundStyle() { * * @return a separator */ - @Nullable - public String getItemSeparator() { + public @Nullable String getItemSeparator() { return itemSeparator; } @@ -153,17 +153,21 @@ public MouseHandler getMouseHandler() { }; } - private StatusItem itemAt(int x, int y) { + private @Nullable StatusItem itemAt(int x, int y) { Rectangle rect = getRect(); if (!rect.contains(x, y)) { return null; } int ix = 0; for (StatusItem item : items) { - if (x < ix + item.getTitle().length()) { + String title = item.getTitle(); + if (!StringUtils.hasText(title)) { + continue; + } + if (x < ix + title.length()) { return item; } - ix += item.getTitle().length(); + ix += title.length(); } return null; } @@ -218,8 +222,8 @@ private void registerHotKeys() { public static class StatusItem { private String title; - private Runnable action; - private Integer hotKey; + private @Nullable Runnable action; + private @Nullable Integer hotKey; private boolean primary = true; private int priority = 0; @@ -227,11 +231,11 @@ public StatusItem(String title) { this(title, null); } - public StatusItem(String title, Runnable action) { + public StatusItem(String title, @Nullable Runnable action) { this(title, action, null); } - public StatusItem(String title, Runnable action, Integer hotKey) { + public StatusItem(String title, @Nullable Runnable action, @Nullable Integer hotKey) { this.title = title; this.action = action; this.hotKey = hotKey; @@ -261,7 +265,7 @@ public static StatusItem of(String title, Runnable action, Integer hotKey, boole return new StatusItem(title, action, hotKey, primary, priority); } - public String getTitle() { + public @Nullable String getTitle() { return title; } @@ -269,7 +273,7 @@ public void setTitle(String title) { this.title = title; } - public Runnable getAction() { + public @Nullable Runnable getAction() { return action; } @@ -278,7 +282,7 @@ public StatusItem setAction(Runnable action) { return this; } - public Integer getHotKey() { + public @Nullable Integer getHotKey() { return hotKey; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/View.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/View.java index 5905b2cd4..fdd10b024 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/View.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/View.java @@ -17,7 +17,7 @@ import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.event.EventLoop; import org.springframework.shell.tui.component.view.event.KeyHandler; import org.springframework.shell.tui.component.view.event.MouseHandler; @@ -27,6 +27,7 @@ * itself and contains zero or more nested {@code Views}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface View extends Control { @@ -61,7 +62,6 @@ public interface View extends Control { * @return a view mouse handler * @see MouseHandler */ - @Nullable MouseHandler getMouseHandler(); /** @@ -71,7 +71,6 @@ public interface View extends Control { * @return a view key handler * @see KeyHandler */ - @Nullable KeyHandler getKeyHandler(); /** @@ -81,7 +80,6 @@ public interface View extends Control { * @return a view hotkey handler * @see KeyHandler */ - @Nullable KeyHandler getHotKeyHandler(); /** diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ViewService.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ViewService.java index af7763ad8..68c7cd0d3 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ViewService.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/ViewService.java @@ -15,12 +15,13 @@ */ package org.springframework.shell.tui.component.view.control; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Provides services for a {@link View} like handling modals. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface ViewService { @@ -29,8 +30,7 @@ public interface ViewService { * * @return current modal view */ - @Nullable - View getModal(); + @Nullable View getModal(); /** * Sets a new modal view. Setting modal to {@code null} clears existing modal. diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/AbstractListCell.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/AbstractListCell.java index e9d111d1c..586c491ea 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/AbstractListCell.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/AbstractListCell.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.tui.component.view.control.cell; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.control.ListView.ItemStyle; import org.springframework.shell.tui.component.view.screen.Screen; import org.springframework.shell.tui.component.view.screen.Screen.Writer; @@ -26,10 +27,11 @@ * Base implementation of a {@link ListCell}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractListCell extends AbstractCell implements ListCell { - private ItemStyle itemStyle; + private @Nullable ItemStyle itemStyle; private boolean selected; public AbstractListCell(T item) { @@ -45,7 +47,7 @@ protected String getText() { return getItem().toString(); } - protected String getIndicator() { + protected @Nullable String getIndicator() { if (getItemStyle() == ItemStyle.CHECKED || getItemStyle() == ItemStyle.RADIO) { return selected ? "[x]" : "[ ]"; } @@ -85,7 +87,7 @@ public void setItemStyle(ItemStyle itemStyle) { this.itemStyle = itemStyle; } - public ItemStyle getItemStyle() { + public @Nullable ItemStyle getItemStyle() { return itemStyle; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java new file mode 100644 index 000000000..819c71d9f --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.view.control.cell; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java new file mode 100644 index 000000000..8442e2279 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.view.control; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/DefaultEventLoop.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/DefaultEventLoop.java index deedf987d..9eba38e71 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/DefaultEventLoop.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/DefaultEventLoop.java @@ -23,9 +23,11 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; import reactor.core.Disposable; import reactor.core.Disposables; import reactor.core.publisher.Flux; @@ -50,6 +52,7 @@ * Default implementation of an {@link EventLoop}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class DefaultEventLoop implements EventLoop { @@ -67,7 +70,7 @@ public DefaultEventLoop() { this(null); } - public DefaultEventLoop(List processors) { + public DefaultEventLoop(@Nullable List processors) { this.processors = new ArrayList<>(); if (processors != null) { this.processors.addAll(processors); @@ -127,10 +130,12 @@ public Flux events(EventLoop.Type type, Class clazz) { @SuppressWarnings("unchecked") public Flux events(Type type, ParameterizedTypeReference typeRef) { ResolvableType resolvableType = ResolvableType.forType(typeRef); + Class rawClass = resolvableType.getRawClass(); + Assert.state(rawClass != null, "'rawClass' must not be null"); return (Flux) events() .filter(m -> type.equals(StaticShellMessageHeaderAccessor.getEventType(m))) .map(m -> m.getPayload()) - .ofType(resolvableType.getRawClass()); + .ofType(rawClass); } @Override diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyEvent.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyEvent.java index ad90689d0..ca2b563b7 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyEvent.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyEvent.java @@ -15,15 +15,17 @@ */ package org.springframework.shell.tui.component.view.event; +import org.jspecify.annotations.Nullable; + /** * * mask special keys unicode keys ascii keys * [ ] [ ] [ ] [ ] * 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 * - * + * @author Piotr Olaszewski */ -public record KeyEvent(int key, String data) { +public record KeyEvent(int key, @Nullable String data) { public static KeyEvent of(int key) { return new KeyEvent(key, null); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyHandler.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyHandler.java index 78632db08..e4305e8b4 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyHandler.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/KeyHandler.java @@ -17,7 +17,7 @@ import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.control.View; /** @@ -25,6 +25,7 @@ * {@link KeyHandlerResult}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ @FunctionalInterface public interface KeyHandler { @@ -105,7 +106,7 @@ static KeyHandlerArgs argsOf(KeyEvent event) { * @param focus the view * @return a Key handler result */ - static KeyHandlerResult resultOf(KeyEvent event, boolean consumed, View focus) { + static KeyHandlerResult resultOf(KeyEvent event, boolean consumed, @Nullable View focus) { return new KeyHandlerResult(event, consumed, focus, null); } @@ -126,6 +127,6 @@ record KeyHandlerArgs(KeyEvent event) { * @param capture the view which captured an event */ record KeyHandlerResult(@Nullable KeyEvent event, boolean consumed, @Nullable View focus, - @Nullable View capture) { + @Nullable View capture) { } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/MouseHandler.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/MouseHandler.java index 400d1f8a9..7733dc0aa 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/MouseHandler.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/MouseHandler.java @@ -17,7 +17,7 @@ import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.control.View; /** @@ -27,6 +27,7 @@ * {@link MouseHandler} itself don't define any restrictions how it's used. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ @FunctionalInterface public interface MouseHandler { @@ -117,7 +118,7 @@ record MouseHandlerArgs(MouseEvent event) { * @param capture the view which captured an event * @return a mouse handler result */ - static MouseHandlerResult resultOf(MouseEvent event, boolean consumed, View focus, View capture) { + static MouseHandlerResult resultOf(MouseEvent event, boolean consumed, @Nullable View focus, @Nullable View capture) { return new MouseHandlerResult(event, consumed, focus, capture); } @@ -130,6 +131,6 @@ static MouseHandlerResult resultOf(MouseEvent event, boolean consumed, View focu * @param capture the view which captured an event */ record MouseHandlerResult(@Nullable MouseEvent event, boolean consumed, @Nullable View focus, - @Nullable View capture) { + @Nullable View capture) { } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java new file mode 100644 index 000000000..0d87c37de --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.view.event; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java new file mode 100644 index 000000000..dae7538c1 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.view.event.processor; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java new file mode 100644 index 000000000..177e272e5 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.view; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/DefaultScreen.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/DefaultScreen.java index f43cf8187..748f08570 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/DefaultScreen.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/DefaultScreen.java @@ -23,6 +23,7 @@ import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +39,7 @@ * Default implementation of a {@link Screen}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class DefaultScreen implements Screen, DisplayLines { @@ -96,7 +98,7 @@ public ScreenItem[][] getItems() { } @Override - public Screen clip(int x, int y, int width, int height) { + public @Nullable Screen clip(int x, int y, int width, int height) { return null; } @@ -169,14 +171,14 @@ else if (item.getBorder() > 0) { */ private static class DefaultScreenItem implements ScreenItem { - CharSequence content; + @Nullable CharSequence content; int foreground = -1; int background = -1; int style = -1; int border; @Override - public CharSequence getContent() { + public @Nullable CharSequence getContent() { return content; } @@ -242,7 +244,7 @@ private void reset() { private class Layer { DefaultScreenItem[][] items = new DefaultScreenItem[rows][columns]; - DefaultScreenItem getScreenItem(int x, int y) { + @Nullable DefaultScreenItem getScreenItem(int x, int y) { if (y < rows && x < columns) { if (items[y][x] == null) { items[y][x] = new DefaultScreenItem(); @@ -362,7 +364,7 @@ public void background(Rectangle rect, int color) { } @Override - public void text(String text, Rectangle rect, HorizontalAlign hAlign, VerticalAlign vAlign) { + public void text(String text, Rectangle rect, @Nullable HorizontalAlign hAlign, @Nullable VerticalAlign vAlign) { int x = rect.x(); if (hAlign == HorizontalAlign.CENTER) { x = x + rect.width() / 2; @@ -391,6 +393,9 @@ private void printBorderHorizontal(int x, int y, int width) { continue; } DefaultScreenItem item = layer.getScreenItem(i, y); + if (item == null) { + continue; + } if (i > x) { item.border |= ScreenItem.BORDER_RIGHT; } @@ -410,6 +415,9 @@ private void printBorderVertical(int x, int y, int height) { continue; } DefaultScreenItem item = layer.getScreenItem(x, i); + if (item == null) { + continue; + } if (i > y) { item.border |= ScreenItem.BORDER_BOTTOM; } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/Screen.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/Screen.java index 962869709..6ff60f0bc 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/Screen.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/Screen.java @@ -17,7 +17,7 @@ import org.jline.utils.AttributedString; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.geom.HorizontalAlign; import org.springframework.shell.tui.geom.Position; import org.springframework.shell.tui.geom.Rectangle; @@ -29,6 +29,7 @@ * with visible content. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface Screen { @@ -91,7 +92,7 @@ public interface Screen { * @param height the height * @return new clipped screen */ - Screen clip(int x, int y, int width, int height); + @Nullable Screen clip(int x, int y, int width, int height); /** * Interface to write into a {@link Screen}. Contains convenient methods user is diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/ScreenItem.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/ScreenItem.java index b21b68701..8746a1364 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/ScreenItem.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/ScreenItem.java @@ -15,10 +15,13 @@ */ package org.springframework.shell.tui.component.view.screen; +import org.jspecify.annotations.Nullable; + /** * * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface ScreenItem { @@ -36,7 +39,7 @@ public interface ScreenItem { static final int BORDER_RIGHT = BORDER_LEFT << 2; static final int BORDER_BOTTOM = BORDER_LEFT << 3; - CharSequence getContent(); + @Nullable CharSequence getContent(); int getBorder(); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java new file mode 100644 index 000000000..dd0a0e671 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.component.view.screen; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java new file mode 100644 index 000000000..f563e9847 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.geom; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/PartsTextRenderer.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/PartsTextRenderer.java index dc3e24f5c..c92b28b37 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/PartsTextRenderer.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/PartsTextRenderer.java @@ -19,11 +19,15 @@ import java.util.Locale; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.stringtemplate.v4.AttributeRenderer; import org.springframework.shell.tui.style.PartsText.PartText; import org.springframework.util.Assert; +/** + * @author Piotr Olaszewski + */ public class PartsTextRenderer implements AttributeRenderer { private final ThemeResolver themeResolver; @@ -41,7 +45,7 @@ public String toString(PartsText value, String formatString, Locale locale) { int len = 0; int dots = 2; int prefix = values.prefix; - int width = values.width; + int width = values.width == null ? formatString.length() : values.width; int max = width - prefix; List parts = value.getParts(); @@ -95,15 +99,15 @@ else if (currentLen > max) { } private static class Values { - Integer width; - Integer prefix; - String textStyle; - String matchStyle; + @Nullable Integer width; + int prefix = 0; + @Nullable String textStyle; + @Nullable String matchStyle; - public void setWidth(Integer width) { + public void setWidth(int width) { this.width = width; } - public void setPrefix(Integer prefix) { + public void setPrefix(int prefix) { this.prefix = prefix; } public void setTextStyle(String textStyle) { diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StringToStyleExpressionRenderer.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StringToStyleExpressionRenderer.java index 684f45b7e..0de7d58e3 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StringToStyleExpressionRenderer.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StringToStyleExpressionRenderer.java @@ -19,6 +19,7 @@ import java.util.stream.Stream; import org.jline.style.StyleExpression; +import org.jspecify.annotations.Nullable; import org.stringtemplate.v4.AttributeRenderer; import org.springframework.util.Assert; @@ -30,6 +31,7 @@ * settings. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class StringToStyleExpressionRenderer implements AttributeRenderer { @@ -42,7 +44,7 @@ public StringToStyleExpressionRenderer(ThemeResolver themeResolver) { } @Override - public String toString(String value, String formatString, Locale locale) { + public String toString(String value, @Nullable String formatString, Locale locale) { if (!StringUtils.hasText(formatString)) { return value; } @@ -52,8 +54,9 @@ else if (formatString.startsWith("style-")) { else if (formatString.startsWith(TRUNCATE)) { String f = formatString.substring(TRUNCATE.length()); TruncateValues config = mapValues(f); - if (value.length() + config.prefix > config.width) { - return String.format(locale, "%1." + (config.width - config.prefix - 2) + "s.." , value); + Integer width = config.width; + if (width != null && value.length() + config.prefix > width) { + return String.format(locale, "%1." + (width - config.prefix - 2) + "s.." , value); } else { return value; @@ -65,13 +68,13 @@ else if (formatString.startsWith(TRUNCATE)) { } private static class TruncateValues { - Integer width; - Integer prefix; + @Nullable Integer width; + int prefix = 0; public void setWidth(Integer width) { this.width = width; } - public void setPrefix(Integer prefix) { + public void setPrefix(int prefix) { this.prefix = prefix; } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StyleSettings.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StyleSettings.java index 5f8a98a78..eec7f9025 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StyleSettings.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/StyleSettings.java @@ -15,10 +15,13 @@ */ package org.springframework.shell.tui.style; +import org.jspecify.annotations.Nullable; + /** * Base class defining a settings for styles. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class StyleSettings { @@ -198,7 +201,11 @@ public String statusbarBackground() { * @param tag the tag * @return a theme setting */ - public String resolveTag(String tag) { + public String resolveTag(@Nullable String tag) { + if (tag == null) { + throw new IllegalArgumentException("Unknown tag 'null'"); + } + switch (tag) { case TAG_TITLE: return title(); @@ -259,7 +266,7 @@ public static StyleSettings dump() { * @return array of all tags */ public static String[] tags() { - return new String[] { + return new String[]{ TAG_TITLE, TAG_VALUE, TAG_LIST_KEY, diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/TemplateExecutor.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/TemplateExecutor.java index 56ab85fc6..a3346d9a7 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/TemplateExecutor.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/TemplateExecutor.java @@ -20,6 +20,7 @@ import java.util.stream.Stream; import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.stringtemplate.v4.ST; @@ -32,6 +33,7 @@ * Template executor which knows to use styling. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class TemplateExecutor { @@ -54,7 +56,7 @@ public TemplateExecutor(ThemeResolver themeResolver) { * @param attributes the ST template attributes * @return a rendered template */ - public AttributedString render(String template, Map attributes) { + public AttributedString render(String template, Map attributes) { STGroup group = new STGroup(); group.setListener(ERROR_LISTENER); group.registerRenderer(String.class, renderer1); @@ -76,7 +78,7 @@ public AttributedString render(String template, Map attributes) * @param attributes the ST template attributes * @return a rendered template */ - public AttributedString renderGroup(String template, Map attributes) { + public AttributedString renderGroup(String template, Map attributes) { STGroup group = new STGroupString(template); group.setListener(ERROR_LISTENER); group.registerRenderer(String.class, renderer1); diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeRegistry.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeRegistry.java index cceb92b46..3b69aa9c9 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeRegistry.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeRegistry.java @@ -19,12 +19,14 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** * Registry which stores {@link Theme}'s with its name. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ThemeRegistry { @@ -36,7 +38,7 @@ public class ThemeRegistry { * @param name the theme name * @return a theme */ - public Theme get(String name) { + public @Nullable Theme get(String name) { return themes.get(name); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeResolver.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeResolver.java index 733b562e4..3b8e53d2a 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeResolver.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/ThemeResolver.java @@ -25,13 +25,16 @@ import org.jline.utils.AttributedStyle; import org.jline.utils.Colors; +import org.jspecify.annotations.Nullable; import org.springframework.shell.tui.component.view.control.Spinner; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Service which helps to do various things with styles. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class ThemeResolver { @@ -39,7 +42,7 @@ public class ThemeResolver { private StyleResolver styleResolver = new StyleResolver(styleSource, "default"); private StyleExpression styleExpression = new StyleExpression(styleResolver); private ThemeRegistry themeRegistry; - private final Theme theme; + private final @Nullable Theme theme; public ThemeResolver(ThemeRegistry themeRegistry, String themeName) { this.themeRegistry = themeRegistry; @@ -116,7 +119,8 @@ public AttributedString evaluateExpression(String expression) { * @param tag the tag * @return a style */ - public String resolveStyleTag(String tag) { + public String resolveStyleTag(@Nullable String tag) { + Assert.state(theme != null, "'theme' must not be null"); return theme.getSettings().styles().resolveTag(tag); } @@ -127,8 +131,9 @@ public String resolveStyleTag(String tag) { * @param themeName the theme name * @return a style */ - public String resolveStyleTag(String tag, String themeName) { + public String resolveStyleTag(String tag, @Nullable String themeName) { Theme t = StringUtils.hasText(themeName) ? themeRegistry.get(themeName) : theme; + Assert.state(t != null, "'theme' must not be null"); return t.getSettings().styles().resolveTag(tag); } @@ -139,6 +144,7 @@ public String resolveStyleTag(String tag, String themeName) { * @return a style */ public String resolveFigureTag(String tag) { + Assert.state(theme != null, "'theme' must not be null"); return theme.getSettings().figures().resolveTag(tag); } @@ -149,6 +155,7 @@ public String resolveFigureTag(String tag) { * @return a spinner */ public Spinner resolveSpinnerTag(String tag) { + Assert.state(theme != null, "'theme' must not be null"); return theme.getSettings().spinners().resolveTag(tag); } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java new file mode 100644 index 000000000..c79615ce9 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.style; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/SearchMatchResult.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/SearchMatchResult.java index 77cc75a5d..1c898f337 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/SearchMatchResult.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/SearchMatchResult.java @@ -15,10 +15,13 @@ */ package org.springframework.shell.tui.support.search; +import org.jspecify.annotations.Nullable; + /** * Interface defining result used in {@link SearchMatch}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public interface SearchMatchResult { @@ -55,7 +58,7 @@ public interface SearchMatchResult { * * @return {@link SearchMatchAlgorithm} handling a search */ - SearchMatchAlgorithm getAlgorithm(); + @Nullable SearchMatchAlgorithm getAlgorithm(); /** * Construct {@link SearchMatchResult} with given parameters. @@ -66,7 +69,7 @@ public interface SearchMatchResult { * @param positions the positions * @return a search match result */ - public static SearchMatchResult of(int start, int end, int score, int[] positions, SearchMatchAlgorithm algo) { + public static SearchMatchResult of(int start, int end, int score, int[] positions, @Nullable SearchMatchAlgorithm algo) { return new DefaultResult(start, end, score, positions, algo); } @@ -84,9 +87,9 @@ static class DefaultResult implements SearchMatchResult { int end; int score; int[] positions; - SearchMatchAlgorithm algo; + @Nullable SearchMatchAlgorithm algo; - DefaultResult(int start, int end, int score, int[] positions, SearchMatchAlgorithm algo) { + DefaultResult(int start, int end, int score, int[] positions, @Nullable SearchMatchAlgorithm algo) { this.start = start; this.end = end; this.score = score; @@ -115,7 +118,7 @@ public int[] getPositions() { } @Override - public SearchMatchAlgorithm getAlgorithm() { + public @Nullable SearchMatchAlgorithm getAlgorithm() { return algo; } } diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java new file mode 100644 index 000000000..d361293e7 --- /dev/null +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.support.search; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/PartsTextRendererTests.java b/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/PartsTextRendererTests.java index 77118815d..5e7a232ff 100644 --- a/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/PartsTextRendererTests.java +++ b/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/PartsTextRendererTests.java @@ -20,6 +20,7 @@ import org.jline.utils.AttributedString; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -27,6 +28,7 @@ import org.springframework.shell.tui.style.PartsText.PartText; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class PartsTextRendererTests { @@ -235,4 +237,74 @@ void test(String expression, PartsText text, String expected) { assertThat(raw).isEqualTo(expected); } + @Test + void handleMissingWidth() { + PartsText partsText = PartsText.of( + PartText.of("a", false), + PartText.of("b", true), + PartText.of("cd", false), + PartText.of("efg", true), + PartText.of("h", false), + PartText.of("i", true), + PartText.of("jkl", true) + ); + + String rendered = renderer.toString(partsText, "prefix:0,textStyle:style-item-selector,matchStyle:style-level-warn", LOCALE); + AttributedString evaluated = themeResolver.evaluateExpression(rendered); + String raw = AttributedString.stripAnsi(evaluated.toAnsi()); + assertThat(raw).isEqualTo("abcdefghijkl"); + } + + @Test + void handleMissingPrefix() { + PartsText partsText = PartsText.of( + PartText.of("a", false), + PartText.of("b", true), + PartText.of("cd", false), + PartText.of("efg", true), + PartText.of("h", false), + PartText.of("i", true), + PartText.of("jkl", true) + ); + + String rendered = renderer.toString(partsText, "width:3,textStyle:style-item-selector,matchStyle:style-level-warn", LOCALE); + AttributedString evaluated = themeResolver.evaluateExpression(rendered); + String raw = AttributedString.stripAnsi(evaluated.toAnsi()); + assertThat(raw).isEqualTo("a.."); + } + + @Test + void handleMissingTextStyle() { + PartsText partsText = PartsText.of( + PartText.of("a", false), + PartText.of("b", true), + PartText.of("cd", false), + PartText.of("efg", true), + PartText.of("h", false), + PartText.of("i", true), + PartText.of("jkl", true) + ); + + assertThatThrownBy(() -> renderer.toString(partsText, "width:3,prefix:0,matchStyle:style-level-warn", LOCALE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown tag 'null'"); + } + + @Test + void handleMissingMatchStyle() { + PartsText partsText = PartsText.of( + PartText.of("a", true), + PartText.of("b", true), + PartText.of("cd", false), + PartText.of("efg", true), + PartText.of("h", false), + PartText.of("i", true), + PartText.of("jkl", true) + ); + + assertThatThrownBy(() -> renderer.toString(partsText, "width:3,prefix:0,textStyle:style-item-selector", LOCALE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unknown tag 'null'"); + } + } diff --git a/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/StringToStyleExpressionRendererTests.java b/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/StringToStyleExpressionRendererTests.java index 86790be57..93d7229d1 100644 --- a/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/StringToStyleExpressionRendererTests.java +++ b/spring-shell-tui/src/test/java/org/springframework/shell/tui/style/StringToStyleExpressionRendererTests.java @@ -68,4 +68,14 @@ static Stream truncate() { void truncate(String value, String expression, String expected) { assertThat(renderer.toString(value, expression, LOCALE)).isEqualTo(expected); } + + @Test + void handleMissingPrefix() { + assertThat(renderer.toString("0123456789", "truncate-width:6", LOCALE)).isEqualTo("0123.."); + } + + @Test + void handleMissingWidth() { + assertThat(renderer.toString("0123456789", "truncate-prefix:6", LOCALE)).isEqualTo("0123456789"); + } } From 12ae734f2c2212f0bc3b2b62d201d1033c94003c Mon Sep 17 00:00:00 2001 From: Piotr Olaszewski Date: Thu, 30 Oct 2025 21:31:59 +0100 Subject: [PATCH 3/5] Cleanup Signed-off-by: Piotr Olaszewski --- spring-shell-tui/pom.xml | 5 +++++ .../shell/tui/component/context/package-info.java | 2 +- .../shell/tui/component/flow/package-info.java | 2 +- .../shell/tui/component/message/package-info.java | 2 +- .../springframework/shell/tui/component/package-info.java | 2 +- .../tui/component/support/AbstractSelectorComponent.java | 1 + .../shell/tui/component/support/package-info.java | 2 +- .../shell/tui/component/view/control/MenuBarView.java | 1 + .../shell/tui/component/view/control/cell/package-info.java | 2 +- .../shell/tui/component/view/control/package-info.java | 2 +- .../shell/tui/component/view/event/package-info.java | 2 +- .../tui/component/view/event/processor/package-info.java | 2 +- .../shell/tui/component/view/package-info.java | 2 +- .../shell/tui/component/view/screen/package-info.java | 2 +- .../org/springframework/shell/tui/geom/package-info.java | 2 +- .../org/springframework/shell/tui/style/package-info.java | 2 +- .../shell/tui/support/search/package-info.java | 2 +- 17 files changed, 21 insertions(+), 14 deletions(-) diff --git a/spring-shell-tui/pom.xml b/spring-shell-tui/pom.xml index 7a6406cd6..ba0a3528d 100644 --- a/spring-shell-tui/pom.xml +++ b/spring-shell-tui/pom.xml @@ -66,6 +66,11 @@ slf4j-api ${slf4j.version}
+ + org.jspecify + jspecify + ${jspecify.version} + diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java index fe455127c..5933468c8 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/context/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.context; +package org.springframework.shell.tui.component.context; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java index 623eafd75..34c8ee7bc 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/flow/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.flow; +package org.springframework.shell.tui.component.flow; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java index 4cff09cb2..c8deb4009 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/message/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.message; +package org.springframework.shell.tui.component.message; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java index 0999b1547..b83a8f932 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component; +package org.springframework.shell.tui.component; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java index 341a8dc5b..b1e39ba81 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/AbstractSelectorComponent.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.tui.component.support; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java index 47e0ace0c..11ddff3cd 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/support/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.support; +package org.springframework.shell.tui.component.support; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java index a04f7bb46..214edeccf 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/MenuBarView.java @@ -28,6 +28,7 @@ import org.springframework.shell.tui.component.view.control.MenuView.MenuItem; import org.springframework.shell.tui.component.view.control.MenuView.MenuViewOpenSelectedItemEvent; +import org.springframework.shell.tui.component.view.event.EventLoop; import org.springframework.shell.tui.component.view.event.KeyEvent.Key; import org.springframework.shell.tui.component.view.event.KeyHandler; import org.springframework.shell.tui.component.view.event.MouseEvent; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java index 819c71d9f..520c1ed36 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/cell/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.view.control.cell; +package org.springframework.shell.tui.component.view.control.cell; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java index 8442e2279..86a06e5a0 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/control/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.view.control; +package org.springframework.shell.tui.component.view.control; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java index 0d87c37de..abbcbe4f7 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.view.event; +package org.springframework.shell.tui.component.view.event; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java index dae7538c1..78f586d37 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/event/processor/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.view.event.processor; +package org.springframework.shell.tui.component.view.event.processor; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java index 177e272e5..b1fb98f76 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.view; +package org.springframework.shell.tui.component.view; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java index dd0a0e671..c72bbe8aa 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/component/view/screen/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.component.view.screen; +package org.springframework.shell.tui.component.view.screen; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java index f563e9847..903301966 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/geom/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.geom; +package org.springframework.shell.tui.geom; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java index c79615ce9..4eb076836 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/style/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.style; +package org.springframework.shell.tui.style; import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java b/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java index d361293e7..2a1d8ff6e 100644 --- a/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java +++ b/spring-shell-tui/src/main/java/org/springframework/shell/tui/support/search/package-info.java @@ -1,4 +1,4 @@ @NullMarked -package org.springframework.shell.support.search; +package org.springframework.shell.tui.support.search; import org.jspecify.annotations.NullMarked; From 773e576f3435ff5e2f46a4e82c2aef4c2fb2585d Mon Sep 17 00:00:00 2001 From: Piotr Olaszewski Date: Thu, 30 Oct 2025 23:42:44 +0100 Subject: [PATCH 4/5] Add JSpecify to spring-shell-standard Signed-off-by: Piotr Olaszewski --- .github/workflows/ci-pr.yml | 2 +- .../StandardCommandsAutoConfiguration.java | 14 ++- ...tandardCommandsAutoConfigurationTests.java | 2 +- .../shell/command/CommandRegistration.java | 4 +- .../standard/AbstractShellComponent.java | 118 ++++++++++-------- .../shell/standard/FileValueProvider.java | 17 ++- .../StandardMethodTargetRegistrar.java | 6 +- .../StandardResourcesRuntimeHints.java | 4 +- .../CommandAvailabilityInfoModel.java | 11 +- .../standard/commands/CommandInfoModel.java | 17 ++- .../commands/CommandParameterInfoModel.java | 16 +-- .../shell/standard/commands/Completion.java | 12 +- .../shell/standard/commands/Help.java | 19 +-- .../shell/standard/commands/Version.java | 6 +- .../shell/standard/commands/package-info.java | 2 + .../completion/AbstractCompletions.java | 14 ++- .../StandardCompletionModelsRuntimeHints.java | 4 +- .../standard/completion/package-info.java | 4 + .../shell/standard/package-info.java | 3 + 19 files changed, 168 insertions(+), 107 deletions(-) create mode 100644 spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/package-info.java diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 64f419005..3a445a270 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -20,5 +20,5 @@ jobs: cache: 'maven' - name: Build with Maven - run: ./mvnw --no-transfer-progress --batch-mode --update-snapshots verify + run: ./mvnw --no-transfer-progress --batch-mode --update-snapshots verify -Pnullaway 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 e8dd41e59..92176db04 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 @@ -41,12 +41,14 @@ import org.springframework.shell.standard.commands.Stacktrace; import org.springframework.shell.standard.commands.Version; import org.springframework.shell.tui.style.TemplateExecutor; +import org.springframework.util.Assert; /** * Creates beans for standard commands. * * @author Eric Bottard * @author Mahmoud Ben Hassine + * @author Piotr Olaszewski */ @AutoConfiguration @ConditionalOnClass({ Help.Command.class }) @@ -57,7 +59,9 @@ public class StandardCommandsAutoConfiguration { @ConditionalOnMissingBean(Help.Command.class) @ConditionalOnProperty(prefix = "spring.shell.command.help", value = "enabled", havingValue = "true", matchIfMissing = true) public Help help(SpringShellProperties properties, ObjectProvider templateExecutor) { - Help help = new Help(templateExecutor.getIfAvailable()); + TemplateExecutor executor = templateExecutor.getIfAvailable(); + Assert.notNull(executor, "'executor' must not be null"); + Help help = new Help(executor); if (properties.getCommand().getHelp().getGroupingMode() == GroupingMode.FLAT) { help.setShowGroups(false); } @@ -105,7 +109,9 @@ public History historyCommand(org.jline.reader.History jLineHistory) { @ConditionalOnMissingBean(Completion.Command.class) @Conditional(OnCompletionCommandCondition.class) public Completion completion(SpringShellProperties properties) { - return new Completion(properties.getCommand().getCompletion().getRootCommand()); + String rootCommand = properties.getCommand().getCompletion().getRootCommand(); + Assert.hasText(rootCommand, "'rootCommand' must be specified"); + return new Completion(rootCommand); } @Bean @@ -113,7 +119,9 @@ public Completion completion(SpringShellProperties properties) { @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()); + TemplateExecutor executor = templateExecutor.getIfAvailable(); + Assert.notNull(executor, "'executor' must not be null"); + Version version = new Version(executor); VersionCommand versionProperties = properties.getCommand().getVersion(); version.setTemplate(versionProperties.getTemplate()); return version; diff --git a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/StandardCommandsAutoConfigurationTests.java b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/StandardCommandsAutoConfigurationTests.java index b0db85ad8..ae05d371a 100644 --- a/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/StandardCommandsAutoConfigurationTests.java +++ b/spring-shell-autoconfigure/src/test/java/org/springframework/shell/boot/StandardCommandsAutoConfigurationTests.java @@ -31,7 +31,7 @@ class StandardCommandsAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(StandardCommandsAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(StandardCommandsAutoConfiguration.class, ThemingAutoConfiguration.class)); @Test void testCompletionCommand() { diff --git a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java index 9e0dc1545..cac07a3c7 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java @@ -707,7 +707,7 @@ public interface Builder { * @param availability the availability * @return builder for chaining */ - Builder availability(Supplier availability); + Builder availability(@Nullable Supplier availability); /** * Define a group for a command. @@ -1389,7 +1389,7 @@ public Builder hidden(boolean hidden) { } @Override - public Builder availability(Supplier availability) { + public Builder availability(@Nullable Supplier availability) { this.availability = availability; return this; } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/AbstractShellComponent.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/AbstractShellComponent.java index 7fa9f66e9..7453ac7d2 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/AbstractShellComponent.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/AbstractShellComponent.java @@ -37,81 +37,91 @@ * Base class helping to build shell components. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractShellComponent implements ApplicationContextAware, InitializingBean, ResourceLoaderAware { - private ApplicationContext applicationContext; + @SuppressWarnings("NullAway.Init") + private ApplicationContext applicationContext; - private ResourceLoader resourceLoader; + @SuppressWarnings("NullAway.Init") + private ResourceLoader resourceLoader; - private ObjectProvider shellProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider shellProvider; - private ObjectProvider terminalProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider terminalProvider; - private ObjectProvider commandCatalogProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider commandCatalogProvider; - private ObjectProvider completionResolverProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider completionResolverProvider; - private ObjectProvider templateExecutorProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider templateExecutorProvider; - private ObjectProvider themeResolverProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider themeResolverProvider; - private ObjectProvider viewComponentBuilderProvider; + @SuppressWarnings("NullAway.Init") + private ObjectProvider viewComponentBuilderProvider; - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = applicationContext; - } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } + @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } - @Override - public void afterPropertiesSet() throws Exception { - shellProvider = applicationContext.getBeanProvider(Shell.class); - terminalProvider = applicationContext.getBeanProvider(Terminal.class); - commandCatalogProvider = applicationContext.getBeanProvider(CommandCatalog.class); - completionResolverProvider = applicationContext.getBeanProvider(CompletionResolver.class); - templateExecutorProvider = applicationContext.getBeanProvider(TemplateExecutor.class); - themeResolverProvider = applicationContext.getBeanProvider(ThemeResolver.class); - viewComponentBuilderProvider = applicationContext.getBeanProvider(ViewComponentBuilder.class); - } + @Override + public void afterPropertiesSet() throws Exception { + shellProvider = applicationContext.getBeanProvider(Shell.class); + terminalProvider = applicationContext.getBeanProvider(Terminal.class); + commandCatalogProvider = applicationContext.getBeanProvider(CommandCatalog.class); + completionResolverProvider = applicationContext.getBeanProvider(CompletionResolver.class); + templateExecutorProvider = applicationContext.getBeanProvider(TemplateExecutor.class); + themeResolverProvider = applicationContext.getBeanProvider(ThemeResolver.class); + viewComponentBuilderProvider = applicationContext.getBeanProvider(ViewComponentBuilder.class); + } - protected ApplicationContext getApplicationContext() { - return applicationContext; - } + protected ApplicationContext getApplicationContext() { + return applicationContext; + } - protected ResourceLoader getResourceLoader() { - return resourceLoader; - } + protected ResourceLoader getResourceLoader() { + return resourceLoader; + } - protected Shell getShell() { - return shellProvider.getObject(); - } + protected Shell getShell() { + return shellProvider.getObject(); + } - protected Terminal getTerminal() { - return terminalProvider.getObject(); - } + protected Terminal getTerminal() { + return terminalProvider.getObject(); + } - protected CommandCatalog getCommandCatalog() { - return commandCatalogProvider.getObject(); - } + protected CommandCatalog getCommandCatalog() { + return commandCatalogProvider.getObject(); + } - protected Stream getCompletionResolver() { - return completionResolverProvider.orderedStream(); - } + protected Stream getCompletionResolver() { + return completionResolverProvider.orderedStream(); + } - protected TemplateExecutor getTemplateExecutor() { - return templateExecutorProvider.getObject(); - } + protected TemplateExecutor getTemplateExecutor() { + return templateExecutorProvider.getObject(); + } - protected ThemeResolver getThemeResolver() { - return themeResolverProvider.getObject(); - } + protected ThemeResolver getThemeResolver() { + return themeResolverProvider.getObject(); + } - protected ViewComponentBuilder getViewComponentBuilder() { - return viewComponentBuilderProvider.getObject(); - } + protected ViewComponentBuilder getViewComponentBuilder() { + return viewComponentBuilderProvider.getObject(); + } } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java index 3777da7b3..cf2909cb3 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/FileValueProvider.java @@ -27,6 +27,7 @@ import org.springframework.shell.CompletionContext; import org.springframework.shell.CompletionProposal; +import org.springframework.util.StringUtils; import static java.nio.file.FileVisitOption.FOLLOW_LINKS; @@ -36,15 +37,25 @@ * * @author Eric Bottard * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class FileValueProvider implements ValueProvider { @Override public List complete(CompletionContext completionContext) { String input = completionContext.currentWordUpToCursor(); - int lastSlash = input.lastIndexOf(File.separatorChar); - Path dir = lastSlash > -1 ? Paths.get(input.substring(0, lastSlash + 1)) : Paths.get(""); - String prefix = input.substring(lastSlash + 1); + + Path dir = Paths.get(""); + String prefix; + if (StringUtils.hasText(input)) { + int lastSlash = input.lastIndexOf(File.separatorChar); + if (lastSlash > -1) { + dir = Paths.get(input.substring(0, lastSlash + 1)); + } + prefix = input.substring(lastSlash + 1); + } else { + prefix = ""; + } try { return Files diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java index 87c7fc546..5bd6a5d37 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardMethodTargetRegistrar.java @@ -28,6 +28,7 @@ import java.util.stream.Collectors; import org.jline.terminal.Terminal; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +65,7 @@ * @author Florent Biville * @author Camilo Gonzalez * @author Janne Valkealahti + * @author Piotr Olaszewski */ public class StandardMethodTargetRegistrar implements MethodTargetRegistrar { @@ -237,7 +239,7 @@ else if (ClassUtils.isAssignable(Boolean.class, parameterType)) { */ private String getOrInferGroup(Method method) { ShellMethod methodAnn = AnnotationUtils.getAnnotation(method, ShellMethod.class); - if (!methodAnn.group().equals(ShellMethod.INHERITED)) { + if (methodAnn != null && !methodAnn.group().equals(ShellMethod.INHERITED)) { return methodAnn.group(); } Class clazz = method.getDeclaringClass(); @@ -267,7 +269,7 @@ private String getOrInferGroup(Method method) { * selected * */ - private Supplier findAvailabilityIndicator(String[] commandKeys, Object bean, Method method) { + private @Nullable Supplier findAvailabilityIndicator(String[] commandKeys, Object bean, Method method) { ShellMethodAvailability explicit = method.getAnnotation(ShellMethodAvailability.class); final Method indicator; if (explicit != null) { diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardResourcesRuntimeHints.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardResourcesRuntimeHints.java index 2d1a981ef..3bed36303 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardResourcesRuntimeHints.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/StandardResourcesRuntimeHints.java @@ -15,6 +15,7 @@ */ package org.springframework.shell.standard; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -22,11 +23,12 @@ * {@link RuntimeHintsRegistrar} for Shell Standard resources. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class StandardResourcesRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.resources().registerPattern("org/springframework/shell/component/*.stg"); } } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java index 79a949850..f5d5da29a 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandAvailabilityInfoModel.java @@ -15,17 +15,20 @@ */ package org.springframework.shell.standard.commands; +import org.jspecify.annotations.Nullable; + /** * Model encapsulating info about {@code command availability}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class CommandAvailabilityInfoModel { private boolean available; - private String reason; + private @Nullable String reason; - CommandAvailabilityInfoModel(boolean available, String reason) { + CommandAvailabilityInfoModel(boolean available, @Nullable String reason) { this.available = available; this.reason = reason; } @@ -37,7 +40,7 @@ class CommandAvailabilityInfoModel { * @param reason the reason * @return a command parameter availability model */ - static CommandAvailabilityInfoModel of(boolean available, String reason) { + static CommandAvailabilityInfoModel of(boolean available, @Nullable String reason) { return new CommandAvailabilityInfoModel(available, reason); } @@ -45,7 +48,7 @@ public boolean getAvailable() { return available; } - public String getReason() { + public @Nullable String getReason() { return reason; } } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java index fe79a27ce..e0c2b0823 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandInfoModel.java @@ -20,9 +20,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.shell.Availability; import org.springframework.shell.command.CommandOption; import org.springframework.shell.command.CommandRegistration; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -30,17 +32,18 @@ * Model encapsulating info about {@code command}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class CommandInfoModel { private String name; private List aliases; - private String description; + private @Nullable String description; private List parameters; private CommandAvailabilityInfoModel availability; - CommandInfoModel(String name, List aliases, String description, List parameters, - CommandAvailabilityInfoModel availability) { + CommandInfoModel(String name, List aliases, @Nullable String description, List parameters, + CommandAvailabilityInfoModel availability) { this.name = name; this.aliases = aliases; this.description = description; @@ -92,11 +95,13 @@ private static String commandOptionType(CommandOption o) { } else { if (o.getType() != null) { - if (ClassUtils.isAssignable(o.getType().getRawClass(), Void.class)) { + Class rawClass = o.getType().getRawClass(); + Assert.notNull(rawClass, "'rawClass' must not be null"); + if (ClassUtils.isAssignable(rawClass, Void.class)) { return ""; } else { - return ClassUtils.getShortName(o.getType().getRawClass()); + return ClassUtils.getShortName(rawClass); } } else { @@ -117,7 +122,7 @@ public List getAliases() { return this.aliases; } - public String getDescription() { + public @Nullable String getDescription() { return description; } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java index a35470262..d21bfe72e 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/CommandParameterInfoModel.java @@ -17,23 +17,25 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** * Model encapsulating info about {@code command parameter}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class CommandParameterInfoModel { private String type; private List arguments; private boolean required; - private String description; - private String defaultValue; + private @Nullable String description; + private @Nullable String defaultValue; - CommandParameterInfoModel(String type, List arguments, boolean required, String description, - String defaultValue) { + CommandParameterInfoModel(String type, List arguments, boolean required, @Nullable String description, + @Nullable String defaultValue) { this.type = type; this.arguments = arguments; this.required = required; @@ -52,7 +54,7 @@ class CommandParameterInfoModel { * @return a command parameter info model */ static CommandParameterInfoModel of(String type, List arguments, boolean required, - String description, String defaultValue) { + @Nullable String description, @Nullable String defaultValue) { return new CommandParameterInfoModel(type, arguments, required, description, defaultValue); } @@ -68,11 +70,11 @@ public boolean getRequired() { return required; } - public String getDescription() { + public @Nullable String getDescription() { return description; } - public String getDefaultValue() { + public @Nullable String getDefaultValue() { return defaultValue; } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Completion.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Completion.java index 5d52e7beb..88604c26b 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Completion.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Completion.java @@ -15,7 +15,6 @@ */ package org.springframework.shell.standard.commands; -import org.springframework.core.io.ResourceLoader; import org.springframework.shell.standard.AbstractShellComponent; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; @@ -26,6 +25,7 @@ * Command to create a shell completion files, i.e. for {@code bash}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ @ShellComponent public class Completion extends AbstractShellComponent { @@ -36,27 +36,21 @@ public class Completion extends AbstractShellComponent { public interface Command { } - private ResourceLoader resourceLoader; private String rootCommand; public Completion(String rootCommand) { this.rootCommand = rootCommand; } - @Override - public void setResourceLoader(ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } - @ShellMethod(key = "completion bash", value = "Generate bash completion script") public String bash() { - BashCompletions bashCompletions = new BashCompletions(resourceLoader, getCommandCatalog()); + BashCompletions bashCompletions = new BashCompletions(getResourceLoader(), getCommandCatalog()); return bashCompletions.generate(rootCommand); } @ShellMethod(key = "completion zsh", value = "Generate zsh completion script") public String zsh() { - ZshCompletions zshCompletions = new ZshCompletions(resourceLoader, getCommandCatalog()); + ZshCompletions zshCompletions = new ZshCompletions(getResourceLoader(), getCommandCatalog()); return zshCompletions.generate(rootCommand); } } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Help.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Help.java index 42e2ee90a..69181be69 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Help.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Help.java @@ -27,6 +27,7 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; import org.springframework.shell.Utils; import org.springframework.shell.command.CommandRegistration; @@ -36,6 +37,7 @@ import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.standard.ShellOption; import org.springframework.shell.tui.style.TemplateExecutor; +import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; /** @@ -43,6 +45,7 @@ * * @author Eric Bottard * @author Janne Valkealahti + * @author Piotr Olaszewski */ @ShellComponent public class Help extends AbstractShellComponent { @@ -67,8 +70,8 @@ public interface Command { private boolean showGroups = true; private TemplateExecutor templateExecutor; - private String commandTemplate; - private String commandsTemplate; + private @Nullable String commandTemplate; + private @Nullable String commandsTemplate; public Help(TemplateExecutor templateExecutor) { @@ -78,7 +81,7 @@ public Help(TemplateExecutor templateExecutor) { @ShellMethod(value = "Display help about available commands") public AttributedString help( @ShellOption(defaultValue = ShellOption.NULL, valueProvider = CommandValueProvider.class, value = { "-C", - "--command" }, help = "The command to obtain help for.", arity = Integer.MAX_VALUE) String[] command) + "--command" }, help = "The command to obtain help for.", arity = Integer.MAX_VALUE) String @Nullable [] command) throws IOException { if (command == null) { return renderCommands(); @@ -123,12 +126,13 @@ private AttributedString renderCommands() { Map registrations = Utils .removeHiddenCommands(getCommandCatalog().getRegistrations()); - boolean isStg = this.commandTemplate.endsWith(".stg"); + boolean isStg = commandTemplate != null && commandTemplate.endsWith(".stg"); Map model = new HashMap<>(); model.put("model", GroupsInfoModel.of(this.showGroups, registrations)); - String templateResource = resourceAsString(getResourceLoader().getResource(this.commandsTemplate)); + Assert.notNull(commandsTemplate, "'commandsTemplate' must not be null"); + String templateResource = resourceAsString(getResourceLoader().getResource(commandsTemplate)); return isStg ? this.templateExecutor.renderGroup(templateResource, model) : this.templateExecutor.render(templateResource, model); } @@ -141,12 +145,13 @@ private AttributedString renderCommand(String command) { throw new IllegalArgumentException("Unknown command '" + command + "'"); } - boolean isStg = this.commandTemplate.endsWith(".stg"); + boolean isStg = commandTemplate != null && commandTemplate.endsWith(".stg"); Map model = new HashMap<>(); model.put("model", CommandInfoModel.of(command, registration)); - String templateResource = resourceAsString(getResourceLoader().getResource(this.commandTemplate)); + Assert.notNull(commandTemplate, "'commandsTemplate' must not be null"); + String templateResource = resourceAsString(getResourceLoader().getResource(commandTemplate)); return isStg ? this.templateExecutor.renderGroup(templateResource, model) : this.templateExecutor.render(templateResource, model); } diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Version.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Version.java index 89d8a8a9e..ea9213e35 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Version.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/Version.java @@ -25,11 +25,13 @@ import org.jline.utils.AttributedString; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; import org.springframework.shell.standard.AbstractShellComponent; import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.tui.style.TemplateExecutor; +import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; /** @@ -37,6 +39,7 @@ * * @author Janne Valkealahti * @author Mahmoud Ben Hassine + * @author Piotr Olaszewski */ @ShellComponent public class Version extends AbstractShellComponent { @@ -48,7 +51,7 @@ public interface Command { } private TemplateExecutor templateExecutor; - private String template; + private @Nullable String template; public Version(TemplateExecutor templateExecutor) { this.templateExecutor = templateExecutor; @@ -56,6 +59,7 @@ public Version(TemplateExecutor templateExecutor) { @ShellMethod(key = "version", value = "Show version info") public AttributedString version() { + Assert.notNull(template, "'template' must not be null"); String templateResource = resourceAsString(getResourceLoader().getResource(template)); Map attributes = new HashMap<>(); diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/package-info.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/package-info.java index ebf6944a4..8a0df93f2 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/package-info.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/commands/package-info.java @@ -19,7 +19,9 @@ * * @author Eric Bottard */ +@NullMarked @ShellCommandGroup("Built-In Commands") package org.springframework.shell.standard.commands; +import org.jspecify.annotations.NullMarked; import org.springframework.shell.standard.ShellCommandGroup; diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java index a3e03ad78..3df38f537 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/AbstractCompletions.java @@ -30,6 +30,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; +import org.springframework.util.Assert; import org.stringtemplate.v4.ST; import org.stringtemplate.v4.STGroup; import org.stringtemplate.v4.STGroupString; @@ -48,6 +50,7 @@ * resource handling and templating with {@code antrl stringtemplate}. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ public abstract class AbstractCompletions { @@ -151,7 +154,7 @@ interface CommandModelCommand { * Gets a description of a command. * @return command description */ - String getDescription(); + @Nullable String getDescription(); /** * Gets sub-commands known to this command. @@ -246,18 +249,18 @@ class DefaultCommandModelCommand implements CommandModelCommand { private String fullCommand; private String mainCommand; - private String description; + private @Nullable String description; private List commands = new ArrayList<>(); private List options = new ArrayList<>(); - DefaultCommandModelCommand(String fullCommand, String mainCommand, String description) { + DefaultCommandModelCommand(String fullCommand, String mainCommand, @Nullable String description) { this.fullCommand = fullCommand; this.mainCommand = mainCommand; this.description = description; } @Override - public String getDescription() { + public @Nullable String getDescription() { return description; } @@ -385,7 +388,7 @@ class DefaultBuilder implements Builder { private final MultiValueMap defaultAttributes = new LinkedMultiValueMap<>(); private final List> operations = new ArrayList<>(); - private String groupResource; + private @Nullable String groupResource; @Override public Builder attribute(String name, Object value) { @@ -403,6 +406,7 @@ public Builder group(String resource) { public Builder appendGroup(String instance) { // delay so that we render with build Supplier operation = () -> { + Assert.notNull(groupResource, "'groupResource' must not be null"); String template = resourceAsString(resourceLoader.getResource(groupResource)); STGroup group = new STGroupString(template); ST st = group.getInstanceOf(instance); diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/StandardCompletionModelsRuntimeHints.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/StandardCompletionModelsRuntimeHints.java index 28bc9238b..c7a8ade80 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/StandardCompletionModelsRuntimeHints.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/StandardCompletionModelsRuntimeHints.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; @@ -28,11 +29,12 @@ * {@link RuntimeHintsRegistrar} for Shell Standard completion temlate model classes. * * @author Janne Valkealahti + * @author Piotr Olaszewski */ class StandardCompletionModelsRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { ReflectionHints reflection = hints.reflection(); registerForDeclaredMethodsInvocation(reflection, AbstractCompletions.DefaultCommandModel.class, AbstractCompletions.DefaultCommandModelCommand.class, diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/package-info.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/package-info.java new file mode 100644 index 000000000..8245ad0a0 --- /dev/null +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/completion/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell.standard.completion; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-standard/src/main/java/org/springframework/shell/standard/package-info.java b/spring-shell-standard/src/main/java/org/springframework/shell/standard/package-info.java index 1a891cc9a..ea92c3e93 100644 --- a/spring-shell-standard/src/main/java/org/springframework/shell/standard/package-info.java +++ b/spring-shell-standard/src/main/java/org/springframework/shell/standard/package-info.java @@ -19,4 +19,7 @@ * * @author Eric Bottard */ +@NullMarked package org.springframework.shell.standard; + +import org.jspecify.annotations.NullMarked; From d117e6a6c04f79af4871ee14c24978f734ff7b5e Mon Sep 17 00:00:00 2001 From: Piotr Olaszewski Date: Thu, 30 Oct 2025 23:54:20 +0100 Subject: [PATCH 5/5] Add JSpecify to spring-shell-table Signed-off-by: Piotr Olaszewski --- .../org/springframework/shell/package-info.java | 4 ++++ .../shell/table/ArrayTableModel.java | 4 +++- .../shell/table/BeanListTableModel.java | 11 ++++++++--- .../shell/table/BorderSpecification.java | 14 ++++++++++++-- .../shell/table/DefaultFormatter.java | 5 ++++- .../org/springframework/shell/table/Formatter.java | 5 ++++- .../springframework/shell/table/MapFormatter.java | 8 +++++++- .../springframework/shell/table/TableModel.java | 7 +++++-- .../shell/table/TableModelBuilder.java | 4 +++- .../springframework/shell/table/package-info.java | 5 ++++- 10 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 spring-shell-table/src/main/java/org/springframework/shell/package-info.java diff --git a/spring-shell-table/src/main/java/org/springframework/shell/package-info.java b/spring-shell-table/src/main/java/org/springframework/shell/package-info.java new file mode 100644 index 000000000..674913599 --- /dev/null +++ b/spring-shell-table/src/main/java/org/springframework/shell/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.shell; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java b/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java index 16ae2e3de..f919bf607 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/ArrayTableModel.java @@ -16,12 +16,14 @@ package org.springframework.shell.table; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** * A TableModel backed by a row-first array. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class ArrayTableModel extends TableModel { @@ -43,7 +45,7 @@ public int getColumnCount() { return data.length > 0 ? data[0].length : 0; } - public Object getValue(int row, int column) { + public @Nullable Object getValue(int row, int column) { return data[row][column]; } } diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java b/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java index db1d15d2e..9a9713993 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/BeanListTableModel.java @@ -22,9 +22,11 @@ import java.util.LinkedHashMap; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; +import org.springframework.util.Assert; /** * A table model that is backed by a list of beans. @@ -33,6 +35,7 @@ * a convenience constructor for adding a special header row.

* * @author Eric Bottard + * @author Piotr Olaszewski */ public class BeanListTableModel extends TableModel { @@ -40,7 +43,7 @@ public class BeanListTableModel extends TableModel { private final List propertyNames; - private final List headerRow; + private final @Nullable List headerRow; public BeanListTableModel(Class clazz, Iterable list) { this.data = new ArrayList(); @@ -86,14 +89,16 @@ public int getColumnCount() { } @Override - public Object getValue(int row, int column) { + public @Nullable Object getValue(int row, int column) { if (headerRow != null && row == 0) { return headerRow.get(column); } else { int rowToUse = headerRow == null ? row : row - 1; String propertyName = propertyNames.get(column); - return data.get(rowToUse).getPropertyValue(propertyName); + BeanWrapper beanWrapper = data.get(rowToUse); + Assert.notNull(beanWrapper, "'beanWrapper' must not be null"); + return beanWrapper.getPropertyValue(propertyName); } } } diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java b/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java index 19f8d2fec..743b6ddd4 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/BorderSpecification.java @@ -16,6 +16,7 @@ package org.springframework.shell.table; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -27,6 +28,7 @@ * Multiple specifications can be combined on a single table. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class BorderSpecification { @@ -102,14 +104,22 @@ public String toString() { private String matchConstants() { try { for (String field : new String[] {"NONE", "INNER", "FULL", "OUTLINE"}) { - int value = ReflectionUtils.findField(getClass(), field).getInt(null); + Field reflectedField = ReflectionUtils.findField(getClass(), field); + if (reflectedField == null) { + continue; + } + int value = reflectedField.getInt(null); if (match == value) { return field; } } List constants = new ArrayList(); for (String field : new String[] {"TOP", "BOTTOM", "LEFT", "RIGHT", "INNER_HORIZONTAL", "INNER_VERTICAL"}) { - int value = ReflectionUtils.findField(getClass(), field).getInt(null); + Field reflectedField = ReflectionUtils.findField(getClass(), field); + if (reflectedField == null) { + continue; + } + int value = reflectedField.getInt(null); if ((match & value) == value) { constants.add(field); } diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java b/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java index 5fc3f7c87..34daa0813 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/DefaultFormatter.java @@ -16,14 +16,17 @@ package org.springframework.shell.table; +import org.jspecify.annotations.Nullable; + /** * A very simple formatter that uses {@link Object#toString()} and splits on newlines. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class DefaultFormatter implements Formatter { - public String[] format(Object value) { + public String[] format(@Nullable Object value) { return value == null ? new String[] {""} : value.toString().split("\n"); } diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java b/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java index f92d06d36..6d2067840 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/Formatter.java @@ -16,6 +16,8 @@ package org.springframework.shell.table; +import org.jspecify.annotations.Nullable; + /** * A Formatter is responsible for the initial rendering of a value to lines of text. * @@ -24,8 +26,9 @@ * raw text representation (e.g. format numbers).

* * @author Eric Bottard + * @author Piotr Olaszewski */ public interface Formatter { - public String[] format(Object value); + public String[] format(@Nullable Object value); } diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java b/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java index da018feff..447481851 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/MapFormatter.java @@ -16,12 +16,15 @@ package org.springframework.shell.table; +import org.jspecify.annotations.Nullable; + import java.util.Map; /** * A formatter suited for key-value pairs, that renders each mapping on a new line. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class MapFormatter implements Formatter { @@ -32,7 +35,10 @@ public MapFormatter(String separator) { } @Override - public String[] format(Object value) { + public String[] format(@Nullable Object value) { + if (value == null) { + return new String[]{""}; + } Map map = (Map) value; String[] result = new String[map.size()]; int i = 0; diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java index 7128a6a83..c401b5232 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModel.java @@ -16,10 +16,13 @@ package org.springframework.shell.table; +import org.jspecify.annotations.Nullable; + /** * Abstracts away the contract a {@link Table} will use to retrieve tabular data. * * @author Eric Bottard + * @author Piotr Olaszewski */ public abstract class TableModel { @@ -40,7 +43,7 @@ public abstract class TableModel { * @param row the row that is being queried * @param column the column that is being queried */ - public abstract Object getValue(int row, int column); + public abstract @Nullable Object getValue(int row, int column); /** * @return a transposed view of this model, where rows become columns and vice-versa. @@ -58,7 +61,7 @@ public int getColumnCount() { } @Override - public Object getValue(int row, int column) { + public @Nullable Object getValue(int row, int column) { return TableModel.this.getValue(column, row); } }; diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java index 9a7a84a37..52a746280 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/TableModelBuilder.java @@ -19,12 +19,14 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** * Helper class to build a TableModel incrementally. * * @author Eric Bottard + * @author Piotr Olaszewski */ public class TableModelBuilder { @@ -74,7 +76,7 @@ public int getColumnCount() { } @Override - public Object getValue(int row, int column) { + public @Nullable Object getValue(int row, int column) { return rows.get(row).get(column); } }; diff --git a/spring-shell-table/src/main/java/org/springframework/shell/table/package-info.java b/spring-shell-table/src/main/java/org/springframework/shell/table/package-info.java index f9e3ef71e..aa0724f43 100644 --- a/spring-shell-table/src/main/java/org/springframework/shell/table/package-info.java +++ b/spring-shell-table/src/main/java/org/springframework/shell/table/package-info.java @@ -19,4 +19,7 @@ * * @author Eric Bottard */ -package org.springframework.shell.table; \ No newline at end of file +@NullMarked +package org.springframework.shell.table; + +import org.jspecify.annotations.NullMarked;