diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellAutoConfiguration.java index 711638f1..5b3ee8cb 100644 --- a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellAutoConfiguration.java +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/SpringShellAutoConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.shell.boot; import java.util.Collection; +import java.util.Set; import javax.validation.Validation; import javax.validation.Validator; @@ -33,8 +34,9 @@ import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.shell.ResultHandler; +import org.springframework.shell.ResultHandlerService; import org.springframework.shell.Shell; -import org.springframework.shell.result.IterableResultHandler; +import org.springframework.shell.result.GenericResultHandlerService; import org.springframework.shell.result.ResultHandlerConfig; /** @@ -71,8 +73,16 @@ public class SpringShellAutoConfiguration { } @Bean - public Shell shell(@Qualifier("main") ResultHandler resultHandler, @Qualifier("iterableResultHandler") IterableResultHandler iterableResultHandler) { - iterableResultHandler.setDelegate(resultHandler); - return new Shell(resultHandler); + public ResultHandlerService resultHandlerService(Set> resultHandlers) { + GenericResultHandlerService service = new GenericResultHandlerService(); + for (ResultHandler resultHandler : resultHandlers) { + service.addResultHandler(resultHandler); + } + return service; + } + + @Bean + public Shell shell(ResultHandlerService resultHandlerService) { + return new Shell(resultHandlerService); } } 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 new file mode 100644 index 00000000..733559b3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/ResultHandlerService.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +/** + * A service interface for result handling. + * + * @author Janne Valkealahti + */ +public interface ResultHandlerService { + + /** + * Handle result. + * + * @param result the result + */ + void handle(@Nullable Object result); + + /** + * Handle result to the specified {@ resultType}. + * + * @param result the result + * @param resultType the result type + */ + void handle(@Nullable Object result, @Nullable TypeDescriptor resultType); +} 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 50505c11..fe4b94ad 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 @@ -55,10 +55,11 @@ import org.springframework.util.ReflectionUtils; *

* * @author Eric Bottard + * @author Janne Valkealahti */ public class Shell { - private final ResultHandler resultHandler; + private final ResultHandlerService resultHandlerService; /** * Marker object returned to signify that there was no input to turn into a command @@ -84,8 +85,8 @@ public class Shell { */ protected static final Object UNRESOLVED = new Object(); - public Shell(ResultHandler resultHandler) { - this.resultHandler = resultHandler; + public Shell(ResultHandlerService resultHandlerService) { + this.resultHandlerService = resultHandlerService; } @Autowired(required = false) @@ -125,7 +126,7 @@ public class Shell { if (e instanceof ExitRequest) { // Handles ExitRequest thrown from hitting CTRL-C break; } - resultHandler.handleResult(e); + resultHandlerService.handle(e); continue; } if (input == null) { @@ -134,7 +135,7 @@ public class Shell { result = evaluate(input); if (result != NO_INPUT && !(result instanceof ExitRequest)) { - resultHandler.handleResult(result); + resultHandlerService.handle(result); } } } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandler.java new file mode 100644 index 00000000..c679f4b4 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandler.java @@ -0,0 +1,14 @@ +package org.springframework.shell.result; + +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; + +public interface GenericResultHandler { + + Set> getHandlerTypes(); + + void handle(Object result, TypeDescriptor resultType); + + boolean matches(TypeDescriptor resultType); +} 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 new file mode 100644 index 00000000..783a776c --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/GenericResultHandlerService.java @@ -0,0 +1,284 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.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.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; + +/** + * Base {@ResultHandlerService} implementation suitable for use in most + * environments. + * + * @author Janne Valkealahti + */ +public class GenericResultHandlerService implements ResultHandlerService { + + private final ResultHandlers resultHandlers = new ResultHandlers(); + + @Override + public void handle(Object source) { + handle(source, TypeDescriptor.forObject(source)); + } + + @Override + public void handle(Object result, TypeDescriptor resultType) { + if (result == null) { + return; + } + GenericResultHandler handler = getResultHandler(resultType); + if (handler != null) { + invokeHandler(handler, result, resultType); + return; + } + handleResultHandlerNotFound(result, resultType); + } + + /** + * Add a plain result handler to this registry. + * + * @param resultHandler the result handler + */ + public void addResultHandler(ResultHandler resultHandler) { + ResolvableType[] typeInfo = getRequiredTypeInfo(resultHandler.getClass(), ResultHandler.class); + if (typeInfo == null) { + throw new IllegalArgumentException("Unable to determine result type for your " + + "ResultHandler [" + resultHandler.getClass().getName() + "]; does the class parameterize those types?"); + } + addResultHandler(new ResultHandlerAdapter(resultHandler, typeInfo[0])); + } + + /** + * Add a plain result handler to this registry. + * + * @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) { + addResultHandler(new ResultHandlerAdapter(resultHandler, ResolvableType.forClass(resultType))); + } + + /** + * Add a generic result handler this this registry. + * + * @param handler the generic result handler + */ + public void addResultHandler(GenericResultHandler handler) { + this.resultHandlers.add(handler); + } + + private GenericResultHandler getResultHandler(TypeDescriptor resultType) { + return this.resultHandlers.find(resultType); + } + + @Nullable + private Object handleResultHandlerNotFound( + @Nullable Object source, @Nullable TypeDescriptor sourceType) { + if (source == null) { + return null; + } + if (sourceType == null) { + return source; + } + throw new ResultHandlerNotFoundException(sourceType); + } + + @Nullable + private ResolvableType[] getRequiredTypeInfo(Class handlerClass, Class genericIfc) { + ResolvableType resolvableType = ResolvableType.forClass(handlerClass).as(genericIfc); + ResolvableType[] generics = resolvableType.getGenerics(); + if (generics.length < 1) { + return null; + } + Class resultType = generics[0].resolve(); + if (resultType == null) { + return null; + } + return generics; + } + + @SuppressWarnings("unchecked") + private final static class ResultHandlerAdapter implements GenericResultHandler { + + ResultHandler handler; + Class result; + + public ResultHandlerAdapter(ResultHandler handler, ResolvableType resultType) { + this.handler = (ResultHandler) handler; + this.result = resultType.toClass(); + } + + @Override + public Set> getHandlerTypes() { + return Collections.singleton(this.result); + } + + @Override + public void handle(Object result, TypeDescriptor resultType) { + this.handler.handleResult(result); + } + + @Override + public boolean matches(TypeDescriptor resultType) { + // always true until we create conditional handlers + return true; + } + } + + /** + * Manages handlers registered with a specific {@link Class}. + */ + private static class ResultHandlersForType { + + private final Deque handlers = new ConcurrentLinkedDeque<>(); + + public void add(GenericResultHandler handler) { + this.handlers.addFirst(handler); + } + + @Nullable + public GenericResultHandler getHandler(TypeDescriptor resultType) { + for (GenericResultHandler handler : this.handlers) { + if (handler.matches(resultType)) { + return handler; + } + } + return null; + } + + @Override + public String toString() { + return StringUtils.collectionToCommaDelimitedString(this.handlers); + } + } + + private static class ResultHandlers { + + private final Set globalHandlers = new CopyOnWriteArraySet<>(); + private final Map, ResultHandlersForType> handlers = new ConcurrentHashMap<>(16); + + public void add(GenericResultHandler handler) { + Set> handlerTypes = handler.getHandlerTypes(); + if (handlerTypes == null) { + this.globalHandlers.add(handler); + } + else { + for (Class handlerType : handlerTypes) { + getMatchableConverters(handlerType).add(handler); + } + } + } + + private ResultHandlersForType getMatchableConverters(Class handlerType) { + return this.handlers.computeIfAbsent(handlerType, k -> new ResultHandlersForType()); + } + + public GenericResultHandler find(TypeDescriptor resultType) { + List> resultCandidates = getClassHierarchy(resultType.getType()); + for (Class resultCandidate : resultCandidates) { + GenericResultHandler handler = getRegisteredHandler(resultType, resultCandidate); + if (handler != null) { + return handler; + } + } + return null; + } + + @Nullable + private GenericResultHandler getRegisteredHandler(TypeDescriptor resultType, Class handlerType) { + ResultHandlersForType resultHandlersForType = this.handlers.get(handlerType); + if (resultHandlersForType != null) { + GenericResultHandler handler = resultHandlersForType.getHandler(resultType); + if (handler != null) { + return handler; + } + } + for (GenericResultHandler globalHandler : this.globalHandlers) { + if (globalHandler.matches(resultType)) { + return globalHandler; + } + } + return null; + } + + private List> getClassHierarchy(Class type) { + List> hierarchy = new ArrayList<>(20); + Set> visited = new HashSet<>(20); + addToClassHierarchy(0, ClassUtils.resolvePrimitiveIfNecessary(type), false, hierarchy, visited); + boolean array = type.isArray(); + + int i = 0; + while (i < hierarchy.size()) { + Class candidate = hierarchy.get(i); + candidate = (array ? candidate.getComponentType() : ClassUtils.resolvePrimitiveIfNecessary(candidate)); + Class superclass = candidate.getSuperclass(); + if (superclass != null && superclass != Object.class && superclass != Enum.class) { + addToClassHierarchy(i + 1, candidate.getSuperclass(), array, hierarchy, visited); + } + addInterfacesToClassHierarchy(candidate, array, hierarchy, visited); + i++; + } + + if (Enum.class.isAssignableFrom(type)) { + addToClassHierarchy(hierarchy.size(), Enum.class, array, hierarchy, visited); + addToClassHierarchy(hierarchy.size(), Enum.class, false, hierarchy, visited); + addInterfacesToClassHierarchy(Enum.class, array, hierarchy, visited); + } + + addToClassHierarchy(hierarchy.size(), Object.class, array, hierarchy, visited); + addToClassHierarchy(hierarchy.size(), Object.class, false, hierarchy, visited); + return hierarchy; + } + + private void addInterfacesToClassHierarchy(Class type, boolean asArray, + 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) { + if (asArray) { + type = Array.newInstance(type, 0).getClass(); + } + if (visited.add(type)) { + hierarchy.add(index, type); + } + } + } + + private static void invokeHandler(GenericResultHandler handler, Object result, TypeDescriptor resultType) { + handler.handle(result, resultType);; + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java deleted file mode 100644 index fbf8db3d..00000000 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/IterableResultHandler.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell.result; - -import org.springframework.shell.ResultHandler; - -/** - * A {@link ResultHandler} that deals with {@link Iterable}s and delegates to - * {@link TypeHierarchyResultHandler} for each element in turn. - * - * @author Eric Bottard - */ -public class IterableResultHandler implements ResultHandler { - - private ResultHandler delegate; - - // Setter injection to avoid circular dependency at creation time - public void setDelegate(ResultHandler delegate) { - this.delegate = delegate; - } - - @Override - @SuppressWarnings("unchecked") - public void handleResult(Iterable result) { - for (Object o : result) { - delegate.handleResult(o); - } - } -} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java index 978e09b3..dcf77c37 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-2021 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. @@ -16,33 +16,20 @@ package org.springframework.shell.result; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.shell.ResultHandler; import org.springframework.shell.TerminalSizeAware; /** * Used for explicit configuration of {@link org.springframework.shell.ResultHandler}s. * * @author Eric Bottard + * @author Janne Valkealahti */ @Configuration public class ResultHandlerConfig { - @Bean - @Qualifier("main") - public ResultHandler mainResultHandler() { - return new TypeHierarchyResultHandler(); - } - - @Bean - @Qualifier("iterableResultHandler") - public IterableResultHandler iterableResultHandler() { - return new IterableResultHandler(); - } - @Bean @ConditionalOnClass(TerminalSizeAware.class) public TerminalSizeAwareResultHandler terminalSizeAwareResultHandler() { 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 new file mode 100644 index 00000000..c4df57ba --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlerNotFoundException.java @@ -0,0 +1,44 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.result; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.lang.Nullable; + +public class ResultHandlerNotFoundException extends ResultHandlingException { + + @Nullable + private final TypeDescriptor resultType; + + /** + * Create a new handling executor not found exception. + * + * @param resultType the result type requested to handle from + */ + public ResultHandlerNotFoundException(@Nullable TypeDescriptor resultType) { + super("No handler found capable of handling from type [" + resultType + "]"); + this.resultType = resultType; + } + + + /** + * Return the source type that was requested to convert from. + */ + @Nullable + public TypeDescriptor getResultType() { + return this.resultType; + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlingException.java b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlingException.java new file mode 100644 index 00000000..98b16ba3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/result/ResultHandlingException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.result; + +import org.springframework.core.NestedRuntimeException; + +public class ResultHandlingException extends NestedRuntimeException { + + /** + * Construct a new result handling exception. + * + * @param message the exception message + */ + public ResultHandlingException(String message) { + super(message); + } + + /** + * Construct a new result handling exception. + * + * @param message the exception message + * @param cause the cause + */ + public ResultHandlingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/result/TypeHierarchyResultHandler.java b/spring-shell-core/src/main/java/org/springframework/shell/result/TypeHierarchyResultHandler.java deleted file mode 100644 index 1c4a1b48..00000000 --- a/spring-shell-core/src/main/java/org/springframework/shell/result/TypeHierarchyResultHandler.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.shell.result; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.ResolvableType; -import org.springframework.shell.ResultHandler; - -/** - * A delegating {@link ResultHandler} that dispatches handling based on the type of the result. - *

- * If no direct match is found, the type hierarchy of the result is considered, including implemented interfaces. - * Auto-populates the handler map based on Generics type declaration of each discovered {@link ResultHandler} in the - * ApplicationContext. - *

- * - * @author Eric Bottard - */ -public class TypeHierarchyResultHandler implements ResultHandler { - - private Map, ResultHandler> resultHandlers = new HashMap<>(); - - @Override - @SuppressWarnings("unchecked") - public void handleResult(Object result) { - if (result == null) { // void methods - return; - } - Class clazz = result.getClass(); - ResultHandler handler = getResultHandler(clazz); - handler.handleResult(result); - } - - private ResultHandler getResultHandler(Class clazz) { - ResultHandler handler = resultHandlers.get(clazz); - if (handler != null) { - return handler; - } - else { - for (Class type : clazz.getInterfaces()) { - handler = getResultHandler(type); - if (handler != null) { - return handler; - } - } - return clazz.getSuperclass() != null ? getResultHandler(clazz.getSuperclass()) : null; - } - } - - @Autowired - public void setResultHandlers(Set> resultHandlers) { - for (ResultHandler resultHandler : resultHandlers) { - ResolvableType type = ResolvableType.forInstance(resultHandler).as(ResultHandler.class); - registerHandler(type.resolveGeneric(0), resultHandler); - } - } - - private void registerHandler(Class type, ResultHandler resultHandler) { - ResultHandler previous = this.resultHandlers.put(type, resultHandler); - if (previous != null) { - throw new IllegalArgumentException(String.format("Multiple ResultHandlers configured for %s: both %s and %s", type, previous, resultHandler)); - } - } - -} diff --git a/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java b/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java index 92ef6749..719030a6 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/ShellTest.java @@ -46,17 +46,13 @@ import static org.mockito.Mockito.when; * @author Eric Bottard */ @ExtendWith(MockitoExtension.class) -// @RunWith(JUnitPlatform.class) public class ShellTest { - // @Rule - // public MockitoRule mockitoRule = MockitoJUnit.rule(); - @Mock private InputProvider inputProvider; @Mock - private ResultHandler resultHandler; + ResultHandlerService resultHandlerService; @Mock private ParameterResolver parameterResolver; @@ -79,7 +75,7 @@ public class ShellTest { when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?", null); valueResult = new ValueResult(null, "test"); when(parameterResolver.resolve(any(), any())).thenReturn(valueResult); - doThrow(new Exit()).when(resultHandler).handleResult(any()); + doThrow(new Exit()).when(resultHandlerService).handle(any()); shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))); @@ -97,7 +93,7 @@ public class ShellTest { @Test public void commandNotFound() throws IOException { when(inputProvider.readInput()).thenReturn(() -> "hello world how are you doing ?", null); - doThrow(new Exit()).when(resultHandler).handleResult(isA(CommandNotFound.class)); + doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class)); shell.methodTargets = Collections.singletonMap("bonjour", MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))); @@ -114,7 +110,7 @@ public class ShellTest { // See https://github.com/spring-projects/spring-shell/issues/142 public void commandNotFoundPrefix() throws IOException { when(inputProvider.readInput()).thenReturn(() -> "helloworld how are you doing ?", null); - doThrow(new Exit()).when(resultHandler).handleResult(isA(CommandNotFound.class)); + doThrow(new Exit()).when(resultHandlerService).handle(isA(CommandNotFound.class)); shell.methodTargets = Collections.singletonMap("hello", MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))); @@ -133,7 +129,7 @@ public class ShellTest { when(inputProvider.readInput()).thenReturn(() -> "", () -> "hello world how are you doing ?", null); valueResult = new ValueResult(null, "test"); when(parameterResolver.resolve(any(), any())).thenReturn(valueResult); - doThrow(new Exit()).when(resultHandler).handleResult(any()); + doThrow(new Exit()).when(resultHandlerService).handle(any()); shell.methodTargets = Collections.singletonMap("hello world", MethodTarget.of("helloWorld", this, new Command.Help("Say hello"))); @@ -151,7 +147,7 @@ public class ShellTest { @Test public void commandThrowingAnException() throws IOException { when(inputProvider.readInput()).thenReturn(() -> "fail", null); - doThrow(new Exit()).when(resultHandler).handleResult(isA(SomeException.class)); + doThrow(new Exit()).when(resultHandlerService).handle(isA(SomeException.class)); shell.methodTargets = Collections.singletonMap("fail", MethodTarget.of("failing", this, new Command.Help("Will throw an exception"))); diff --git a/spring-shell-core/src/test/java/org/springframework/shell/jline/GenericResultHandlerServiceTests.java b/spring-shell-core/src/test/java/org/springframework/shell/jline/GenericResultHandlerServiceTests.java new file mode 100644 index 00000000..389d9561 --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/jline/GenericResultHandlerServiceTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.jline; + +import org.junit.jupiter.api.Test; + +import org.springframework.shell.ResultHandler; +import org.springframework.shell.result.GenericResultHandlerService; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GenericResultHandlerServiceTests { + + @Test + public void testSimpleHandling() { + StringResultHandler stringResultHandler = new StringResultHandler(); + IntegerResultHandler integerResultHandler = new IntegerResultHandler(); + GenericResultHandlerService resultHandlerService = new GenericResultHandlerService(); + resultHandlerService.addResultHandler(stringResultHandler); + resultHandlerService.addResultHandler(integerResultHandler); + resultHandlerService.handle("string"); + assertThat(stringResultHandler.result).isEqualTo("string"); + assertThat(integerResultHandler.result).isNull();; + resultHandlerService.handle(0); + assertThat(integerResultHandler.result).isEqualTo(0); + } + + @Test + public void testObjectHandling() { + ObjectResultHandler resultHandler = new ObjectResultHandler(); + GenericResultHandlerService resultHandlerService = new GenericResultHandlerService(); + resultHandlerService.addResultHandler(resultHandler); + resultHandlerService.handle("string"); + assertThat(resultHandler.result).isEqualTo("string"); + } + + private static class StringResultHandler implements ResultHandler { + + String result; + + @Override + public void handleResult(String result) { + this.result = result; + } + } + + private static class IntegerResultHandler implements ResultHandler { + + Integer result; + + @Override + public void handleResult(Integer result) { + this.result = result; + } + } + + private static class ObjectResultHandler implements ResultHandler { + + Object result; + + @Override + public void handleResult(Object result) { + this.result = result; + } + } +}