Flow system for UI components

- Implement basic flow system with builder patterns
  to easily use components and hook those together
  as a flow.
- Fixes #364
This commit is contained in:
Janne Valkealahti
2022-03-03 15:34:55 +00:00
parent e5151bb3a6
commit c532dc227e
30 changed files with 3017 additions and 6 deletions

View File

@@ -63,7 +63,7 @@ public class ConfirmationInput extends AbstractTextComponent<Boolean, Confirmati
}
@Override
protected ConfirmationInputContext getThisContext(ComponentContext<?> context) {
public ConfirmationInputContext getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}

View File

@@ -52,7 +52,7 @@ public class MultiItemSelector<T, I extends Nameable & Matchable & Enableable &
}
@Override
protected MultiItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
public MultiItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}

View File

@@ -60,7 +60,7 @@ public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
}
@Override
protected PathInputContext getThisContext(ComponentContext<?> context) {
public PathInputContext getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}

View File

@@ -52,7 +52,7 @@ public class SingleItemSelector<T, I extends Nameable & Matchable & Enableable &
}
@Override
protected SingleItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
public SingleItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}

View File

@@ -68,7 +68,7 @@ public class StringInput extends AbstractTextComponent<String, StringInputContex
}
@Override
protected StringInputContext getThisContext(ComponentContext<?> context) {
public StringInputContext getThisContext(ComponentContext<?> context) {
if (context != null && currentContext == context) {
return currentContext;
}

View File

@@ -0,0 +1,139 @@
/*
* 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.component.flow;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
/**
* Base impl for {@link ConfirmationInputSpec}.
*
* @author Janne Valkealahti
*/
public abstract class BaseConfirmationInput extends BaseInput<ConfirmationInputSpec> implements ConfirmationInputSpec {
private String name;
private Boolean defaultValue;
private Function<ConfirmationInputContext, List<AttributedString>> renderer;
private List<Consumer<ConfirmationInputContext>> preHandlers = new ArrayList<>();
private List<Consumer<ConfirmationInputContext>> postHandlers = new ArrayList<>();
private boolean storeResult = true;
private String templateLocation;
private Function<ConfirmationInputContext, String> next;
public BaseConfirmationInput(BaseBuilder builder, String id) {
super(builder, id);
}
@Override
public ConfirmationInputSpec name(String name) {
this.name = name;
return this;
}
@Override
public ConfirmationInputSpec defaultValue(Boolean defaultValue) {
this.defaultValue = defaultValue;
return this;
}
@Override
public ConfirmationInputSpec renderer(Function<ConfirmationInputContext, List<AttributedString>> renderer) {
this.renderer = renderer;
return this;
}
@Override
public ConfirmationInputSpec template(String location) {
this.templateLocation = location;
return this;
}
@Override
public ConfirmationInputSpec preHandler(Consumer<ConfirmationInputContext> handler) {
this.preHandlers.add(handler);
return this;
}
@Override
public ConfirmationInputSpec postHandler(Consumer<ConfirmationInputContext> handler) {
this.postHandlers.add(handler);
return this;
}
@Override
public ConfirmationInputSpec storeResult(boolean store) {
this.storeResult = store;
return this;
}
@Override
public ConfirmationInputSpec next(Function<ConfirmationInputContext, String> next) {
this.next = next;
return this;
}
@Override
public Builder and() {
getBuilder().addConfirmationInput(this);
return getBuilder();
}
@Override
public ConfirmationInputSpec getThis() {
return this;
}
public String getName() {
return name;
}
public boolean getDefaultValue() {
return defaultValue != null ? defaultValue : true;
}
public Function<ConfirmationInputContext, List<AttributedString>> getRenderer() {
return renderer;
}
public String getTemplateLocation() {
return templateLocation;
}
public List<Consumer<ConfirmationInputContext>> getPreHandlers() {
return preHandlers;
}
public List<Consumer<ConfirmationInputContext>> getPostHandlers() {
return postHandlers;
}
public boolean isStoreResult() {
return storeResult;
}
public Function<ConfirmationInputContext, String> getNext() {
return next;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.component.flow;
import org.springframework.core.Ordered;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
/**
* Base impl for specs.
*
* @author Janne Valkealahti
*/
public abstract class BaseInput<T extends BaseInputSpec<T>> implements Ordered, BaseInputSpec<T> {
private final BaseBuilder builder;
private final String id;
private int order;
BaseInput(BaseBuilder builder, String id) {
this.builder = builder;
this.id = id;
}
@Override
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public T order(int order) {
this.order = order;
return getThis();
}
public BaseBuilder getBuilder() {
return builder;
}
public String getId() {
return id;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.component.flow;
/**
* Base spec for other specs.
*
* @author Janne Valkealahti
*/
public interface BaseInputSpec<T extends BaseInputSpec<T>> {
/**
* Sets order of this component.
*
* @param order the order
* @return a builder
*/
T order(int order);
/**
* Usual this trick to get typed child.
*
* @return a builder
*/
T getThis();
}

View File

@@ -0,0 +1,186 @@
/*
* 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.component.flow;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
import org.springframework.shell.component.support.SelectorItem;
/**
* Base impl for {@link MultiItemSelectorSpec}.
*
* @author Janne Valkealahti
*/
public abstract class BaseMultiItemSelector extends BaseInput<MultiItemSelectorSpec> implements MultiItemSelectorSpec {
private String name;
private List<String> resultValues = new ArrayList<>();
private ResultMode resultMode;
private List<SelectItem> selectItems = new ArrayList<>();
private Comparator<SelectorItem<String>> comparator;
private Function<MultiItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer;
private Integer maxItems;
private List<Consumer<MultiItemSelectorContext<String, SelectorItem<String>>>> preHandlers = new ArrayList<>();
private List<Consumer<MultiItemSelectorContext<String, SelectorItem<String>>>> postHandlers = new ArrayList<>();
private boolean storeResult = true;
private String templateLocation;
private Function<MultiItemSelectorContext<String, SelectorItem<String>>, String> next;
public BaseMultiItemSelector(BaseBuilder builder, String id) {
super(builder, id);
}
@Override
public MultiItemSelectorSpec name(String name) {
this.name = name;
return this;
}
@Override
public MultiItemSelectorSpec resultValues(List<String> resultValues) {
this.resultValues.addAll(resultValues);
return this;
}
@Override
public MultiItemSelectorSpec resultMode(ResultMode resultMode) {
this.resultMode = resultMode;
return this;
}
@Override
public MultiItemSelectorSpec selectItems(List<SelectItem> selectItems) {
this.selectItems = selectItems;
return this;
}
@Override
public MultiItemSelectorSpec sort(Comparator<SelectorItem<String>> comparator) {
this.comparator = comparator;
return this;
}
@Override
public MultiItemSelectorSpec renderer(Function<MultiItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer) {
this.renderer = renderer;
return this;
}
@Override
public MultiItemSelectorSpec template(String location) {
this.templateLocation = location;
return this;
}
@Override
public MultiItemSelectorSpec max(int max) {
this.maxItems = max;
return this;
}
@Override
public MultiItemSelectorSpec preHandler(Consumer<MultiItemSelectorContext<String, SelectorItem<String>>> handler) {
this.preHandlers.add(handler);
return this;
}
@Override
public MultiItemSelectorSpec postHandler(Consumer<MultiItemSelectorContext<String, SelectorItem<String>>> handler) {
this.postHandlers.add(handler);
return this;
}
@Override
public MultiItemSelectorSpec storeResult(boolean store) {
this.storeResult = store;
return this;
}
@Override
public MultiItemSelectorSpec next(
Function<MultiItemSelectorContext<String, SelectorItem<String>>, String> next) {
this.next = next;
return this;
}
@Override
public Builder and() {
getBuilder().addMultiItemSelector(this);
return getBuilder();
}
@Override
public MultiItemSelectorSpec getThis() {
return this;
}
public String getName() {
return name;
}
public List<String> getResultValues() {
return resultValues;
}
public ResultMode getResultMode() {
return resultMode;
}
public List<SelectItem> getSelectItems() {
return selectItems;
}
public Comparator<SelectorItem<String>> getComparator() {
return comparator;
}
public Function<MultiItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> getRenderer() {
return renderer;
}
public String getTemplateLocation() {
return templateLocation;
}
public Integer getMaxItems() {
return maxItems;
}
public List<Consumer<MultiItemSelectorContext<String, SelectorItem<String>>>> getPreHandlers() {
return preHandlers;
}
public List<Consumer<MultiItemSelectorContext<String, SelectorItem<String>>>> getPostHandlers() {
return postHandlers;
}
public boolean isStoreResult() {
return storeResult;
}
public Function<MultiItemSelectorContext<String, SelectorItem<String>>, String> getNext() {
return next;
}
}

View File

@@ -0,0 +1,161 @@
/*
* 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.component.flow;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.PathInput.PathInputContext;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
/**
* Base impl for {@link PathInputSpec}.
*
* @author Janne Valkealahti
*/
public abstract class BasePathInput extends BaseInput<PathInputSpec> implements PathInputSpec {
private String name;
private String resultValue;
private ResultMode resultMode;
private String defaultValue;
private Function<PathInputContext, List<AttributedString>> renderer;
private List<Consumer<PathInputContext>> preHandlers = new ArrayList<>();
private List<Consumer<PathInputContext>> postHandlers = new ArrayList<>();
private boolean storeResult = true;
private String templateLocation;
private Function<PathInputContext, String> next;
public BasePathInput(BaseBuilder builder, String id) {
super(builder, id);
}
@Override
public PathInputSpec name(String name) {
this.name = name;
return this;
}
@Override
public PathInputSpec resultValue(String resultValue) {
this.resultValue = resultValue;
return this;
}
@Override
public PathInputSpec resultMode(ResultMode resultMode) {
this.resultMode = resultMode;
return this;
}
@Override
public PathInputSpec defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
@Override
public PathInputSpec renderer(Function<PathInputContext, List<AttributedString>> renderer) {
this.renderer = renderer;
return this;
}
@Override
public PathInputSpec template(String location) {
this.templateLocation = location;
return this;
}
@Override
public PathInputSpec preHandler(Consumer<PathInputContext> handler) {
this.preHandlers.add(handler);
return this;
}
@Override
public PathInputSpec postHandler(Consumer<PathInputContext> handler) {
this.postHandlers.add(handler);
return this;
}
@Override
public PathInputSpec storeResult(boolean store) {
this.storeResult = store;
return this;
}
@Override
public PathInputSpec next(Function<PathInputContext, String> next) {
this.next = next;
return this;
}
@Override
public Builder and() {
getBuilder().addPathInput(this);
return getBuilder();
}
@Override
public PathInputSpec getThis() {
return this;
}
public String getName() {
return name;
}
public String getResultValue() {
return resultValue;
}
public ResultMode getResultMode() {
return resultMode;
}
public String getDefaultValue() {
return defaultValue;
}
public Function<PathInputContext, List<AttributedString>> getRenderer() {
return renderer;
}
public String getTemplateLocation() {
return templateLocation;
}
public List<Consumer<PathInputContext>> getPreHandlers() {
return preHandlers;
}
public List<Consumer<PathInputContext>> getPostHandlers() {
return postHandlers;
}
public boolean isStoreResult() {
return storeResult;
}
public Function<PathInputContext, String> getNext() {
return next;
}
}

View File

@@ -0,0 +1,194 @@
/*
* 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.component.flow;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
import org.springframework.shell.component.support.SelectorItem;
/**
* Base impl for {@link SingleItemSelectorSpec}.
*
* @author Janne Valkealahti
*/
public abstract class BaseSingleItemSelector extends BaseInput<SingleItemSelectorSpec> implements SingleItemSelectorSpec {
private String name;
private String resultValue;
private ResultMode resultMode;
private Map<String, String> selectItems = new HashMap<>();
private Comparator<SelectorItem<String>> comparator;
private Function<SingleItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer;
private Integer maxItems;
private List<Consumer<SingleItemSelectorContext<String, SelectorItem<String>>>> preHandlers = new ArrayList<>();
private List<Consumer<SingleItemSelectorContext<String, SelectorItem<String>>>> postHandlers = new ArrayList<>();
private boolean storeResult = true;
private String templateLocation;
private Function<SingleItemSelectorContext<String, SelectorItem<String>>, String> next;
public BaseSingleItemSelector(BaseBuilder builder, String id) {
super(builder, id);
}
@Override
public SingleItemSelectorSpec name(String name) {
this.name = name;
return this;
}
@Override
public SingleItemSelectorSpec resultValue(String resultValue) {
this.resultValue = resultValue;
return this;
}
@Override
public SingleItemSelectorSpec resultMode(ResultMode resultMode) {
this.resultMode = resultMode;
return this;
}
@Override
public SingleItemSelectorSpec selectItem(String name, String item) {
this.selectItems.put(name, item);
return this;
}
@Override
public SingleItemSelectorSpec selectItems(Map<String, String> selectItems) {
this.selectItems.putAll(selectItems);
return this;
}
@Override
public SingleItemSelectorSpec sort(Comparator<SelectorItem<String>> comparator) {
this.comparator = comparator;
return this;
}
@Override
public SingleItemSelectorSpec renderer(Function<SingleItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer) {
this.renderer = renderer;
return this;
}
@Override
public SingleItemSelectorSpec template(String location) {
this.templateLocation = location;
return this;
}
@Override
public SingleItemSelectorSpec max(int max) {
this.maxItems = max;
return this;
}
@Override
public SingleItemSelectorSpec preHandler(Consumer<SingleItemSelectorContext<String, SelectorItem<String>>> handler) {
this.preHandlers.add(handler);
return null;
}
@Override
public SingleItemSelectorSpec postHandler(Consumer<SingleItemSelectorContext<String, SelectorItem<String>>> handler) {
this.postHandlers.add(handler);
return this;
}
@Override
public SingleItemSelectorSpec storeResult(boolean store) {
this.storeResult = store;
return this;
}
@Override
public SingleItemSelectorSpec next(
Function<SingleItemSelectorContext<String, SelectorItem<String>>, String> next) {
this.next = next;
return this;
}
@Override
public Builder and() {
getBuilder().addSingleItemSelector(this);
return getBuilder();
}
@Override
public SingleItemSelectorSpec getThis() {
return this;
}
public String getName() {
return name;
}
public String getResultValue() {
return resultValue;
}
public ResultMode getResultMode() {
return resultMode;
}
public Map<String, String> getSelectItems() {
return selectItems;
}
public Comparator<SelectorItem<String>> getComparator() {
return comparator;
}
public Function<SingleItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> getRenderer() {
return renderer;
}
public String getTemplateLocation() {
return templateLocation;
}
public Integer getMaxItems() {
return maxItems;
}
public List<Consumer<SingleItemSelectorContext<String, SelectorItem<String>>>> getPreHandlers() {
return preHandlers;
}
public List<Consumer<SingleItemSelectorContext<String, SelectorItem<String>>>> getPostHandlers() {
return postHandlers;
}
public boolean isStoreResult() {
return storeResult;
}
public Function<SingleItemSelectorContext<String, SelectorItem<String>>, String> getNext() {
return next;
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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.component.flow;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.StringInput.StringInputContext;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
/**
* Base impl for {@link StringInputSpec}.
*
* @author Janne Valkealahti
*/
public abstract class BaseStringInput extends BaseInput<StringInputSpec> implements StringInputSpec {
private String name;
private String resultValue;
private ResultMode resultMode;
private String defaultValue;
private Character maskCharacter;
private Function<StringInputContext, List<AttributedString>> renderer;
private List<Consumer<StringInputContext>> preHandlers = new ArrayList<>();
private List<Consumer<StringInputContext>> postHandlers = new ArrayList<>();
private boolean storeResult = true;
private String templateLocation;
private Function<StringInputContext, String> next;
public BaseStringInput(BaseBuilder builder, String id) {
super(builder, id);
}
@Override
public StringInputSpec name(String name) {
this.name = name;
return this;
}
@Override
public StringInputSpec resultValue(String resultValue) {
this.resultValue = resultValue;
return this;
}
@Override
public StringInputSpec resultMode(ResultMode resultMode) {
this.resultMode = resultMode;
return this;
}
@Override
public StringInputSpec defaultValue(String defaultValue) {
this.defaultValue = defaultValue;
return this;
}
@Override
public StringInputSpec maskCharacter(Character maskCharacter) {
this.maskCharacter = maskCharacter;
return this;
}
@Override
public StringInputSpec renderer(Function<StringInputContext, List<AttributedString>> renderer) {
this.renderer = renderer;
return this;
}
@Override
public StringInputSpec template(String location) {
this.templateLocation = location;
return this;
}
@Override
public StringInputSpec preHandler(Consumer<StringInputContext> handler) {
this.preHandlers.add(handler);
return this;
}
@Override
public StringInputSpec postHandler(Consumer<StringInputContext> handler) {
this.postHandlers.add(handler);
return this;
}
@Override
public StringInputSpec storeResult(boolean store) {
this.storeResult = store;
return this;
}
@Override
public StringInputSpec next(Function<StringInputContext, String> next) {
this.next = next;
return this;
}
@Override
public Builder and() {
getBuilder().addStringInput(this);
return getBuilder();
}
@Override
public StringInputSpec getThis() {
return this;
}
public String getName() {
return name;
}
public String getResultValue() {
return resultValue;
}
public ResultMode getResultMode() {
return resultMode;
}
public String getDefaultValue() {
return defaultValue;
}
public Character getMaskCharacter() {
return maskCharacter;
}
public Function<StringInputContext, List<AttributedString>> getRenderer() {
return renderer;
}
public String getTemplateLocation() {
return templateLocation;
}
public List<Consumer<StringInputContext>> getPreHandlers() {
return preHandlers;
}
public List<Consumer<StringInputContext>> getPostHandlers() {
return postHandlers;
}
public boolean isStoreResult() {
return storeResult;
}
public Function<StringInputContext, String> getNext() {
return next;
}
}

View File

@@ -0,0 +1,628 @@
/*
* 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.component.flow;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jline.terminal.Terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.io.ResourceLoader;
import org.springframework.shell.component.ConfirmationInput;
import org.springframework.shell.component.MultiItemSelector;
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
import org.springframework.shell.component.PathInput;
import org.springframework.shell.component.PathInput.PathInputContext;
import org.springframework.shell.component.SingleItemSelector;
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
import org.springframework.shell.component.StringInput;
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
import org.springframework.shell.component.StringInput.StringInputContext;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.support.SelectorItem;
import org.springframework.shell.style.TemplateExecutor;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Wizart providing an implementation which allows more polished way to ask various inputs
* from a user using shell style components for simple text/path input, single select and
* multi-select.
*
* @author Janne Valkealahti
*/
public interface ComponentFlow {
/**
* Run a wizard and returns a result from it.
*
* @return the input wizard result
*/
ComponentFlowResult run();
/**
* 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);
}
/**
* Results from a flow run.
*/
interface ComponentFlowResult {
/**
* Gets a context.
*
* @return a context
*/
ComponentContext<?> getContext();
}
/**
* Interface for a wizard builder.
*/
interface Builder {
/**
* Gets a builder for string input.
*
* @param id the identifier
* @return builder for string input
*/
StringInputSpec withStringInput(String id);
/**
* Gets a builder for path input.
*
* @param id the identifier
* @return builder for text input
*/
PathInputSpec withPathInput(String id);
/**
* Gets a builder for confirmation input.
*
* @param id the identifier
* @return builder for text input
*/
ConfirmationInputSpec withConfirmationInput(String id);
/**
* Gets a builder for single item selector.
*
* @param id the identifier
* @return builder for single item selector
*/
SingleItemSelectorSpec withSingleItemSelector(String id);
/**
* Gets a builder for multi item selector.
*
* @param id the identifier
* @return builder for multi item selector
*/
MultiItemSelectorSpec withMultiItemSelector(String id);
/**
* Sets a {@link ResourceLoader}.
*
* @param resourceLoader the resource loader
* @return a builder
*/
Builder resourceLoader(ResourceLoader resourceLoader);
/**
* Sets a {@link TemplateExecutor}.
*
* @param templateExecutor the template executor
* @return a builder
*/
Builder templateExecutor(TemplateExecutor templateExecutor);
/**
* Builds instance of input wizard.
*
* @return instance of input wizard
*/
ComponentFlow build();
}
static abstract class BaseBuilder implements Builder {
private Terminal terminal;
private final List<BaseStringInput> stringInputs = new ArrayList<>();
private final List<BasePathInput> pathInputs = new ArrayList<>();
private final List<BaseConfirmationInput> confirmationInputs = new ArrayList<>();
private final List<BaseSingleItemSelector> singleItemSelectors = new ArrayList<>();
private final List<BaseMultiItemSelector> multiItemSelectors = new ArrayList<>();
private final AtomicInteger order = new AtomicInteger();
private final HashSet<String> uniqueIds = new HashSet<>();
private ResourceLoader resourceLoader;
private TemplateExecutor templateExecutor;
BaseBuilder(Terminal terminal) {
this.terminal = terminal;
}
@Override
public ComponentFlow build() {
return new DefaultComponentFlow(terminal, resourceLoader, templateExecutor, stringInputs, pathInputs,
confirmationInputs, singleItemSelectors, multiItemSelectors);
}
@Override
public StringInputSpec withStringInput(String id) {
return new DefaultStringInputSpec(this, id);
}
@Override
public PathInputSpec withPathInput(String id) {
return new DefaultPathInputSpec(this, id);
}
@Override
public ConfirmationInputSpec withConfirmationInput(String id) {
return new DefaultConfirmationInputSpec(this, id);
}
@Override
public SingleItemSelectorSpec withSingleItemSelector(String id) {
return new DefaultSingleInputSpec(this, id);
}
@Override
public MultiItemSelectorSpec withMultiItemSelector(String id) {
return new DefaultMultiInputSpec(this, id);
}
@Override
public Builder resourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
return this;
}
@Override
public Builder templateExecutor(TemplateExecutor templateExecutor) {
this.templateExecutor = templateExecutor;
return this;
}
void addStringInput(BaseStringInput input) {
checkUniqueId(input.getId());
input.setOrder(order.getAndIncrement());
stringInputs.add(input);
}
void addPathInput(BasePathInput input) {
checkUniqueId(input.getId());
input.setOrder(order.getAndIncrement());
pathInputs.add(input);
}
void addConfirmationInput(BaseConfirmationInput input) {
checkUniqueId(input.getId());
input.setOrder(order.getAndIncrement());
confirmationInputs.add(input);
}
void addSingleItemSelector(BaseSingleItemSelector input) {
checkUniqueId(input.getId());
input.setOrder(order.getAndIncrement());
singleItemSelectors.add(input);
}
void addMultiItemSelector(BaseMultiItemSelector input) {
checkUniqueId(input.getId());
input.setOrder(order.getAndIncrement());
multiItemSelectors.add(input);
}
ResourceLoader getResourceLoader() {
return resourceLoader;
}
TemplateExecutor getTemplateExecutor() {
return templateExecutor;
}
private void checkUniqueId(String id) {
if (uniqueIds.contains(id)) {
throw new IllegalArgumentException(String.format("Component with id %s is already registered", id));
}
uniqueIds.add(id);
}
}
static class DefaultBuilder extends BaseBuilder {
DefaultBuilder(Terminal terminal) {
super(terminal);
}
}
static class DefaultComponentFlowResult implements ComponentFlowResult {
private ComponentContext<?> context;
DefaultComponentFlowResult(ComponentContext<?> context) {
this.context = context;
}
public ComponentContext<?> getContext() {
return context;
}
}
static class DefaultComponentFlow implements ComponentFlow {
private static final Logger log = LoggerFactory.getLogger(DefaultComponentFlow.class);
private final Terminal terminal;
private final List<BaseStringInput> stringInputs;
private final List<BasePathInput> pathInputs;
private final List<BaseConfirmationInput> confirmationInputs;
private final List<BaseSingleItemSelector> singleInputs;
private final List<BaseMultiItemSelector> multiInputs;
private final ResourceLoader resourceLoader;
private final TemplateExecutor templateExecutor;
DefaultComponentFlow(Terminal terminal, ResourceLoader resourceLoader, TemplateExecutor templateExecutor,
List<BaseStringInput> stringInputs, List<BasePathInput> pathInputs, List<BaseConfirmationInput> confirmationInputs,
List<BaseSingleItemSelector> singleInputs, List<BaseMultiItemSelector> multiInputs) {
this.terminal = terminal;
this.resourceLoader = resourceLoader;
this.templateExecutor = templateExecutor;
this.stringInputs = stringInputs;
this.pathInputs = pathInputs;
this.confirmationInputs = confirmationInputs;
this.singleInputs = singleInputs;
this.multiInputs = multiInputs;
}
@Override
public ComponentFlowResult run() {
return runGetResults();
}
private static class OrderedInputOperationList {
private final Map<String, Node> map = new HashMap<>();
private Node first;
OrderedInputOperationList(List<OrderedInputOperation> values) {
Node ref = null;
for (OrderedInputOperation oio : values) {
Node node = new Node(oio);
map.put(oio.id, node);
if (ref != null) {
ref.next = node;
}
ref = node;
if (first == null) {
first = node;
}
}
}
Node get(String id) {
return map.get(id);
}
Node getFirst() {
return first;
}
static class Node {
OrderedInputOperation data;
Node next;
Node(OrderedInputOperation data) {
this.data = data;
}
}
}
private DefaultComponentFlowResult runGetResults() {
List<OrderedInputOperation> oios = Stream
.of(stringInputsStream(), pathInputsStream(), confirmationInputsStream(),
singleItemSelectorsStream(), multiItemSelectorsStream())
.flatMap(oio -> oio)
.sorted(OrderComparator.INSTANCE)
.collect(Collectors.toList());
OrderedInputOperationList oiol = new OrderedInputOperationList(oios);
ComponentContext<?> context = ComponentContext.empty();
OrderedInputOperationList.Node node = oiol.getFirst();
while (node != null) {
log.debug("Calling apply for {}", node.data.id);
context = node.data.getOperation().apply(context);
if (node.data.next != null) {
Optional<String> n = node.data.next.apply(context);
if (n.isPresent()) {
node = oiol.get(n.get());
}
else {
node = node.next;
}
}
else {
node = node.next;
}
}
return new DefaultComponentFlowResult(context);
}
private Stream<OrderedInputOperation> stringInputsStream() {
return stringInputs.stream().map(input -> {
StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue());
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
&& StringUtils.hasText(input.getResultValue())) {
context.put(input.getId(), input.getResultValue());
return context;
}
selector.setResourceLoader(resourceLoader);
selector.setTemplateExecutor(templateExecutor);
selector.setMaskCharater(input.getMaskCharacter());
if (StringUtils.hasText(input.getTemplateLocation())) {
selector.setTemplateLocation(input.getTemplateLocation());
}
if (input.getRenderer() != null) {
selector.setRenderer(input.getRenderer());
}
if (input.isStoreResult()) {
if (input.getResultMode() == ResultMode.VERIFY && StringUtils.hasText(input.getResultValue())) {
selector.addPreRunHandler(c -> {
c.setDefaultValue(input.getResultValue());
});
}
selector.addPostRunHandler(c -> {
c.put(input.getId(), c.getResultValue());
});
}
for (Consumer<StringInputContext> handler : input.getPreHandlers()) {
selector.addPreRunHandler(handler);
}
for (Consumer<StringInputContext> handler : input.getPostHandlers()) {
selector.addPostRunHandler(handler);
}
return selector.run(context);
};
Function<StringInputContext, String> f1 = input.getNext();
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
: Optional.empty();
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
});
}
private Stream<OrderedInputOperation> pathInputsStream() {
return pathInputs.stream().map(input -> {
PathInput selector = new PathInput(terminal, input.getName());
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
&& StringUtils.hasText(input.getResultValue())) {
context.put(input.getId(), Paths.get(input.getResultValue()));
return context;
}
selector.setResourceLoader(resourceLoader);
selector.setTemplateExecutor(templateExecutor);
if (StringUtils.hasText(input.getTemplateLocation())) {
selector.setTemplateLocation(input.getTemplateLocation());
}
if (input.getRenderer() != null) {
selector.setRenderer(input.getRenderer());
}
if (input.isStoreResult()) {
selector.addPostRunHandler(c -> {
c.put(input.getId(), c.getResultValue());
});
}
for (Consumer<PathInputContext> handler : input.getPreHandlers()) {
selector.addPreRunHandler(handler);
}
for (Consumer<PathInputContext> handler : input.getPostHandlers()) {
selector.addPostRunHandler(handler);
}
return selector.run(context);
};
Function<PathInputContext, String> f1 = input.getNext();
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
: Optional.empty();
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
});
}
private Stream<OrderedInputOperation> confirmationInputsStream() {
return confirmationInputs.stream().map(input -> {
ConfirmationInput selector = new ConfirmationInput(terminal, input.getName(), input.getDefaultValue());
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
selector.setResourceLoader(resourceLoader);
selector.setTemplateExecutor(templateExecutor);
if (StringUtils.hasText(input.getTemplateLocation())) {
selector.setTemplateLocation(input.getTemplateLocation());
}
if (input.getRenderer() != null) {
selector.setRenderer(input.getRenderer());
}
if (input.isStoreResult()) {
selector.addPostRunHandler(c -> {
c.put(input.getId(), c.getResultValue());
});
}
for (Consumer<ConfirmationInputContext> handler : input.getPreHandlers()) {
selector.addPreRunHandler(handler);
}
for (Consumer<ConfirmationInputContext> handler : input.getPostHandlers()) {
selector.addPostRunHandler(handler);
}
return selector.run(context);
};
Function<ConfirmationInputContext, String> f1 = input.getNext();
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
: Optional.empty();
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
});
}
private Stream<OrderedInputOperation> singleItemSelectorsStream() {
return singleInputs.stream().map(input -> {
List<SelectorItem<String>> selectorItems = input.getSelectItems().entrySet().stream()
.map(e -> SelectorItem.of(e.getKey(), e.getValue()))
.collect(Collectors.toList());
SingleItemSelector<String, SelectorItem<String>> selector = new SingleItemSelector<>(terminal,
selectorItems, input.getName(), input.getComparator());
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
&& StringUtils.hasText(input.getResultValue())) {
context.put(input.getId(), input.getResultValue());
return context;
}
selector.setResourceLoader(resourceLoader);
selector.setTemplateExecutor(templateExecutor);
if (StringUtils.hasText(input.getTemplateLocation())) {
selector.setTemplateLocation(input.getTemplateLocation());
}
if (input.getRenderer() != null) {
selector.setRenderer(input.getRenderer());
}
if (input.getMaxItems() != null) {
selector.setMaxItems(input.getMaxItems());
}
if (input.isStoreResult()) {
selector.addPostRunHandler(c -> {
c.getValue().ifPresent(v -> {
c.put(input.getId(), v);
});
});
}
for (Consumer<SingleItemSelectorContext<String, SelectorItem<String>>> handler : input.getPreHandlers()) {
selector.addPreRunHandler(handler);
}
for (Consumer<SingleItemSelectorContext<String, SelectorItem<String>>> handler : input.getPostHandlers()) {
selector.addPostRunHandler(handler);
}
return selector.run(context);
};
Function<SingleItemSelectorContext<String, SelectorItem<String>>, String> f1 = input.getNext();
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
: Optional.empty();
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
});
}
private Stream<OrderedInputOperation> multiItemSelectorsStream() {
return multiInputs.stream().map(input -> {
List<SelectorItem<String>> selectorItems = input.getSelectItems().stream()
.map(si -> SelectorItem.of(si.name(), si.item(), si.enabled()))
.collect(Collectors.toList());
MultiItemSelector<String, SelectorItem<String>> selector = new MultiItemSelector<>(terminal,
selectorItems, input.getName(), input.getComparator());
Function<ComponentContext<?>, ComponentContext<?>> operation = (context) -> {
if (input.getResultMode() == ResultMode.ACCEPT && input.isStoreResult()
&& !ObjectUtils.isEmpty(input.getResultValues())) {
context.put(input.getId(), input.getResultValues());
return context;
}
selector.setResourceLoader(resourceLoader);
selector.setTemplateExecutor(templateExecutor);
if (StringUtils.hasText(input.getTemplateLocation())) {
selector.setTemplateLocation(input.getTemplateLocation());
}
if (input.getRenderer() != null) {
selector.setRenderer(input.getRenderer());
}
if (input.getMaxItems() != null) {
selector.setMaxItems(input.getMaxItems());
}
if (input.isStoreResult()) {
selector.addPostRunHandler(c -> {
c.put(input.getId(), c.getValues());
});
}
for (Consumer<MultiItemSelectorContext<String, SelectorItem<String>>> handler : input.getPreHandlers()) {
selector.addPreRunHandler(handler);
}
for (Consumer<MultiItemSelectorContext<String, SelectorItem<String>>> handler : input.getPostHandlers()) {
selector.addPostRunHandler(handler);
}
return selector.run(context);
};
Function<MultiItemSelectorContext<String, SelectorItem<String>>, String> f1 = input.getNext();
Function<ComponentContext<?>, Optional<String>> f2 = context -> f1 != null
? Optional.ofNullable(f1.apply(selector.getThisContext(context)))
: Optional.empty();
return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2);
});
}
}
static class OrderedInputOperation implements Ordered {
private String id;
private int order;
private Function<ComponentContext<?>, ComponentContext<?>> operation;
private Function<ComponentContext<?>, Optional<String>> next;
@Override
public int getOrder() {
return order;
}
public String getId() {
return id;
}
public Function<ComponentContext<?>, ComponentContext<?>> getOperation() {
return operation;
}
public Function<ComponentContext<?>, Optional<String>> getNext() {
return next;
}
static OrderedInputOperation of(String id, int order,
Function<ComponentContext<?>, ComponentContext<?>> operation,
Function<ComponentContext<?>, Optional<String>> next) {
OrderedInputOperation oio = new OrderedInputOperation();
oio.id = id;
oio.order = order;
oio.operation = operation;
oio.next = next;
return oio;
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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.component.flow;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.ConfirmationInput.ConfirmationInputContext;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
/**
* Interface for string input spec builder.
*
* @author Janne Valkealahti
*/
public interface ConfirmationInputSpec extends BaseInputSpec<ConfirmationInputSpec> {
/**
* Sets a name.
*
* @param name the name
* @return a builder
*/
ConfirmationInputSpec name(String name);
/**
* Sets a default value.
*
* @param defaultValue the defult value
* @return a builder
*/
ConfirmationInputSpec defaultValue(Boolean defaultValue);
/**
* Sets a renderer function.
*
* @param renderer the renderer
* @return a builder
*/
ConfirmationInputSpec renderer(Function<ConfirmationInputContext, List<AttributedString>> renderer);
/**
* Sets a default renderer template location.
*
* @param location the template location
* @return a builder
*/
ConfirmationInputSpec template(String location);
/**
* Adds a pre-run context handler.
*
* @param handler the context handler
* @return a builder
*/
ConfirmationInputSpec preHandler(Consumer<ConfirmationInputContext> handler);
/**
* Adds a post-run context handler.
*
* @param handler the context handler
* @return a builder
*/
ConfirmationInputSpec postHandler(Consumer<ConfirmationInputContext> handler);
/**
* Automatically stores result from a {@link ConfirmationInputContext} into
* {@link ComponentContext} with key given to builder. Defaults to {@code true}.
*
* @param store the flag if storing result
* @return a builder
*/
ConfirmationInputSpec storeResult(boolean store);
/**
* Define a function which may return id of a next component to go.
*
* @param next next component function
* @return a builder
*/
ConfirmationInputSpec next(Function<ConfirmationInputContext, String> next);
/**
* Build and return parent builder.
*
* @return the parent builder
*/
Builder and();
}

View File

@@ -0,0 +1,30 @@
/*
* 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.component.flow;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
/**
* Default impl for {@link BaseConfirmationInput}.
*
* @author Janne Valkealahti
*/
public class DefaultConfirmationInputSpec extends BaseConfirmationInput {
public DefaultConfirmationInputSpec(BaseBuilder builder, String id) {
super(builder, id);
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.component.flow;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
/**
* Default impl for {@link BaseMultiItemSelector}.
*
* @author Janne Valkealahti
*/
public class DefaultMultiInputSpec extends BaseMultiItemSelector {
public DefaultMultiInputSpec(BaseBuilder builder, String id) {
super(builder, id);
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.component.flow;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
/**
* Default impl for {@link BasePathInput}.
*
* @author Janne Valkealahti
*/
public class DefaultPathInputSpec extends BasePathInput {
public DefaultPathInputSpec(BaseBuilder builder, String id) {
super(builder, id);
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.component.flow;
/**
* Default impl for {@link SelectItem}.
*
* @author Janne Valkealahti
*/
public class DefaultSelectItem implements SelectItem {
private String name;
private String item;
private boolean enabled;
public DefaultSelectItem(String name, String item, boolean enabled) {
this.name = name;
this.item = item;
this.enabled = enabled;
}
@Override
public String name() {
return name;
}
@Override
public String item() {
return item;
}
@Override
public boolean enabled() {
return enabled;
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.component.flow;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
/**
* Default impl for {@link BaseSingleItemSelector}.
*
* @author Janne Valkealahti
*/
public class DefaultSingleInputSpec extends BaseSingleItemSelector {
public DefaultSingleInputSpec(BaseBuilder builder, String id) {
super(builder, id);
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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.component.flow;
import org.springframework.shell.component.flow.ComponentFlow.BaseBuilder;
/**
* Default impl for {@link BaseStringInput}.
*
* @author Janne Valkealahti
*/
public class DefaultStringInputSpec extends BaseStringInput {
public DefaultStringInputSpec(BaseBuilder builder, String id) {
super(builder, id);
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.component.flow;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
import org.springframework.shell.component.support.SelectorItem;
/**
* Interface for multi input spec builder.
*
* @author Janne Valkealahti
*/
public interface MultiItemSelectorSpec extends BaseInputSpec<MultiItemSelectorSpec>{
/**
* Sets a name.
*
* @param name the name
* @return a builder
*/
MultiItemSelectorSpec name(String name);
/**
* Sets a result values.
*
* @param resultValues the result values
* @return a builder
*/
MultiItemSelectorSpec resultValues(List<String> resultValues);
/**
* Sets a result mode.
*
* @param resultMode the result mode
* @return a builder
*/
MultiItemSelectorSpec resultMode(ResultMode resultMode);
/**
* Adds a list of select items.
*
* @param selectItems the select items
* @return a builder
*/
MultiItemSelectorSpec selectItems(List<SelectItem> selectItems);
/**
* Sets a {@link Comparator} for sorting items.
*
* @param comparator the item comparator
* @return a builder
*/
MultiItemSelectorSpec sort(Comparator<SelectorItem<String>> comparator);
/**
* Sets a renderer function.
*
* @param renderer the renderer
* @return a builder
*/
MultiItemSelectorSpec renderer(Function<MultiItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer);
/**
* Sets a default renderer template location.
*
* @param location the template location
* @return a builder
*/
MultiItemSelectorSpec template(String location);
/**
* Sets a maximum number of items in a selector list;
*
* @param max the maximum number of items
* @return a builder
*/
MultiItemSelectorSpec max(int max);
/**
* Adds a pre-run context handler.
*
* @param handler the context handler
* @return a builder
*/
MultiItemSelectorSpec preHandler(Consumer<MultiItemSelectorContext<String, SelectorItem<String>>> handler);
/**
* Adds a post-run context handler.
*
* @param handler the context handler
* @return a builder
*/
MultiItemSelectorSpec postHandler(Consumer<MultiItemSelectorContext<String, SelectorItem<String>>> handler);
/**
* Automatically stores result from a {@link MultiItemSelectorContext} into
* {@link ComponentContext} with key given to builder. Defaults to {@code true}.
*
* @param store the flag if storing result
* @return a builder
*/
MultiItemSelectorSpec storeResult(boolean store);
/**
* Define a function which may return id of a next component to go.
*
* @param next next component function
* @return a builder
*/
MultiItemSelectorSpec next(Function<MultiItemSelectorContext<String, SelectorItem<String>>, String> next);
/**
* Build and return parent builder.
*
* @return the parent builder
*/
Builder and();
}

View File

@@ -0,0 +1,123 @@
/*
* 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.component.flow;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.PathInput.PathInputContext;
import org.springframework.shell.component.StringInput.StringInputContext;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
/**
* Interface for path input spec builder.
*
* @author Janne Valkealahti
*/
public interface PathInputSpec extends BaseInputSpec<PathInputSpec> {
/**
* Sets a name.
*
* @param name the name
* @return a builder
*/
PathInputSpec name(String name);
/**
* Sets a result value.
*
* @param resultValue the result value
* @return a builder
*/
PathInputSpec resultValue(String resultValue);
/**
* Sets a result mode.
*
* @param resultMode the result mode
* @return a builder
*/
PathInputSpec resultMode(ResultMode resultMode);
/**
* Sets a default value.
*
* @param defaultValue the defult value
* @return a builder
*/
PathInputSpec defaultValue(String defaultValue);
/**
* Sets a renderer function.
*
* @param renderer the renderer
* @return a builder
*/
PathInputSpec renderer(Function<PathInputContext, List<AttributedString>> renderer);
/**
* Sets a default renderer template location.
*
* @param location the template location
* @return a builder
*/
PathInputSpec template(String location);
/**
* Adds a pre-run context handler.
*
* @param handler the context handler
* @return a builder
*/
PathInputSpec preHandler(Consumer<PathInputContext> handler);
/**
* Adds a post-run context handler.
*
* @param handler the context handler
* @return a builder
*/
PathInputSpec postHandler(Consumer<PathInputContext> handler);
/**
* Automatically stores result from a {@link StringInputContext} into
* {@link ComponentContext} with key given to builder. Defaults to {@code true}.
*
* @param store the flag if storing result
* @return a builder
*/
PathInputSpec storeResult(boolean store);
/**
* Define a function which may return id of a next component to go.
*
* @param next next component function
* @return a builder
*/
PathInputSpec next(Function<PathInputContext, String> next);
/**
* Build and return parent builder.
*
* @return the parent builder
*/
Builder and();
}

View File

@@ -0,0 +1,37 @@
/*
* 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.component.flow;
/**
* Enumeration of a modes instructing how {@code resultValue} is handled.
*
* @author Janne Valkealahti
*/
public enum ResultMode {
/**
* Blindly accept a given {@code resultValue} resulting component run to be
* skipped and value set as a result automatically.
*/
ACCEPT,
/**
* Verify a given {@code resultValue} with a component run. It is up to a
* component to define how it's done but usually result value is set as a
* default value which allows user to just continue.
*/
VERIFY
}

View File

@@ -0,0 +1,53 @@
/*
* 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.component.flow;
/**
* Interface for selectitem contract in selectors.
*
* @author Janne Valkealahti
*/
public interface SelectItem {
/**
* Gets a name.
*
* @return a name
*/
String name();
/**
* Gets an item
*
* @return an item
*/
String item();
/**
* Returns if item is enabled.
*
* @return if item is enabled
*/
boolean enabled();
public static SelectItem of(String name, String item) {
return of(name, item, true);
}
public static SelectItem of(String name, String item, boolean enabled) {
return new DefaultSelectItem(name, item, enabled);
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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.component.flow;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
import org.springframework.shell.component.support.SelectorItem;
/**
* Interface for single item selector spec builder.
*
* @author Janne Valkealahti
*/
public interface SingleItemSelectorSpec extends BaseInputSpec<SingleItemSelectorSpec> {
/**
* Sets a name.
*
* @param name the name
* @return a builder
*/
SingleItemSelectorSpec name(String name);
/**
* Sets a result value.
*
* @param resultValue the result value
* @return a builder
*/
SingleItemSelectorSpec resultValue(String resultValue);
/**
* Sets a result mode.
*
* @param resultMode the result mode
* @return a builder
*/
SingleItemSelectorSpec resultMode(ResultMode resultMode);
/**
* Adds a select item.
*
* @param name the name
* @param item the item
* @return a builder
*/
SingleItemSelectorSpec selectItem(String name, String item);
/**
* Adds a map of select items.
*
* @param selectItems the select items
* @return a builder
*/
SingleItemSelectorSpec selectItems(Map<String, String> selectItems);
/**
* Sets a {@link Comparator} for sorting items.
*
* @param comparator the item comparator
* @return a builder
*/
SingleItemSelectorSpec sort(Comparator<SelectorItem<String>> comparator);
/**
* Sets a renderer function.
*
* @param renderer the renderer
* @return a builder
*/
SingleItemSelectorSpec renderer(Function<SingleItemSelectorContext<String, SelectorItem<String>>, List<AttributedString>> renderer);
/**
* Sets a default renderer template location.
*
* @param location the template location
* @return a builder
*/
SingleItemSelectorSpec template(String location);
/**
* Sets a maximum number of items in a selector list;
*
* @param max the maximum number of items
* @return a builder
*/
SingleItemSelectorSpec max(int max);
/**
* Adds a pre-run context handler.
*
* @param handler the context handler
* @return a builder
*/
SingleItemSelectorSpec preHandler(Consumer<SingleItemSelectorContext<String, SelectorItem<String>>> handler);
/**
* Adds a post-run context handler.
*
* @param handler the context handler
* @return a builder
*/
SingleItemSelectorSpec postHandler(Consumer<SingleItemSelectorContext<String, SelectorItem<String>>> handler);
/**
* Automatically stores result from a {@link SingleItemSelectorContext} into
* {@link ComponentContext} with key given to builder. Defaults to {@code true}.
*
* @param store the flag if storing result
* @return a builder
*/
SingleItemSelectorSpec storeResult(boolean store);
/**
* Define a function which may return id of a next component to go.
*
* @param next next component function
* @return a builder
*/
SingleItemSelectorSpec next(Function<SingleItemSelectorContext<String, SelectorItem<String>>, String> next);
/**
* Build and return parent builder.
*
* @return the parent builder
*/
Builder and();
}

View File

@@ -0,0 +1,130 @@
/*
* 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.component.flow;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jline.utils.AttributedString;
import org.springframework.shell.component.StringInput.StringInputContext;
import org.springframework.shell.component.context.ComponentContext;
import org.springframework.shell.component.flow.ComponentFlow.Builder;
/**
* Interface for string input spec builder.
*
* @author Janne Valkealahti
*/
public interface StringInputSpec extends BaseInputSpec<StringInputSpec> {
/**
* Sets a name.
*
* @param name the name
* @return a builder
*/
StringInputSpec name(String name);
/**
* Sets a result value.
*
* @param resultValue the result value
* @return a builder
*/
StringInputSpec resultValue(String resultValue);
/**
* Sets a result mode.
*
* @param resultMode the result mode
* @return a builder
*/
StringInputSpec resultMode(ResultMode resultMode);
/**
* Sets a default value.
*
* @param defaultValue the defult value
* @return a builder
*/
StringInputSpec defaultValue(String defaultValue);
/**
* Sets a mask character.
*
* @param maskCharacter the mask character
* @return a builder
*/
StringInputSpec maskCharacter(Character maskCharacter);
/**
* Sets a renderer function.
*
* @param renderer the renderer
* @return a builder
*/
StringInputSpec renderer(Function<StringInputContext, List<AttributedString>> renderer);
/**
* Sets a default renderer template location.
*
* @param location the template location
* @return a builder
*/
StringInputSpec template(String location);
/**
* Adds a pre-run context handler.
*
* @param handler the context handler
* @return a builder
*/
StringInputSpec preHandler(Consumer<StringInputContext> handler);
/**
* Adds a post-run context handler.
*
* @param handler the context handler
* @return a builder
*/
StringInputSpec postHandler(Consumer<StringInputContext> handler);
/**
* Automatically stores result from a {@link StringInputContext} into
* {@link ComponentContext} with key given to builder. Defaults to {@code true}.
*
* @param store the flag if storing result
* @return a builder
*/
StringInputSpec storeResult(boolean store);
/**
* Define a function which may return id of a next component to go.
*
* @param next next component function
* @return a builder
*/
StringInputSpec next(Function<StringInputContext, String> next);
/**
* Build and return parent builder.
*
* @return the parent builder
*/
Builder and();
}

View File

@@ -219,7 +219,7 @@ public abstract class AbstractComponent<T extends ComponentContext<T>> implement
* @param context the context
* @return a component context
*/
protected abstract T getThisContext(ComponentContext<?> context);
public abstract T getThisContext(ComponentContext<?> context);
/**
* Read input.

View File

@@ -0,0 +1,196 @@
/*
* 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.component.flow;
import java.io.ByteArrayOutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import org.jline.keymap.KeyMap;
import org.jline.terminal.Terminal;
import org.jline.terminal.impl.DumbTerminal;
import org.jline.utils.AttributedString;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.shell.style.TemplateExecutor;
import org.springframework.shell.style.Theme;
import org.springframework.shell.style.ThemeRegistry;
import org.springframework.shell.style.ThemeResolver;
import org.springframework.shell.style.ThemeSettings;
import static org.jline.keymap.KeyMap.del;
public abstract class AbstractShellTests {
private ExecutorService executorService;
private PipedInputStream pipedInputStream;
private PipedOutputStream pipedOutputStream;
private LinkedBlockingQueue<byte[]> bytesQueue;
private ByteArrayOutputStream consoleOut;
private Terminal terminal;
private TemplateExecutor templateExecutor;
private ResourceLoader resourceLoader;
@BeforeEach
public void setup() throws Exception {
executorService = Executors.newFixedThreadPool(1);
pipedInputStream = new PipedInputStream();
pipedOutputStream = new PipedOutputStream();
bytesQueue = new LinkedBlockingQueue<>();
consoleOut = new ByteArrayOutputStream();
ThemeRegistry themeRegistry = new ThemeRegistry();
themeRegistry.register(new Theme() {
@Override
public String getName() {
return "default";
}
@Override
public ThemeSettings getSettings() {
return ThemeSettings.themeSettings();
}
});
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
templateExecutor = new TemplateExecutor(themeResolver);
resourceLoader = new DefaultResourceLoader();
pipedInputStream.connect(pipedOutputStream);
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
executorService.execute(() -> {
try {
while (true) {
byte[] take = bytesQueue.take();
pipedOutputStream.write(take);
pipedOutputStream.flush();
}
} catch (Exception e) {
}
});
}
@AfterEach
public void cleanup() {
executorService.shutdown();
}
protected void write(byte[] bytes) {
bytesQueue.add(bytes);
}
protected String consoleOut() {
return AttributedString.fromAnsi(consoleOut.toString()).toString();
}
protected Terminal getTerminal() {
return terminal;
}
protected ResourceLoader getResourceLoader() {
return resourceLoader;
}
protected TemplateExecutor getTemplateExecutor() {
return templateExecutor;
}
protected class TestBuffer {
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
public TestBuffer() {
}
public TestBuffer(String str) {
append(str);
}
public TestBuffer(char[] chars) {
append(new String(chars));
}
@Override
public String toString() {
try {
return out.toString(StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
public TestBuffer cr() {
return append("\r");
}
public TestBuffer backspace() {
return append(del());
}
public TestBuffer backspace(int count) {
TestBuffer buf = this;
for (int i = 0; i < count; i++) {
buf = backspace();
}
return buf;
}
public TestBuffer down() {
return append("\033[B");
}
public TestBuffer ctrl(char let) {
return append(KeyMap.ctrl(let));
}
public TestBuffer ctrlE() {
return ctrl('E');
}
public TestBuffer ctrlY() {
return ctrl('Y');
}
public TestBuffer space() {
return append(" ");
}
public byte[] getBytes() {
return out.toByteArray();
}
public TestBuffer append(final String str) {
for (byte b : str.getBytes(StandardCharsets.UTF_8)) {
append(b);
}
return this;
}
public TestBuffer append(final int i) {
// consoleOut.reset();
out.write((byte) i);
return this;
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* 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.component.flow;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.springframework.shell.component.flow.ComponentFlow.ComponentFlowResult;
import static org.assertj.core.api.Assertions.assertThat;
public class ComponentFlowTests extends AbstractShellTests {
@Test
public void testSimpleFlow() throws InterruptedException {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
ComponentFlow wizard = ComponentFlow.builder(getTerminal())
.resourceLoader(getResourceLoader())
.templateExecutor(getTemplateExecutor())
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.and()
.withStringInput("field2")
.name("Field2")
.and()
.withPathInput("path1")
.name("Path1")
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.and()
.build();
ExecutorService service = Executors.newFixedThreadPool(1);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
service.execute(() -> {
result.set(wizard.run());
latch.countDown();
});
// field1
TestBuffer testBuffer = new TestBuffer().cr();
write(testBuffer.getBytes());
// field2
testBuffer = new TestBuffer().append("Field2Value").cr();
write(testBuffer.getBytes());
// path1
testBuffer = new TestBuffer().append("fakedir").cr();
write(testBuffer.getBytes());
// single1
testBuffer = new TestBuffer().cr();
write(testBuffer.getBytes());
// multi1
testBuffer = new TestBuffer().ctrlE().space().cr();
write(testBuffer.getBytes());
latch.await(4, TimeUnit.SECONDS);
ComponentFlowResult inputWizardResult = result.get();
assertThat(inputWizardResult).isNotNull();
String field1 = inputWizardResult.getContext().get("field1");
String field2 = inputWizardResult.getContext().get("field2");
Path path1 = inputWizardResult.getContext().get("path1");
String single1 = inputWizardResult.getContext().get("single1");
List<String> multi1 = inputWizardResult.getContext().get("multi1");
assertThat(field1).isEqualTo("defaultField1Value");
assertThat(field2).isEqualTo("Field2Value");
assertThat(path1.toString()).contains("fakedir");
assertThat(single1).isEqualTo("value1");
assertThat(multi1).containsExactlyInAnyOrder("value2");
assertThat(consoleOut()).contains("Field1 defaultField1Value");
}
@Test
public void testSkipsGivenComponents() throws InterruptedException {
ComponentFlow wizard = ComponentFlow.builder(getTerminal())
.withStringInput("id1")
.name("name")
.resultValue("value1")
.resultMode(ResultMode.ACCEPT)
.and()
.withPathInput("id2")
.name("name")
.resultValue("value2")
.resultMode(ResultMode.ACCEPT)
.and()
.withSingleItemSelector("id3")
.resultValue("value3")
.resultMode(ResultMode.ACCEPT)
.and()
.withMultiItemSelector("id4")
.resultValues(Arrays.asList("value4"))
.resultMode(ResultMode.ACCEPT)
.and()
.build();
ExecutorService service = Executors.newFixedThreadPool(1);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
service.execute(() -> {
result.set(wizard.run());
latch.countDown();
});
latch.await(4, TimeUnit.SECONDS);
ComponentFlowResult inputWizardResult = result.get();
assertThat(inputWizardResult).isNotNull();
String id1 = inputWizardResult.getContext().get("id1");
Path id2 = inputWizardResult.getContext().get("id2");
String id3 = inputWizardResult.getContext().get("id3");
List<String> id4 = inputWizardResult.getContext().get("id4");
assertThat(id1).isEqualTo("value1");
assertThat(id2.toString()).contains("value2");
assertThat(id3).isEqualTo("value3");
assertThat(id4).containsExactlyInAnyOrder("value4");
}
@Test
public void testChoosesDynamically() throws InterruptedException {
ComponentFlow wizard = ComponentFlow.builder(getTerminal())
.resourceLoader(getResourceLoader())
.templateExecutor(getTemplateExecutor())
.withStringInput("id1")
.name("name")
.next(ctx -> ctx.get("id1"))
.and()
.withStringInput("id2")
.name("name")
.resultValue("value2")
.resultMode(ResultMode.ACCEPT)
.next(ctx -> null)
.and()
.withStringInput("id3")
.name("name")
.resultValue("value3")
.resultMode(ResultMode.ACCEPT)
.next(ctx -> null)
.and()
.build();
ExecutorService service = Executors.newFixedThreadPool(1);
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<ComponentFlowResult> result = new AtomicReference<>();
service.execute(() -> {
result.set(wizard.run());
latch.countDown();
});
// id1
TestBuffer testBuffer = new TestBuffer().append("id3").cr();
write(testBuffer.getBytes());
// id3
testBuffer = new TestBuffer().cr();
write(testBuffer.getBytes());
latch.await(4, TimeUnit.SECONDS);
ComponentFlowResult inputWizardResult = result.get();
assertThat(inputWizardResult).isNotNull();
String id1 = inputWizardResult.getContext().get("id1");
// TODO: should be able to check if variable exists
// String id2 = inputWizardResult.getContext().get("id2");
String id3 = inputWizardResult.getContext().get("id3");
assertThat(id1).isEqualTo("id3");
// assertThat(id2).isNull();
assertThat(id3).isEqualTo("value3");
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.samples.standard;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.shell.component.flow.ComponentFlow;
import org.springframework.shell.component.flow.SelectItem;
import org.springframework.shell.standard.AbstractShellComponent;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class ComponentFlowCommands extends AbstractShellComponent {
@ShellMethod(key = "flow showcase", value = "Showcase", group = "Flow")
public void showcase() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
ComponentFlow flow = ComponentFlow.builder(getTerminal())
.resourceLoader(getResourceLoader())
.templateExecutor(getTemplateExecutor())
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.and()
.withStringInput("field2")
.name("Field2")
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.and()
.withPathInput("path1")
.name("Path1")
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.and()
.build();
flow.run();
}
@ShellMethod(key = "flow conditional", value = "Second component based on first", group = "Flow")
public void conditional() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("Field1", "field1");
single1SelectItems.put("Field2", "field2");
ComponentFlow flow = ComponentFlow.builder(getTerminal())
.resourceLoader(getResourceLoader())
.templateExecutor(getTemplateExecutor())
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.next(ctx -> ctx.getResultItem().get().getItem())
.and()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.next(ctx -> null)
.and()
.withStringInput("field2")
.name("Field2")
.defaultValue("defaultField2Value")
.next(ctx -> null)
.and()
.build();
flow.run();
}
}