diff --git a/spring-shell-core/src/main/java/org/springframework/shell/ParameterValidationException.java b/spring-shell-core/src/main/java/org/springframework/shell/ParameterValidationException.java new file mode 100644 index 00000000..16994f18 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/ParameterValidationException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell; + +import javax.validation.ConstraintViolation; +import java.util.Set; + +/** + * Thrown when one or more parameters fail bean validation constraints. + * + * @author Eric Bottard + */ +public class ParameterValidationException extends RuntimeException { + private final Set> constraintViolations; + private final MethodTarget methodTarget; + + public ParameterValidationException(Set> constraintViolations, MethodTarget methodTarget) { + this.constraintViolations = constraintViolations; + this.methodTarget = methodTarget; + } + + public Set> getConstraintViolations() { + return constraintViolations; + } + + public MethodTarget getMethodTarget() { + return methodTarget; + } +} 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 78d68660..5245b8ac 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 @@ -63,6 +63,9 @@ public class Shell implements CommandRegistry { */ protected static final Object UNRESOLVED = new Object(); + private final ExecutableValidator executableValidator = Validation + .buildDefaultValidatorFactory().getValidator().forExecutables(); + public Shell(ResultHandler resultHandler) { this.resultHandler = resultHandler; } @@ -229,13 +232,13 @@ public class Shell implements CommandRegistry { throw new IllegalStateException("Could not resolve " + methodParameter); } } - ExecutableValidator executableValidator = Validation - .buildDefaultValidatorFactory().getValidator().forExecutables(); - Set> constraintViolations = executableValidator.validateParameters(methodTarget.getBean(), + Set> constraintViolations = executableValidator.validateParameters( + methodTarget.getBean(), methodTarget.getMethod(), - args); + args + ); if (constraintViolations.size() > 0) { - System.out.println(constraintViolations); + throw new ParameterValidationException(constraintViolations, methodTarget); } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ParameterValidationExceptionResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ParameterValidationExceptionResultHandler.java new file mode 100644 index 00000000..d913170b --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ParameterValidationExceptionResultHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.shell.result; + +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.shell.ParameterDescription; +import org.springframework.shell.ParameterResolver; +import org.springframework.shell.ParameterValidationException; +import org.springframework.shell.Utils; +import org.springframework.stereotype.Component; + +import javax.validation.ElementKind; +import javax.validation.Path; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Displays validation errors on the terminal. + * + * @author Eric Bottard + */ +@Component +public class ParameterValidationExceptionResultHandler + extends TerminalAwareResultHandler { + + @Autowired + private List parameterResolvers; + + @Override + protected void doHandleResult(ParameterValidationException result) { + terminal.writer().println(new AttributedString("The following constraints were not met:", + AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi()); + result.getConstraintViolations().stream() + .forEach(v -> { + Optional parameterIndex = StreamSupport.stream(v.getPropertyPath().spliterator(), false) + .filter(n -> n.getKind() == ElementKind.PARAMETER) + .map(n -> ((Path.ParameterNode) n).getParameterIndex()) + .findFirst(); + + MethodParameter methodParameter = Utils.createMethodParameter(result.getMethodTarget().getMethod(), + parameterIndex.get()); + List descriptions = findParameterResolver(methodParameter) + .describe(methodParameter).collect(Collectors.toList()); + if (descriptions.size() == 1) { + ParameterDescription description = descriptions.get(0); + AttributedStringBuilder ansi = new AttributedStringBuilder(100); + ansi.append("\t").append(description.keys().get(0), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold()); + ansi.append(" ").append(description.formal(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).underline()); + String msg = String.format(" : %s (You passed '%s')", + v.getMessage(), + String.valueOf(v.getInvalidValue()) + ); + ansi.append(msg, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + + terminal.writer().println(ansi.toAnsi(terminal)); + } + // Several formals for one method param, must be framework like JCommander, etc + else { + // Output toString() for now... + terminal.writer().println(new AttributedString(v.toString(), + AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)).toAnsi(terminal)); + } + + }); + } + + private ParameterResolver findParameterResolver(MethodParameter methodParameter) { + return parameterResolvers.stream().filter(pr -> pr.supports(methodParameter)).findFirst().get(); + } +} diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/Args.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/Args.java index fb9b9af3..c6163c7d 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/Args.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/Args.java @@ -21,6 +21,8 @@ import java.util.List; import com.beust.jcommander.Parameter; +import javax.validation.constraints.Min; + /** * An example straight from the JCommander documentation. * @@ -31,6 +33,7 @@ public class Args { @Parameter private List parameters = new ArrayList<>(); + @Min(3) @Parameter(names = { "-log", "-verbose" }, description = "Level of verbosity") private Integer verbose = 1; diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/JCommanderCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/JCommanderCommands.java index 245f6721..e94d429a 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/JCommanderCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/jcommander/JCommanderCommands.java @@ -20,6 +20,8 @@ import org.springframework.shell.standard.ShellComponent; import org.springframework.shell.standard.ShellMethod; import org.springframework.shell.standard.ShellOption; +import javax.validation.Valid; + /** * A class with JCommander commands. * @@ -29,7 +31,7 @@ import org.springframework.shell.standard.ShellOption; public class JCommanderCommands { @ShellMethod("Bind parameters to JCommander POJO.") - public String jcommander(@ShellOption(optOut = true) Args args) { + public String jcommander(@ShellOption(optOut = true) @Valid Args args) { return "You said " + args; } } diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java index 7cca5594..f4957e0f 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/Commands.java @@ -30,6 +30,8 @@ import org.springframework.shell.standard.ShellOption; import org.springframework.shell.standard.ValueProviderSupport; import org.springframework.stereotype.Component; +import javax.validation.constraints.Size; + /** * Example commands for the Shell 2 Standard resolver. * @@ -44,7 +46,7 @@ public class Commands { } @ShellMethod("It's cool.") - public String foo(String bar) { + public String foo(@Size(min = 2) String bar) { return bar; }