diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java index 224f8076..112ea035 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/ConfirmationInput.java @@ -63,7 +63,7 @@ public class ConfirmationInput extends AbstractTextComponent context) { + public ConfirmationInputContext getThisContext(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java index 79147544..bd75a5bf 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/MultiItemSelector.java @@ -52,7 +52,7 @@ public class MultiItemSelector getThisContext(ComponentContext context) { + public MultiItemSelectorContext getThisContext(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java index 7fa1259b..f42509e7 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/PathInput.java @@ -60,7 +60,7 @@ public class PathInput extends AbstractTextComponent { } @Override - protected PathInputContext getThisContext(ComponentContext context) { + public PathInputContext getThisContext(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java index ce187512..859cc815 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/SingleItemSelector.java @@ -52,7 +52,7 @@ public class SingleItemSelector getThisContext(ComponentContext context) { + public SingleItemSelectorContext getThisContext(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java index d9512af7..9c965742 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/StringInput.java @@ -68,7 +68,7 @@ public class StringInput extends AbstractTextComponent context) { + public StringInputContext getThisContext(ComponentContext context) { if (context != null && currentContext == context) { return currentContext; } diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseConfirmationInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseConfirmationInput.java new file mode 100644 index 00000000..5367282c --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseConfirmationInput.java @@ -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 implements ConfirmationInputSpec { + + private String name; + private Boolean defaultValue; + private Function> renderer; + private List> preHandlers = new ArrayList<>(); + private List> postHandlers = new ArrayList<>(); + private boolean storeResult = true; + private String templateLocation; + private Function 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> renderer) { + this.renderer = renderer; + return this; + } + + @Override + public ConfirmationInputSpec template(String location) { + this.templateLocation = location; + return this; + } + + @Override + public ConfirmationInputSpec preHandler(Consumer handler) { + this.preHandlers.add(handler); + return this; + } + + @Override + public ConfirmationInputSpec postHandler(Consumer handler) { + this.postHandlers.add(handler); + return this; + } + + @Override + public ConfirmationInputSpec storeResult(boolean store) { + this.storeResult = store; + return this; + } + + @Override + public ConfirmationInputSpec next(Function 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> getRenderer() { + return renderer; + } + + public String getTemplateLocation() { + return templateLocation; + } + + public List> getPreHandlers() { + return preHandlers; + } + + public List> getPostHandlers() { + return postHandlers; + } + + public boolean isStoreResult() { + return storeResult; + } + + public Function getNext() { + return next; + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseInput.java new file mode 100644 index 00000000..6b00f130 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseInput.java @@ -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> implements Ordered, BaseInputSpec { + + 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; + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseInputSpec.java new file mode 100644 index 00000000..5d01f5ba --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseInputSpec.java @@ -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> { + + /** + * 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(); +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseMultiItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseMultiItemSelector.java new file mode 100644 index 00000000..a9c4a050 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseMultiItemSelector.java @@ -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 implements MultiItemSelectorSpec { + + private String name; + private List resultValues = new ArrayList<>(); + private ResultMode resultMode; + private List selectItems = new ArrayList<>(); + private Comparator> comparator; + private Function>, List> renderer; + private Integer maxItems; + private List>>> preHandlers = new ArrayList<>(); + private List>>> postHandlers = new ArrayList<>(); + private boolean storeResult = true; + private String templateLocation; + private Function>, 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 resultValues) { + this.resultValues.addAll(resultValues); + return this; + } + + @Override + public MultiItemSelectorSpec resultMode(ResultMode resultMode) { + this.resultMode = resultMode; + return this; + } + + @Override + public MultiItemSelectorSpec selectItems(List selectItems) { + this.selectItems = selectItems; + return this; + } + + @Override + public MultiItemSelectorSpec sort(Comparator> comparator) { + this.comparator = comparator; + return this; + } + + @Override + public MultiItemSelectorSpec renderer(Function>, List> 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>> handler) { + this.preHandlers.add(handler); + return this; + } + + @Override + public MultiItemSelectorSpec postHandler(Consumer>> handler) { + this.postHandlers.add(handler); + return this; + } + + @Override + public MultiItemSelectorSpec storeResult(boolean store) { + this.storeResult = store; + return this; + } + + @Override + public MultiItemSelectorSpec next( + Function>, 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 getResultValues() { + return resultValues; + } + + public ResultMode getResultMode() { + return resultMode; + } + + public List getSelectItems() { + return selectItems; + } + + public Comparator> getComparator() { + return comparator; + } + + public Function>, List> getRenderer() { + return renderer; + } + + public String getTemplateLocation() { + return templateLocation; + } + + public Integer getMaxItems() { + return maxItems; + } + + public List>>> getPreHandlers() { + return preHandlers; + } + + public List>>> getPostHandlers() { + return postHandlers; + } + + public boolean isStoreResult() { + return storeResult; + } + + public Function>, String> getNext() { + return next; + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BasePathInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BasePathInput.java new file mode 100644 index 00000000..8af01140 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BasePathInput.java @@ -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 implements PathInputSpec { + + private String name; + private String resultValue; + private ResultMode resultMode; + private String defaultValue; + private Function> renderer; + private List> preHandlers = new ArrayList<>(); + private List> postHandlers = new ArrayList<>(); + private boolean storeResult = true; + private String templateLocation; + private Function 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> renderer) { + this.renderer = renderer; + return this; + } + + @Override + public PathInputSpec template(String location) { + this.templateLocation = location; + return this; + } + + @Override + public PathInputSpec preHandler(Consumer handler) { + this.preHandlers.add(handler); + return this; + } + + @Override + public PathInputSpec postHandler(Consumer handler) { + this.postHandlers.add(handler); + return this; + } + + @Override + public PathInputSpec storeResult(boolean store) { + this.storeResult = store; + return this; + } + + @Override + public PathInputSpec next(Function 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> getRenderer() { + return renderer; + } + + public String getTemplateLocation() { + return templateLocation; + } + + public List> getPreHandlers() { + return preHandlers; + } + + public List> getPostHandlers() { + return postHandlers; + } + + public boolean isStoreResult() { + return storeResult; + } + + public Function getNext() { + return next; + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java new file mode 100644 index 00000000..0b0bebf9 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseSingleItemSelector.java @@ -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 implements SingleItemSelectorSpec { + + private String name; + private String resultValue; + private ResultMode resultMode; + private Map selectItems = new HashMap<>(); + private Comparator> comparator; + private Function>, List> renderer; + private Integer maxItems; + private List>>> preHandlers = new ArrayList<>(); + private List>>> postHandlers = new ArrayList<>(); + private boolean storeResult = true; + private String templateLocation; + private Function>, 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 selectItems) { + this.selectItems.putAll(selectItems); + return this; + } + + @Override + public SingleItemSelectorSpec sort(Comparator> comparator) { + this.comparator = comparator; + return this; + } + + @Override + public SingleItemSelectorSpec renderer(Function>, List> 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>> handler) { + this.preHandlers.add(handler); + return null; + } + + @Override + public SingleItemSelectorSpec postHandler(Consumer>> handler) { + this.postHandlers.add(handler); + return this; + } + + @Override + public SingleItemSelectorSpec storeResult(boolean store) { + this.storeResult = store; + return this; + } + + @Override + public SingleItemSelectorSpec next( + Function>, 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 getSelectItems() { + return selectItems; + } + + public Comparator> getComparator() { + return comparator; + } + + public Function>, List> getRenderer() { + return renderer; + } + + public String getTemplateLocation() { + return templateLocation; + } + + public Integer getMaxItems() { + return maxItems; + } + + public List>>> getPreHandlers() { + return preHandlers; + } + + public List>>> getPostHandlers() { + return postHandlers; + } + + public boolean isStoreResult() { + return storeResult; + } + + public Function>, String> getNext() { + return next; + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java new file mode 100644 index 00000000..14e16546 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/BaseStringInput.java @@ -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 implements StringInputSpec { + + private String name; + private String resultValue; + private ResultMode resultMode; + private String defaultValue; + private Character maskCharacter; + private Function> renderer; + private List> preHandlers = new ArrayList<>(); + private List> postHandlers = new ArrayList<>(); + private boolean storeResult = true; + private String templateLocation; + private Function 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> renderer) { + this.renderer = renderer; + return this; + } + + @Override + public StringInputSpec template(String location) { + this.templateLocation = location; + return this; + } + + @Override + public StringInputSpec preHandler(Consumer handler) { + this.preHandlers.add(handler); + return this; + } + + @Override + public StringInputSpec postHandler(Consumer handler) { + this.postHandlers.add(handler); + return this; + } + + @Override + public StringInputSpec storeResult(boolean store) { + this.storeResult = store; + return this; + } + + @Override + public StringInputSpec next(Function 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> getRenderer() { + return renderer; + } + + public String getTemplateLocation() { + return templateLocation; + } + + public List> getPreHandlers() { + return preHandlers; + } + + public List> getPostHandlers() { + return postHandlers; + } + + public boolean isStoreResult() { + return storeResult; + } + + public Function getNext() { + return next; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..e0184bb3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ComponentFlow.java @@ -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 stringInputs = new ArrayList<>(); + private final List pathInputs = new ArrayList<>(); + private final List confirmationInputs = new ArrayList<>(); + private final List singleItemSelectors = new ArrayList<>(); + private final List multiItemSelectors = new ArrayList<>(); + private final AtomicInteger order = new AtomicInteger(); + private final HashSet 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 stringInputs; + private final List pathInputs; + private final List confirmationInputs; + private final List singleInputs; + private final List multiInputs; + private final ResourceLoader resourceLoader; + private final TemplateExecutor templateExecutor; + + DefaultComponentFlow(Terminal terminal, ResourceLoader resourceLoader, TemplateExecutor templateExecutor, + List stringInputs, List pathInputs, List confirmationInputs, + List singleInputs, List 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 map = new HashMap<>(); + private Node first; + + OrderedInputOperationList(List 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 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 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 stringInputsStream() { + return stringInputs.stream().map(input -> { + StringInput selector = new StringInput(terminal, input.getName(), input.getDefaultValue()); + Function, 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 handler : input.getPreHandlers()) { + selector.addPreRunHandler(handler); + } + for (Consumer handler : input.getPostHandlers()) { + selector.addPostRunHandler(handler); + } + return selector.run(context); + }; + Function f1 = input.getNext(); + Function, Optional> f2 = context -> f1 != null + ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) + : Optional.empty(); + return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); + }); + } + + private Stream pathInputsStream() { + return pathInputs.stream().map(input -> { + PathInput selector = new PathInput(terminal, input.getName()); + Function, 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 handler : input.getPreHandlers()) { + selector.addPreRunHandler(handler); + } + for (Consumer handler : input.getPostHandlers()) { + selector.addPostRunHandler(handler); + } + return selector.run(context); + }; + Function f1 = input.getNext(); + Function, Optional> f2 = context -> f1 != null + ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) + : Optional.empty(); + return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); + }); + } + + private Stream confirmationInputsStream() { + return confirmationInputs.stream().map(input -> { + ConfirmationInput selector = new ConfirmationInput(terminal, input.getName(), input.getDefaultValue()); + Function, 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 handler : input.getPreHandlers()) { + selector.addPreRunHandler(handler); + } + for (Consumer handler : input.getPostHandlers()) { + selector.addPostRunHandler(handler); + } + return selector.run(context); + }; + Function f1 = input.getNext(); + Function, Optional> f2 = context -> f1 != null + ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) + : Optional.empty(); + return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); + }); + } + + private Stream singleItemSelectorsStream() { + return singleInputs.stream().map(input -> { + List> selectorItems = input.getSelectItems().entrySet().stream() + .map(e -> SelectorItem.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + SingleItemSelector> selector = new SingleItemSelector<>(terminal, + selectorItems, input.getName(), input.getComparator()); + Function, 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>> handler : input.getPreHandlers()) { + selector.addPreRunHandler(handler); + } + for (Consumer>> handler : input.getPostHandlers()) { + selector.addPostRunHandler(handler); + } + return selector.run(context); + }; + Function>, String> f1 = input.getNext(); + Function, Optional> f2 = context -> f1 != null + ? Optional.ofNullable(f1.apply(selector.getThisContext(context))) + : Optional.empty(); + return OrderedInputOperation.of(input.getId(), input.getOrder(), operation, f2); + }); + } + + private Stream multiItemSelectorsStream() { + return multiInputs.stream().map(input -> { + List> selectorItems = input.getSelectItems().stream() + .map(si -> SelectorItem.of(si.name(), si.item(), si.enabled())) + .collect(Collectors.toList()); + MultiItemSelector> selector = new MultiItemSelector<>(terminal, + selectorItems, input.getName(), input.getComparator()); + Function, 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>> handler : input.getPreHandlers()) { + selector.addPreRunHandler(handler); + } + for (Consumer>> handler : input.getPostHandlers()) { + selector.addPostRunHandler(handler); + } + return selector.run(context); + }; + Function>, String> f1 = input.getNext(); + Function, Optional> 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> operation; + private Function, Optional> next; + + @Override + public int getOrder() { + return order; + } + + public String getId() { + return id; + } + + public Function, ComponentContext> getOperation() { + return operation; + } + + public Function, Optional> getNext() { + return next; + } + + static OrderedInputOperation of(String id, int order, + Function, ComponentContext> operation, + Function, Optional> next) { + OrderedInputOperation oio = new OrderedInputOperation(); + oio.id = id; + oio.order = order; + oio.operation = operation; + oio.next = next; + return oio; + } + } +} diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java new file mode 100644 index 00000000..8f0f28e8 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ConfirmationInputSpec.java @@ -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 { + + /** + * 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> 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 handler); + + /** + * Adds a post-run context handler. + * + * @param handler the context handler + * @return a builder + */ + ConfirmationInputSpec postHandler(Consumer 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 next); + + /** + * Build and return parent builder. + * + * @return the parent builder + */ + Builder and(); +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultConfirmationInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultConfirmationInputSpec.java new file mode 100644 index 00000000..82d1f884 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultConfirmationInputSpec.java @@ -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); + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultMultiInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultMultiInputSpec.java new file mode 100644 index 00000000..dfeae5ea --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultMultiInputSpec.java @@ -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); + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultPathInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultPathInputSpec.java new file mode 100644 index 00000000..a117917f --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultPathInputSpec.java @@ -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); + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultSelectItem.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultSelectItem.java new file mode 100644 index 00000000..755f1f73 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultSelectItem.java @@ -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; + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultSingleInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultSingleInputSpec.java new file mode 100644 index 00000000..9d37a4d3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultSingleInputSpec.java @@ -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); + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultStringInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultStringInputSpec.java new file mode 100644 index 00000000..a254f006 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/DefaultStringInputSpec.java @@ -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); + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/MultiItemSelectorSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/MultiItemSelectorSpec.java new file mode 100644 index 00000000..db53ffb7 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/MultiItemSelectorSpec.java @@ -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{ + + /** + * 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 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 selectItems); + + /** + * Sets a {@link Comparator} for sorting items. + * + * @param comparator the item comparator + * @return a builder + */ + MultiItemSelectorSpec sort(Comparator> comparator); + + /** + * Sets a renderer function. + * + * @param renderer the renderer + * @return a builder + */ + MultiItemSelectorSpec renderer(Function>, List> 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>> handler); + + /** + * Adds a post-run context handler. + * + * @param handler the context handler + * @return a builder + */ + MultiItemSelectorSpec postHandler(Consumer>> 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>, String> next); + + /** + * Build and return parent builder. + * + * @return the parent builder + */ + Builder and(); +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/PathInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/PathInputSpec.java new file mode 100644 index 00000000..f37b182a --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/PathInputSpec.java @@ -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 { + + /** + * 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> 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 handler); + + /** + * Adds a post-run context handler. + * + * @param handler the context handler + * @return a builder + */ + PathInputSpec postHandler(Consumer 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 next); + + /** + * Build and return parent builder. + * + * @return the parent builder + */ + Builder and(); +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ResultMode.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ResultMode.java new file mode 100644 index 00000000..735b5178 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/ResultMode.java @@ -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 +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SelectItem.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SelectItem.java new file mode 100644 index 00000000..120b41b3 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SelectItem.java @@ -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); + } +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java new file mode 100644 index 00000000..5dfabdcc --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/SingleItemSelectorSpec.java @@ -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 { + + /** + * 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 selectItems); + + /** + * Sets a {@link Comparator} for sorting items. + * + * @param comparator the item comparator + * @return a builder + */ + SingleItemSelectorSpec sort(Comparator> comparator); + + /** + * Sets a renderer function. + * + * @param renderer the renderer + * @return a builder + */ + SingleItemSelectorSpec renderer(Function>, List> 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>> handler); + + /** + * Adds a post-run context handler. + * + * @param handler the context handler + * @return a builder + */ + SingleItemSelectorSpec postHandler(Consumer>> 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>, String> next); + + /** + * Build and return parent builder. + * + * @return the parent builder + */ + Builder and(); +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java new file mode 100644 index 00000000..15c3da34 --- /dev/null +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/flow/StringInputSpec.java @@ -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 { + + /** + * 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> 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 handler); + + /** + * Adds a post-run context handler. + * + * @param handler the context handler + * @return a builder + */ + StringInputSpec postHandler(Consumer 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 next); + + /** + * Build and return parent builder. + * + * @return the parent builder + */ + Builder and(); +} \ No newline at end of file diff --git a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java index 75c0418c..0e04ba63 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/component/support/AbstractComponent.java @@ -219,7 +219,7 @@ public abstract class AbstractComponent> implement * @param context the context * @return a component context */ - protected abstract T getThisContext(ComponentContext context); + public abstract T getThisContext(ComponentContext context); /** * Read input. diff --git a/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java new file mode 100644 index 00000000..18bd4386 --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/AbstractShellTests.java @@ -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 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; + } + } +} 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 new file mode 100644 index 00000000..0f06b96e --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/component/flow/ComponentFlowTests.java @@ -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 single1SelectItems = new HashMap<>(); + single1SelectItems.put("key1", "value1"); + 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()) + .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 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 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 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 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 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"); + } +} 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 new file mode 100644 index 00000000..bce8f6b8 --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/standard/ComponentFlowCommands.java @@ -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 single1SelectItems = new HashMap<>(); + single1SelectItems.put("key1", "value1"); + 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()) + .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 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(); + } +}