diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ComponentFlowAutoConfiguration.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ComponentFlowAutoConfiguration.java new file mode 100644 index 000000000..de8b5bde7 --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ComponentFlowAutoConfiguration.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.boot; + +import org.jline.terminal.Terminal; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.core.annotation.Order; +import org.springframework.core.io.ResourceLoader; +import org.springframework.shell.component.flow.ComponentFlow; +import org.springframework.shell.component.flow.ComponentFlow.Builder; +import org.springframework.shell.style.TemplateExecutor; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link ComponentFlow}. + * + * @author Janne Valkealahti + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(ComponentFlow.class) +public class ComponentFlowAutoConfiguration { + + @Bean + @Scope("prototype") + @ConditionalOnMissingBean + public ComponentFlow.Builder componentFlowBuilder(ObjectProvider customizerProvider) { + ComponentFlow.Builder builder = ComponentFlow.builder(); + customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder; + } + + @Configuration(proxyBeanMethods = false) + protected static class ComponentFlowConfiguration { + + @Bean + @ConditionalOnMissingBean + @Order(0) + public ComponentFlowCustomizer exchangeStrategiesCustomizer(ObjectProvider terminal, + ObjectProvider resourceLoader, ObjectProvider templateExecutor) { + return new CommonComponentFlowCustomizer(terminal, resourceLoader, templateExecutor); + } + + } + + private static class CommonComponentFlowCustomizer implements ComponentFlowCustomizer { + + private final ObjectProvider terminal; + private final ObjectProvider resourceLoader; + private final ObjectProvider templateExecutor; + + CommonComponentFlowCustomizer(ObjectProvider terminal, ObjectProvider resourceLoader, + ObjectProvider templateExecutor) { + this.terminal = terminal; + this.resourceLoader = resourceLoader; + this.templateExecutor = templateExecutor; + } + + @Override + public void customize(Builder componentFlowBuilder) { + terminal.ifAvailable(dep -> componentFlowBuilder.terminal(dep)); + resourceLoader.ifAvailable(dep -> componentFlowBuilder.resourceLoader(dep)); + templateExecutor.ifAvailable(dep -> componentFlowBuilder.templateExecutor(dep)); + } + } +} diff --git a/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ComponentFlowCustomizer.java b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ComponentFlowCustomizer.java new file mode 100644 index 000000000..533d0bcb5 --- /dev/null +++ b/spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/ComponentFlowCustomizer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.boot; + +import org.springframework.shell.component.flow.ComponentFlow; + +/** + * Callback interface that can be used to customize a {@link ComponentFlow.Builder}. + * + * @author Janne Valkealahti + */ +@FunctionalInterface +public interface ComponentFlowCustomizer { + + /** + * Callback to customize a {@link ComponentFlow.Builder} instance. + * @param componentFlowBuilder the component flow builder to customize + */ + void customize(ComponentFlow.Builder componentFlowBuilder); +} diff --git a/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories index 193d4ae87..c2a321113 100644 --- a/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-shell-autoconfigure/src/main/resources/META-INF/spring.factories @@ -12,4 +12,5 @@ org.springframework.shell.boot.JCommanderParameterResolverAutoConfiguration,\ org.springframework.shell.boot.ParameterResolverAutoConfiguration,\ org.springframework.shell.boot.StandardAPIAutoConfiguration,\ org.springframework.shell.boot.ThemingAutoConfiguration,\ -org.springframework.shell.boot.StandardCommandsAutoConfiguration +org.springframework.shell.boot.StandardCommandsAutoConfiguration,\ +org.springframework.shell.boot.ComponentFlowAutoConfiguration diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java index e0184bb37..6a6440928 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java @@ -70,11 +70,10 @@ public interface ComponentFlow { /** * Gets a new instance of an input wizard builder. * - * @param terminal the terminal * @return the input wizard builder */ - public static Builder builder(Terminal terminal) { - return new DefaultBuilder(terminal); + public static Builder builder() { + return new DefaultBuilder(); } /** @@ -135,6 +134,14 @@ interface Builder { */ MultiItemSelectorSpec withMultiItemSelector(String id); + /** + * Sets a {@link Terminal}. + * + * @param terminal the terminal + * @return a builder + */ + Builder terminal(Terminal terminal); + /** * Sets a {@link ResourceLoader}. * @@ -151,6 +158,20 @@ interface Builder { */ Builder templateExecutor(TemplateExecutor templateExecutor); + /** + * Clones existing builder. + * + * @return a builder + */ + Builder clone(); + + /** + * Resets existing builder. + * + * @return a builder + */ + Builder reset(); + /** * Builds instance of input wizard. * @@ -161,7 +182,6 @@ interface Builder { static abstract class BaseBuilder implements Builder { - private Terminal terminal; private final List stringInputs = new ArrayList<>(); private final List pathInputs = new ArrayList<>(); private final List confirmationInputs = new ArrayList<>(); @@ -169,11 +189,11 @@ 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; - BaseBuilder(Terminal terminal) { - this.terminal = terminal; + BaseBuilder() { } @Override @@ -207,6 +227,12 @@ public MultiItemSelectorSpec withMultiItemSelector(String id) { return new DefaultMultiInputSpec(this, id); } + @Override + public Builder terminal(Terminal terminal) { + this.terminal = terminal; + return this; + } + @Override public Builder resourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; @@ -219,6 +245,23 @@ public Builder templateExecutor(TemplateExecutor templateExecutor) { return this; } + @Override + public Builder clone() { + return new DefaultBuilder(this); + } + + @Override + public Builder reset() { + stringInputs.clear(); + pathInputs.clear(); + confirmationInputs.clear(); + singleItemSelectors.clear(); + multiItemSelectors.clear(); + order.set(0); + uniqueIds.clear(); + return this; + } + void addStringInput(BaseStringInput input) { checkUniqueId(input.getId()); input.setOrder(order.getAndIncrement()); @@ -249,6 +292,10 @@ void addMultiItemSelector(BaseMultiItemSelector input) { multiItemSelectors.add(input); } + Terminal getTerminal() { + return terminal; + } + ResourceLoader getResourceLoader() { return resourceLoader; } @@ -267,8 +314,14 @@ private void checkUniqueId(String id) { static class DefaultBuilder extends BaseBuilder { - DefaultBuilder(Terminal terminal) { - super(terminal); + DefaultBuilder() { + super(); + } + + DefaultBuilder(BaseBuilder other) { + terminal(other.getTerminal()); + resourceLoader(other.getResourceLoader()); + templateExecutor(other.getTemplateExecutor()); } } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java index 0f06b96eb..316971585 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java @@ -41,7 +41,8 @@ public void testSimpleFlow() throws InterruptedException { single1SelectItems.put("key2", "value2"); List multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"), SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3")); - ComponentFlow wizard = ComponentFlow.builder(getTerminal()) + ComponentFlow wizard = ComponentFlow.builder() + .terminal(getTerminal()) .resourceLoader(getResourceLoader()) .templateExecutor(getTemplateExecutor()) .withStringInput("field1") @@ -107,7 +108,8 @@ public void testSimpleFlow() throws InterruptedException { @Test public void testSkipsGivenComponents() throws InterruptedException { - ComponentFlow wizard = ComponentFlow.builder(getTerminal()) + ComponentFlow wizard = ComponentFlow.builder() + .terminal(getTerminal()) .withStringInput("id1") .name("name") .resultValue("value1") @@ -154,7 +156,8 @@ public void testSkipsGivenComponents() throws InterruptedException { @Test public void testChoosesDynamically() throws InterruptedException { - ComponentFlow wizard = ComponentFlow.builder(getTerminal()) + ComponentFlow wizard = ComponentFlow.builder() + .terminal(getTerminal()) .resourceLoader(getResourceLoader()) .templateExecutor(getTemplateExecutor()) .withStringInput("id1") diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java index bce8f6b8f..e3cbddc0e 100644 --- a/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.shell.component.flow.ComponentFlow; import org.springframework.shell.component.flow.SelectItem; import org.springframework.shell.standard.AbstractShellComponent; @@ -29,6 +30,9 @@ @ShellComponent public class ComponentFlowCommands extends AbstractShellComponent { + @Autowired + private ComponentFlow.Builder componentFlowBuilder; + @ShellMethod(key = "flow showcase", value = "Showcase", group = "Flow") public void showcase() { Map single1SelectItems = new HashMap<>(); @@ -36,9 +40,7 @@ public void showcase() { single1SelectItems.put("key2", "value2"); List multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"), SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3")); - ComponentFlow flow = ComponentFlow.builder(getTerminal()) - .resourceLoader(getResourceLoader()) - .templateExecutor(getTemplateExecutor()) + ComponentFlow flow = componentFlowBuilder.clone().reset() .withStringInput("field1") .name("Field1") .defaultValue("defaultField1Value") @@ -69,9 +71,7 @@ public void conditional() { Map single1SelectItems = new HashMap<>(); single1SelectItems.put("Field1", "field1"); single1SelectItems.put("Field2", "field2"); - ComponentFlow flow = ComponentFlow.builder(getTerminal()) - .resourceLoader(getResourceLoader()) - .templateExecutor(getTemplateExecutor()) + ComponentFlow flow = componentFlowBuilder.clone().reset() .withSingleItemSelector("single1") .name("Single1") .selectItems(single1SelectItems)