Basic UI components
- Add first preliminary model of building and working with higher level components. - Components for text input, path input, single selector and multi selector. - Components renderings are based on ANTLR ST templates while there is support for building rendering output manually. - Add these into sample. - This is a base of additional work what goes to these components and concepts around it. - Relates #360
This commit is contained in:
6
pom.xml
6
pom.xml
@@ -25,6 +25,7 @@
|
||||
<jline.version>3.21.0</jline.version>
|
||||
<jcommander.version>1.81</jcommander.version>
|
||||
<antlr-st4.version>4.3.1</antlr-st4.version>
|
||||
<jimfs.version>1.2</jimfs.version>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
@@ -110,6 +111,11 @@
|
||||
<artifactId>ST4</artifactId>
|
||||
<version>${antlr-st4.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.jimfs</groupId>
|
||||
<artifactId>jimfs</artifactId>
|
||||
<version>${jimfs.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.jimfs</groupId>
|
||||
<artifactId>jimfs</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractSelectorComponent;
|
||||
import org.springframework.shell.component.support.Enableable;
|
||||
import org.springframework.shell.component.support.Itemable;
|
||||
import org.springframework.shell.component.support.Matchable;
|
||||
import org.springframework.shell.component.support.Nameable;
|
||||
import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext;
|
||||
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
|
||||
|
||||
/**
|
||||
* Component able to pick multiple items.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class MultiItemSelector<T, I extends Nameable & Matchable & Enableable & Itemable<T>>
|
||||
extends AbstractSelectorComponent<T, MultiItemSelectorContext<T, I>, I> {
|
||||
|
||||
private MultiItemSelectorContext<T, I> currentContext;
|
||||
|
||||
public MultiItemSelector(Terminal terminal, List<I> items, String name, Comparator<I> comparator) {
|
||||
super(terminal, name, items, false, comparator);
|
||||
setRenderer(new DefaultRenderer());
|
||||
setTemplateLocation("classpath:org/springframework/shell/component/multi-item-selector-default.stg");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MultiItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
|
||||
if (context != null && currentContext == context) {
|
||||
return currentContext;
|
||||
}
|
||||
currentContext = MultiItemSelectorContext.empty(getItemMapper());
|
||||
currentContext.setName(name);
|
||||
if (currentContext.getItems() == null) {
|
||||
currentContext.setItems(getItems());
|
||||
}
|
||||
context.stream().forEach(e -> {
|
||||
currentContext.put(e.getKey(), e.getValue());
|
||||
});
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MultiItemSelectorContext<T, I> runInternal(MultiItemSelectorContext<T, I> context) {
|
||||
super.runInternal(context);
|
||||
loop(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context {@link MultiItemSelector}.
|
||||
*/
|
||||
public interface MultiItemSelectorContext<T, I extends Nameable & Matchable & Itemable<T>>
|
||||
extends SelectorComponentContext<T, I, MultiItemSelectorContext<T, I>> {
|
||||
|
||||
/**
|
||||
* Gets a values.
|
||||
*
|
||||
* @return a values
|
||||
*/
|
||||
List<String> getValues();
|
||||
|
||||
/**
|
||||
* Creates an empty {@link MultiItemSelectorContext}.
|
||||
*
|
||||
* @return empty context
|
||||
*/
|
||||
static <T, I extends Nameable & Matchable & Itemable<T>> MultiItemSelectorContext<T, I> empty() {
|
||||
return new DefaultMultiItemSelectorContext<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link MultiItemSelectorContext}.
|
||||
*
|
||||
* @return context
|
||||
*/
|
||||
static <T, I extends Nameable & Matchable & Itemable<T>> MultiItemSelectorContext<T, I> empty(Function<T, String> itemMapper) {
|
||||
return new DefaultMultiItemSelectorContext<>(itemMapper);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultMultiItemSelectorContext<T, I extends Nameable & Matchable & Itemable<T>> extends
|
||||
BaseSelectorComponentContext<T, I, MultiItemSelectorContext<T, I>> implements MultiItemSelectorContext<T, I> {
|
||||
|
||||
private Function<T, String> itemMapper = item -> item.toString();
|
||||
|
||||
DefaultMultiItemSelectorContext() {
|
||||
}
|
||||
|
||||
DefaultMultiItemSelectorContext(Function<T, String> itemMapper) {
|
||||
this.itemMapper = itemMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getValues() {
|
||||
if (getResultItems() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return getResultItems().stream()
|
||||
.map(i -> i.getItem())
|
||||
.map(i -> itemMapper.apply(i))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
attributes.put("values", getValues());
|
||||
List<Map<String, Object>> rows = getItemStateView().stream()
|
||||
.map(is -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", is.getName());
|
||||
map.put("selected", is.isSelected());
|
||||
map.put("onrow", getCursorRow().intValue() == is.getIndex());
|
||||
map.put("enabled", is.isEnabled());
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
attributes.put("rows", rows);
|
||||
// finally wrap it into 'model' as that's what
|
||||
// we expect in stg template.
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("model", attributes);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultRenderer implements Function<MultiItemSelectorContext<T, I>, List<AttributedString>> {
|
||||
|
||||
@Override
|
||||
public List<AttributedString> apply(MultiItemSelectorContext<T, I> context) {
|
||||
return renderTemplateResource(context.toTemplateModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.shell.component.PathInput.PathInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
|
||||
import org.springframework.util.StringUtils;;
|
||||
|
||||
/**
|
||||
* Component for a simple path input.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
|
||||
|
||||
private PathInputContext currentContext;
|
||||
private Function<String, Path> pathProvider = (path) -> Paths.get(path);
|
||||
|
||||
public PathInput(Terminal terminal) {
|
||||
this(terminal, null);
|
||||
}
|
||||
|
||||
public PathInput(Terminal terminal, String name) {
|
||||
this(terminal, name, null);
|
||||
}
|
||||
|
||||
public PathInput(Terminal terminal, String name, Function<PathInputContext, List<AttributedString>> renderer) {
|
||||
super(terminal, name, null);
|
||||
setRenderer(renderer != null ? renderer : new DefaultRenderer());
|
||||
setTemplateLocation("classpath:org/springframework/shell/component/path-input-default.stg");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PathInputContext getThisContext(ComponentContext<?> context) {
|
||||
if (context != null && currentContext == context) {
|
||||
return currentContext;
|
||||
}
|
||||
currentContext = PathInputContext.empty();
|
||||
currentContext.setName(getName());
|
||||
context.stream().forEach(e -> {
|
||||
currentContext.put(e.getKey(), e.getValue());
|
||||
});
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, PathInputContext context) {
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_CHAR:
|
||||
String lastBinding = bindingReader.getLastBinding();
|
||||
input = context.getInput();
|
||||
if (input == null) {
|
||||
input = lastBinding;
|
||||
}
|
||||
else {
|
||||
input = input + lastBinding;
|
||||
}
|
||||
context.setInput(input);
|
||||
checkPath(input, context);
|
||||
break;
|
||||
case OPERATION_BACKSPACE:
|
||||
input = context.getInput();
|
||||
if (StringUtils.hasLength(input)) {
|
||||
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
|
||||
}
|
||||
context.setInput(input);
|
||||
checkPath(input, context);
|
||||
break;
|
||||
case OPERATION_EXIT:
|
||||
if (StringUtils.hasText(context.getInput())) {
|
||||
context.setResultValue(Paths.get(context.getInput()));
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a path provider.
|
||||
*
|
||||
* @param pathProvider the path provider
|
||||
*/
|
||||
public void setPathProvider(Function<String, Path> pathProvider) {
|
||||
this.pathProvider = pathProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a {@link Path} from a given raw {@code path}.
|
||||
*
|
||||
* @param path the raw path
|
||||
* @return a resolved path
|
||||
*/
|
||||
protected Path resolvePath(String path) {
|
||||
return this.pathProvider.apply(path);
|
||||
}
|
||||
|
||||
private void checkPath(String path, PathInputContext context) {
|
||||
if (!StringUtils.hasText(path)) {
|
||||
context.setMessage(null);
|
||||
return;
|
||||
}
|
||||
Path p = resolvePath(path);
|
||||
boolean isDirectory = Files.isDirectory(p);
|
||||
if (isDirectory) {
|
||||
context.setMessage("Directory exists", MessageLevel.ERROR);
|
||||
}
|
||||
else {
|
||||
context.setMessage("Path ok", MessageLevel.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
public interface PathInputContext extends TextComponentContext<Path, PathInputContext> {
|
||||
|
||||
/**
|
||||
* Gets an empty {@link PathInputContext}.
|
||||
*
|
||||
* @return empty path input context
|
||||
*/
|
||||
public static PathInputContext empty() {
|
||||
return new DefaultPathInputContext();
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultPathInputContext extends BaseTextComponentContext<Path, PathInputContext>
|
||||
implements PathInputContext {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("model", attributes);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultRenderer implements Function<PathInputContext, List<AttributedString>> {
|
||||
|
||||
@Override
|
||||
public List<AttributedString> apply(PathInputContext context) {
|
||||
return renderTemplateResource(context.toTemplateModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractSelectorComponent;
|
||||
import org.springframework.shell.component.support.Enableable;
|
||||
import org.springframework.shell.component.support.Itemable;
|
||||
import org.springframework.shell.component.support.Matchable;
|
||||
import org.springframework.shell.component.support.Nameable;
|
||||
import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext;
|
||||
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
|
||||
|
||||
/**
|
||||
* Component able to pick single item.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class SingleItemSelector<T, I extends Nameable & Matchable & Enableable & Itemable<T>>
|
||||
extends AbstractSelectorComponent<T, SingleItemSelectorContext<T, I>, I> {
|
||||
|
||||
private SingleItemSelectorContext<T, I> currentContext;
|
||||
|
||||
public SingleItemSelector(Terminal terminal, List<I> items, String name, Comparator<I> comparator) {
|
||||
super(terminal, name, items, true, comparator);
|
||||
setRenderer(new DefaultRenderer());
|
||||
setTemplateLocation("classpath:org/springframework/shell/component/single-item-selector-default.stg");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SingleItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
|
||||
if (context != null && currentContext == context) {
|
||||
return currentContext;
|
||||
}
|
||||
currentContext = SingleItemSelectorContext.empty(getItemMapper());
|
||||
currentContext.setName(name);
|
||||
if (currentContext.getItems() == null) {
|
||||
currentContext.setItems(getItems());
|
||||
}
|
||||
context.stream().forEach(e -> {
|
||||
currentContext.put(e.getKey(), e.getValue());
|
||||
});
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SingleItemSelectorContext<T, I> runInternal(SingleItemSelectorContext<T, I> context) {
|
||||
super.runInternal(context);
|
||||
loop(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context {@link SingleItemSelector}.
|
||||
*/
|
||||
public interface SingleItemSelectorContext<T, I extends Nameable & Matchable & Itemable<T>>
|
||||
extends SelectorComponentContext<T, I, SingleItemSelectorContext<T, I>> {
|
||||
|
||||
/**
|
||||
* Gets a result item.
|
||||
*
|
||||
* @return a result item
|
||||
*/
|
||||
Optional<I> getResultItem();
|
||||
|
||||
/**
|
||||
* Gets a value.
|
||||
*
|
||||
* @return a value
|
||||
*/
|
||||
Optional<String> getValue();
|
||||
|
||||
/**
|
||||
* Creates an empty {@link SingleItemSelectorContext}.
|
||||
*
|
||||
* @return empty context
|
||||
*/
|
||||
static <C, I extends Nameable & Matchable & Itemable<C>> SingleItemSelectorContext<C, I> empty() {
|
||||
return new DefaultSingleItemSelectorContext<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SingleItemSelectorContext}.
|
||||
*
|
||||
* @return context
|
||||
*/
|
||||
static <C, I extends Nameable & Matchable & Itemable<C>> SingleItemSelectorContext<C, I> empty(Function<C, String> itemMapper) {
|
||||
return new DefaultSingleItemSelectorContext<>(itemMapper);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultSingleItemSelectorContext<T, I extends Nameable & Matchable & Itemable<T>> extends
|
||||
BaseSelectorComponentContext<T, I, SingleItemSelectorContext<T, I>> implements SingleItemSelectorContext<T, I> {
|
||||
|
||||
private Function<T, String> itemMapper = item -> item.toString();
|
||||
|
||||
DefaultSingleItemSelectorContext() {
|
||||
}
|
||||
|
||||
DefaultSingleItemSelectorContext(Function<T, String> itemMapper) {
|
||||
this.itemMapper = itemMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<I> getResultItem() {
|
||||
if (getResultItems() == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return getResultItems().stream().findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> getValue() {
|
||||
return getResultItem().map(item -> itemMapper.apply(item.getItem()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
getValue().ifPresent(value -> {
|
||||
attributes.put("value", value);
|
||||
});
|
||||
List<Map<String, Object>> rows = getItemStateView().stream()
|
||||
.map(is -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", is.getName());
|
||||
map.put("selected", getCursorRow().intValue() == is.getIndex());
|
||||
return map;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
attributes.put("rows", rows);
|
||||
// finally wrap it into 'model' as that's what
|
||||
// we expect in stg template.
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("model", attributes);
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultSingleItemSelectorContext [super=" + super.toString() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultRenderer implements Function<SingleItemSelectorContext<T, I>, List<AttributedString>> {
|
||||
|
||||
@Override
|
||||
public List<AttributedString> apply(SingleItemSelectorContext<T, I> context) {
|
||||
return renderTemplateResource(context.toTemplateModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
|
||||
import org.springframework.shell.component.StringInput.StringInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Component for a simple string input.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class StringInput extends AbstractTextComponent<String, StringInputContext> {
|
||||
|
||||
private final String defaultValue;
|
||||
private StringInputContext currentContext;
|
||||
|
||||
public StringInput(Terminal terminal) {
|
||||
this(terminal, null, null, null);
|
||||
}
|
||||
|
||||
public StringInput(Terminal terminal, String name, String defaultValue) {
|
||||
this(terminal, name, defaultValue, null);
|
||||
}
|
||||
|
||||
public StringInput(Terminal terminal, String name, String defaultValue,
|
||||
Function<StringInputContext, List<AttributedString>> renderer) {
|
||||
super(terminal, name, null);
|
||||
setRenderer(renderer != null ? renderer : new DefaultRenderer());
|
||||
setTemplateLocation("classpath:org/springframework/shell/component/string-input-default.stg");
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StringInputContext getThisContext(ComponentContext<?> context) {
|
||||
if (context != null && currentContext == context) {
|
||||
return currentContext;
|
||||
}
|
||||
currentContext = StringInputContext.of(defaultValue);
|
||||
currentContext.setName(getName());
|
||||
context.stream().forEach(e -> {
|
||||
currentContext.put(e.getKey(), e.getValue());
|
||||
});
|
||||
return currentContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, StringInputContext context) {
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_CHAR:
|
||||
String lastBinding = bindingReader.getLastBinding();
|
||||
input = context.getInput();
|
||||
if (input == null) {
|
||||
input = lastBinding;
|
||||
}
|
||||
else {
|
||||
input = input + lastBinding;
|
||||
}
|
||||
context.setInput(input);
|
||||
break;
|
||||
case OPERATION_BACKSPACE:
|
||||
input = context.getInput();
|
||||
if (StringUtils.hasLength(input)) {
|
||||
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
|
||||
}
|
||||
context.setInput(input);
|
||||
break;
|
||||
case OPERATION_EXIT:
|
||||
if (StringUtils.hasText(context.getInput())) {
|
||||
context.setResultValue(context.getInput());
|
||||
}
|
||||
else if (context.getDefaultValue() != null) {
|
||||
context.setResultValue(context.getDefaultValue());
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface StringInputContext extends TextComponentContext<String, StringInputContext> {
|
||||
|
||||
/**
|
||||
* Gets a default value.
|
||||
*
|
||||
* @return a default value
|
||||
*/
|
||||
String getDefaultValue();
|
||||
|
||||
/**
|
||||
* Sets a default value.
|
||||
*
|
||||
* @param defaultValue the default value
|
||||
*/
|
||||
void setDefaultValue(String defaultValue);
|
||||
|
||||
/**
|
||||
* Gets an empty {@link StringInputContext}.
|
||||
*
|
||||
* @return empty path input context
|
||||
*/
|
||||
public static StringInputContext empty() {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an {@link StringInputContext}.
|
||||
*
|
||||
* @return path input context
|
||||
*/
|
||||
public static StringInputContext of(String defaultValue) {
|
||||
return new DefaultStringInputContext(defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultStringInputContext extends BaseTextComponentContext<String, StringInputContext>
|
||||
implements StringInputContext {
|
||||
|
||||
private String defaultValue;
|
||||
|
||||
public DefaultStringInputContext(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultValue(String defaultValue) {
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
attributes.put("defaultValue", getDefaultValue() != null ? getDefaultValue() : null);
|
||||
Map<String, Object> model = new HashMap<>();
|
||||
model.put("model", attributes);
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private class DefaultRenderer implements Function<StringInputContext, List<AttributedString>> {
|
||||
|
||||
@Override
|
||||
public List<AttributedString> apply(StringInputContext context) {
|
||||
return renderTemplateResource(context.toTemplateModel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link ComponentContext}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class BaseComponentContext<C extends ComponentContext<C>> extends LinkedHashMap<Object, Object>
|
||||
implements ComponentContext<C> {
|
||||
|
||||
@Override
|
||||
public Object get(Object key) {
|
||||
Object o = super.get(key);
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
throw new NoSuchElementException("Context does not contain key: " + key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T get(Object key, Class<T> type) {
|
||||
Object value = get(key);
|
||||
if (!type.isAssignableFrom(value.getClass())) {
|
||||
throw new IllegalArgumentException("Incorrect type specified for key '" +
|
||||
key + "'. Expected [" + type + "] but actual type is [" + value.getClass() + "]");
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentContext<C> put(Object key, Object value) {
|
||||
super.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<java.util.Map.Entry<Object, Object>> stream() {
|
||||
return entrySet().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
// hardcoding enclosed map values into 'rawValues'
|
||||
// as it may contain anything
|
||||
attributes.put("rawValues", this);
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.shell.component.context;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Contract for base component context having access to basic key/value pairs.
|
||||
* This is a base context which components can extend to provide their own
|
||||
* component spesific contexts.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public interface ComponentContext<C extends ComponentContext<C>> {
|
||||
|
||||
/**
|
||||
* Gets an empty context.
|
||||
*
|
||||
* @param <C> the type of context
|
||||
* @return empty context
|
||||
*/
|
||||
static <C extends ComponentContext<C>> ComponentContext<C> empty() {
|
||||
return new BaseComponentContext<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from a context.
|
||||
*
|
||||
* @param <T> the type of context
|
||||
* @param key the key
|
||||
* @return a value
|
||||
*/
|
||||
<T> T get(Object key);
|
||||
|
||||
/**
|
||||
* Gets a value from a context with a given type to get cast to.
|
||||
*
|
||||
* @param <T> the type of context
|
||||
* @param key the key
|
||||
* @param type the class type
|
||||
* @return a value
|
||||
*/
|
||||
<T> T get(Object key, Class<T> type);
|
||||
|
||||
/**
|
||||
* Put an entry into a context.
|
||||
*
|
||||
* @param key the entry key
|
||||
* @param value the entry value
|
||||
* @return a context
|
||||
*/
|
||||
ComponentContext<C> put(Object key, Object value);
|
||||
|
||||
/**
|
||||
* Stream key/value pairs from this {@link ComponentContext}
|
||||
*
|
||||
* @return a {@link Stream} of key/value pairs held by this context
|
||||
*/
|
||||
Stream<Map.Entry<Object, Object>> stream();
|
||||
|
||||
/**
|
||||
* Gets context values as a map. Every context implementation can
|
||||
* do their own model as essentially what matter is a one coming
|
||||
* out from a last child which is one most likely to feed into
|
||||
* a template engine.
|
||||
*
|
||||
* @return map of context values
|
||||
*/
|
||||
Map<String, Object> toTemplateModel();
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Attributes;
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.Display;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.style.TemplateExecutor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base class for components.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractComponent<T extends ComponentContext<T>> implements ResourceLoaderAware {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(AbstractComponent.class);
|
||||
public final static String OPERATION_EXIT = "EXIT";
|
||||
public final static String OPERATION_BACKSPACE = "BACKSPACE";
|
||||
public final static String OPERATION_CHAR = "CHAR";
|
||||
public final static String OPERATION_SELECT = "SELECT";
|
||||
public final static String OPERATION_DOWN = "DOWN";
|
||||
public final static String OPERATION_UP = "UP";
|
||||
|
||||
private final Terminal terminal;
|
||||
private final BindingReader bindingReader;
|
||||
private final KeyMap<String> keyMap = new KeyMap<>();
|
||||
private final List<Consumer<T>> preRunHandlers = new ArrayList<>();
|
||||
private final List<Consumer<T>> postRunHandlers = new ArrayList<>();
|
||||
private Function<T, List<AttributedString>> renderer;
|
||||
private boolean printResults = true;
|
||||
private String templateLocation;
|
||||
private TemplateExecutor templateExecutor;
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
public AbstractComponent(Terminal terminal) {
|
||||
Assert.notNull(terminal, "terminal must be set");
|
||||
this.terminal = terminal;
|
||||
this.bindingReader = new BindingReader(terminal.reader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a {@link Terminal}.
|
||||
*
|
||||
* @return a terminal
|
||||
*/
|
||||
public Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a display renderer.
|
||||
*
|
||||
* @param renderer the display renderer function
|
||||
*/
|
||||
public void setRenderer(Function<T, List<AttributedString>> renderer) {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render to be shows content of a display with set display renderer using a
|
||||
* given context.
|
||||
*
|
||||
* @param context the context
|
||||
* @return list of attributed strings
|
||||
*/
|
||||
public List<AttributedString> render(T context) {
|
||||
log.debug("Rendering with context [{}] as class [{}] in [{}]", context, context.getClass(), this);
|
||||
return renderer.apply(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a pre-run handler.
|
||||
*
|
||||
* @param handler the handler
|
||||
*/
|
||||
public void addPreRunHandler(Consumer<T> handler) {
|
||||
this.preRunHandlers.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a post-run handler.
|
||||
*
|
||||
* @param handler the handler
|
||||
*/
|
||||
public void addPostRunHandler(Consumer<T> handler) {
|
||||
this.postRunHandlers.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if results should be printed into a console, Defaults to {@code true}.
|
||||
*
|
||||
* @param printResults flag setting if results are printed
|
||||
*/
|
||||
public void setPrintResults(boolean printResults) {
|
||||
this.printResults = printResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a component logic with a given context and returns updated context.
|
||||
*
|
||||
* @param context the context
|
||||
* @return a context
|
||||
*/
|
||||
public final T run(ComponentContext<?> context) {
|
||||
bindKeyMap(keyMap);
|
||||
context = runPreRunHandlers(getThisContext(context));
|
||||
T run = runInternal(getThisContext(context));
|
||||
context = runPostRunHandlers(getThisContext(context));
|
||||
if (printResults) {
|
||||
printResults(context);
|
||||
}
|
||||
return run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a template executor.
|
||||
*
|
||||
* @return a template executor
|
||||
*/
|
||||
public TemplateExecutor getTemplateExecutor() {
|
||||
return templateExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a template executor.
|
||||
*
|
||||
* @param templateExecutor the template executor
|
||||
*/
|
||||
public void setTemplateExecutor(TemplateExecutor templateExecutor) {
|
||||
this.templateExecutor = templateExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a template location.
|
||||
*
|
||||
* @param templateLocation the template location
|
||||
*/
|
||||
public void setTemplateLocation(String templateLocation) {
|
||||
this.templateLocation = templateLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a given template with attributes.
|
||||
*
|
||||
* @param attributes the attributes
|
||||
* @return rendered content as attributed strings
|
||||
*/
|
||||
protected List<AttributedString> renderTemplateResource(Map<String, Object> attributes) {
|
||||
String templateResource = resourceAsString(resourceLoader.getResource(templateLocation));
|
||||
log.debug("Rendering template: {}", templateResource);
|
||||
log.debug("Rendering template attributes: {}", attributes);
|
||||
AttributedString rendered;
|
||||
if (templateLocation.endsWith(".stg")) {
|
||||
rendered = templateExecutor.renderGroup(templateResource, attributes);
|
||||
}
|
||||
else {
|
||||
rendered = templateExecutor.render(templateResource, attributes);
|
||||
}
|
||||
log.debug("Template executor result: [{}]", rendered);
|
||||
List<AttributedString> rows = rendered.columnSplitLength(Integer.MAX_VALUE);
|
||||
// remove last if empty as columnsplit adds it
|
||||
int lastIndex = rows.size() - 1;
|
||||
if (lastIndex > 0 && rows.get(lastIndex).length() == 0) {
|
||||
rows.remove(lastIndex);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a real component context using common this trick.
|
||||
*
|
||||
* @param context the context
|
||||
* @return a component context
|
||||
*/
|
||||
protected abstract T getThisContext(ComponentContext<?> context);
|
||||
|
||||
/**
|
||||
* Read input.
|
||||
*
|
||||
* @param bindingReader the binding reader
|
||||
* @param keyMap the key map
|
||||
* @param context the context
|
||||
* @return true if read is complete, false to stop
|
||||
*/
|
||||
protected abstract boolean read(BindingReader bindingReader, KeyMap<String> keyMap, T context);
|
||||
|
||||
/**
|
||||
* Run internal logic called from public run method.
|
||||
*
|
||||
* @param context the context
|
||||
* @return a context
|
||||
*/
|
||||
protected abstract T runInternal(T context);
|
||||
|
||||
/**
|
||||
* Bind key map.
|
||||
*
|
||||
* @param keyMap
|
||||
*/
|
||||
protected abstract void bindKeyMap(KeyMap<String> keyMap);
|
||||
|
||||
/**
|
||||
* Enter into read loop. This should be called from a component.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
protected void loop(ComponentContext<?> context) {
|
||||
Display display = new Display(terminal, false);
|
||||
Attributes attr = terminal.enterRawMode();
|
||||
Size size = new Size();
|
||||
|
||||
try {
|
||||
terminal.puts(Capability.keypad_xmit);
|
||||
terminal.puts(Capability.cursor_invisible);
|
||||
terminal.writer().flush();
|
||||
size.copy(terminal.getSize());
|
||||
display.clear();
|
||||
display.reset();
|
||||
|
||||
while (true) {
|
||||
display.resize(size.getRows(), size.getColumns());
|
||||
display.update(render(getThisContext(context)), 0);
|
||||
boolean exit = read(bindingReader, keyMap, getThisContext(context));
|
||||
if (exit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
terminal.setAttributes(attr);
|
||||
terminal.puts(Capability.keypad_local);
|
||||
terminal.puts(Capability.cursor_visible);
|
||||
display.update(Collections.emptyList(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run pre-run handlers
|
||||
*
|
||||
* @param context the context
|
||||
* @return a context
|
||||
*/
|
||||
protected T runPreRunHandlers(T context) {
|
||||
this.preRunHandlers.stream().forEach(c -> c.accept(context));
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run post-run handlers
|
||||
*
|
||||
* @param context the context
|
||||
* @return a context
|
||||
*/
|
||||
protected T runPostRunHandlers(T context) {
|
||||
this.postRunHandlers.stream().forEach(c -> c.accept(context));
|
||||
return context;
|
||||
}
|
||||
|
||||
private void printResults(ComponentContext<?> context) {
|
||||
log.debug("About to write result with incoming context [{}] as class [{}] in [{}]", context, context.getClass(),
|
||||
this);
|
||||
String out = render(getThisContext(context)).stream()
|
||||
.map(as -> as.toAnsi())
|
||||
.collect(Collectors.joining("\n"));
|
||||
log.debug("Writing result [{}] in [{}]", out, this);
|
||||
if (StringUtils.hasText(out)) {
|
||||
terminal.writer().println(out);
|
||||
terminal.writer().flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static String resourceAsString(Resource resource) {
|
||||
try (Reader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return FileCopyUtils.copyToString(reader);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jline.keymap.BindingReader;
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
|
||||
import org.springframework.shell.component.context.BaseComponentContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.jline.keymap.KeyMap.ctrl;
|
||||
import static org.jline.keymap.KeyMap.del;
|
||||
import static org.jline.keymap.KeyMap.key;
|
||||
|
||||
/**
|
||||
* Base component for selectors which provide selectable lists.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractSelectorComponent<T, C extends SelectorComponentContext<T, I, C>, I extends Nameable & Matchable & Enableable & Itemable<T>>
|
||||
extends AbstractComponent<C> {
|
||||
|
||||
protected final String name;
|
||||
private final List<I> items;
|
||||
private Comparator<I> comparator = (o1, o2) -> 0;
|
||||
private boolean exitSelects;
|
||||
private int maxItems = 5;
|
||||
private Function<T, String> itemMapper = item -> item.toString();
|
||||
private boolean stale = false;
|
||||
private AtomicInteger start = new AtomicInteger(0);
|
||||
private AtomicInteger pos = new AtomicInteger(0);
|
||||
|
||||
public AbstractSelectorComponent(Terminal terminal, String name, List<I> items, boolean exitSelects,
|
||||
Comparator<I> comparator) {
|
||||
super(terminal);
|
||||
this.name = name;
|
||||
this.items = items;
|
||||
this.exitSelects = exitSelects;
|
||||
if (comparator != null) {
|
||||
this.comparator = comparator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set max items to show.
|
||||
*
|
||||
* @param maxItems max items
|
||||
*/
|
||||
public void setMaxItems(int maxItems) {
|
||||
Assert.state(maxItems > 0 || maxItems < 33, "maxItems has to be between 1 and 32");
|
||||
this.maxItems = maxItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an item mapper.
|
||||
*
|
||||
* @param itemMapper the item mapper
|
||||
*/
|
||||
public void setItemMapper(Function<T, String> itemMapper) {
|
||||
Assert.notNull(itemMapper, "itemMapper cannot be null");
|
||||
this.itemMapper = itemMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an item mapper.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Function<T, String> getItemMapper() {
|
||||
return itemMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets items.
|
||||
*
|
||||
* @return a list of items
|
||||
*/
|
||||
protected List<I> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bindKeyMap(KeyMap<String> keyMap) {
|
||||
keyMap.bind(OPERATION_SELECT, " ");
|
||||
keyMap.bind(OPERATION_DOWN, ctrl('E'), key(getTerminal(), Capability.key_down));
|
||||
keyMap.bind(OPERATION_UP, ctrl('Y'), key(getTerminal(), Capability.key_up));
|
||||
keyMap.bind(OPERATION_EXIT, "\r");
|
||||
keyMap.bind(OPERATION_BACKSPACE, del(), key(getTerminal(), Capability.key_backspace));
|
||||
// skip 32 - SPACE, 127 - DEL
|
||||
for (char i = 33; i < KeyMap.KEYMAP_LENGTH - 1; i++) {
|
||||
keyMap.bind(OPERATION_CHAR, Character.toString(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected C runInternal(C context) {
|
||||
C thisContext = getThisContext(context);
|
||||
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
|
||||
List<ItemState<I>> itemStateView = buildItemStateView.items;
|
||||
thisContext.setItemStateView(itemStateView);
|
||||
thisContext.setCursorRow(start.get() + pos.get());
|
||||
return thisContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, C context) {
|
||||
|
||||
if (stale) {
|
||||
start.set(0);
|
||||
pos.set(0);
|
||||
stale = false;
|
||||
}
|
||||
C thisContext = getThisContext(context);
|
||||
ItemStateViewProjection buildItemStateView = buildItemStateView(start.get(), thisContext);
|
||||
List<ItemState<I>> itemStateView = buildItemStateView.items;
|
||||
String operation = bindingReader.readBinding(keyMap);
|
||||
String input;
|
||||
switch (operation) {
|
||||
case OPERATION_SELECT:
|
||||
if (!exitSelects) {
|
||||
itemStateView.forEach(i -> {
|
||||
if (i.index == start.get() + pos.get() && i.enabled) {
|
||||
i.selected = !i.selected;
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case OPERATION_DOWN:
|
||||
if (start.get() + pos.get() + 1 < itemStateView.size()) {
|
||||
pos.incrementAndGet();
|
||||
}
|
||||
else if (start.get() + pos.get() + 1 >= buildItemStateView.total) {
|
||||
start.set(0);
|
||||
pos.set(0);
|
||||
}
|
||||
else {
|
||||
start.incrementAndGet();
|
||||
}
|
||||
break;
|
||||
case OPERATION_UP:
|
||||
if (start.get() > 0 && pos.get() == 0) {
|
||||
start.decrementAndGet();
|
||||
}
|
||||
else if (start.get() + pos.get() >= itemStateView.size()) {
|
||||
pos.decrementAndGet();
|
||||
}
|
||||
else if (start.get() + pos.get() <= 0) {
|
||||
start.set(buildItemStateView.total - Math.min(maxItems, itemStateView.size()));
|
||||
pos.set(itemStateView.size() - 1);
|
||||
}
|
||||
else {
|
||||
pos.decrementAndGet();
|
||||
}
|
||||
break;
|
||||
case OPERATION_CHAR:
|
||||
String lastBinding = bindingReader.getLastBinding();
|
||||
input = thisContext.getInput();
|
||||
if (input == null) {
|
||||
input = lastBinding;
|
||||
}
|
||||
else {
|
||||
input = input + lastBinding;
|
||||
}
|
||||
thisContext.setInput(input);
|
||||
|
||||
stale = true;
|
||||
break;
|
||||
case OPERATION_BACKSPACE:
|
||||
input = thisContext.getInput();
|
||||
if (StringUtils.hasLength(input)) {
|
||||
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
|
||||
}
|
||||
thisContext.setInput(input);
|
||||
break;
|
||||
case OPERATION_EXIT:
|
||||
if (exitSelects) {
|
||||
if (itemStateView.size() == 0) {
|
||||
// filter shows nothing, prevent exit
|
||||
break;
|
||||
}
|
||||
itemStateView.forEach(i -> {
|
||||
if (i.index == start.get() + pos.get()) {
|
||||
i.selected = !i.selected;
|
||||
}
|
||||
});
|
||||
}
|
||||
List<I> values = thisContext.getItemStates().stream()
|
||||
.filter(i -> i.selected)
|
||||
.map(i -> i.item)
|
||||
.collect(Collectors.toList());
|
||||
thisContext.setResultItems(values);
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
thisContext.setCursorRow(start.get() + pos.get());
|
||||
buildItemStateView = buildItemStateView(start.get(), thisContext);
|
||||
thisContext.setItemStateView(buildItemStateView.items);
|
||||
return false;
|
||||
}
|
||||
|
||||
private ItemStateViewProjection buildItemStateView(int skip, SelectorComponentContext<T, I, ?> context) {
|
||||
List<ItemState<I>> itemStates = context.getItemStates();
|
||||
if (itemStates == null) {
|
||||
AtomicInteger index = new AtomicInteger(0);
|
||||
itemStates = context.getItems().stream()
|
||||
.sorted(comparator)
|
||||
.map(item -> ItemState.of(item, item.getName(), index.getAndIncrement(), item.isEnabled()))
|
||||
.collect(Collectors.toList());
|
||||
context.setItemStates(itemStates);
|
||||
}
|
||||
AtomicInteger reindex = new AtomicInteger(0);
|
||||
List<ItemState<I>> filtered = itemStates.stream()
|
||||
.filter(i -> {
|
||||
return i.matches(context.getInput());
|
||||
})
|
||||
.map(i -> {
|
||||
i.index = reindex.getAndIncrement();
|
||||
return i;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
List<ItemState<I>> items = filtered.stream()
|
||||
.skip(skip)
|
||||
.limit(maxItems)
|
||||
.collect(Collectors.toList());
|
||||
return new ItemStateViewProjection(items, filtered.size());
|
||||
}
|
||||
|
||||
private class ItemStateViewProjection {
|
||||
List<ItemState<I>> items;
|
||||
int total;
|
||||
ItemStateViewProjection(List<ItemState<I>> items, int total) {
|
||||
this.items = items;
|
||||
this.total = total;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Context interface on a selector component sharing content.
|
||||
*/
|
||||
public interface SelectorComponentContext<T, I extends Nameable & Matchable & Itemable<T>, C extends SelectorComponentContext<T, I, C>>
|
||||
extends ComponentContext<C> {
|
||||
|
||||
/**
|
||||
* Gets a name.
|
||||
*
|
||||
* @return a name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Sets a name
|
||||
*
|
||||
* @param name the name
|
||||
*/
|
||||
void setName(String name);
|
||||
|
||||
/**
|
||||
* Gets an input.
|
||||
*
|
||||
* @return an input
|
||||
*/
|
||||
String getInput();
|
||||
|
||||
/**
|
||||
* Sets an input.
|
||||
*
|
||||
* @param input the input
|
||||
*/
|
||||
void setInput(String input);
|
||||
|
||||
/**
|
||||
* Gets an item states
|
||||
*
|
||||
* @return an item states
|
||||
*/
|
||||
List<ItemState<I>> getItemStates();
|
||||
|
||||
/**
|
||||
* Sets an item states.
|
||||
*
|
||||
* @param itemStateView the input state
|
||||
*/
|
||||
void setItemStates(List<ItemState<I>> itemStateView);
|
||||
|
||||
/**
|
||||
* Gets an item state view.
|
||||
*
|
||||
* @return an item state view
|
||||
*/
|
||||
List<ItemState<I>> getItemStateView();
|
||||
|
||||
/**
|
||||
* Sets an item state view
|
||||
*
|
||||
* @param itemStateView the item state view
|
||||
*/
|
||||
void setItemStateView(List<ItemState<I>> itemStateView);
|
||||
|
||||
/**
|
||||
* Return if there is a result.
|
||||
*
|
||||
* @return true if context represents result
|
||||
*/
|
||||
boolean isResult();
|
||||
|
||||
/**
|
||||
* Gets a cursor row.
|
||||
*
|
||||
* @return a cursor row.
|
||||
*/
|
||||
Integer getCursorRow();
|
||||
|
||||
/**
|
||||
* Sets a cursor row.
|
||||
*
|
||||
* @param cursorRow the cursor row
|
||||
*/
|
||||
void setCursorRow(Integer cursorRow);
|
||||
|
||||
/**
|
||||
* Gets an items.
|
||||
*
|
||||
* @return an items
|
||||
*/
|
||||
List<I> getItems();
|
||||
|
||||
/**
|
||||
* Sets an items.
|
||||
*
|
||||
* @param items the items
|
||||
*/
|
||||
void setItems(List<I> items);
|
||||
|
||||
/**
|
||||
* Gets a result items.
|
||||
*
|
||||
* @return a result items
|
||||
*/
|
||||
List<I> getResultItems();
|
||||
|
||||
/**
|
||||
* Sets a result items.
|
||||
*
|
||||
* @param items the result items
|
||||
*/
|
||||
void setResultItems(List<I> items);
|
||||
|
||||
/**
|
||||
* Creates an empty {@link SelectorComponentContext}.
|
||||
*
|
||||
* @return empty context
|
||||
*/
|
||||
static <T, I extends Nameable & Matchable & Itemable<T>, C extends SelectorComponentContext<T, I, C>> SelectorComponentContext<T, I, C> empty() {
|
||||
return new BaseSelectorComponentContext<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base implementation of a {@link SelectorComponentContext}.
|
||||
*/
|
||||
protected static class BaseSelectorComponentContext<T, I extends Nameable & Matchable & Itemable<T>, C extends SelectorComponentContext<T, I, C>>
|
||||
extends BaseComponentContext<C> implements SelectorComponentContext<T, I, C> {
|
||||
|
||||
private String name;
|
||||
private String input;
|
||||
private List<ItemState<I>> itemStates;
|
||||
private List<ItemState<I>> itemStateView;
|
||||
private Integer cursorRow;
|
||||
private List<I> items;
|
||||
private List<I> resultItems;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInput(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ItemState<I>> getItemStates() {
|
||||
return itemStates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItemStates(List<ItemState<I>> itemStates) {
|
||||
this.itemStates = itemStates;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<ItemState<I>> getItemStateView() {
|
||||
return itemStateView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItemStateView(List<ItemState<I>> itemStateView) {
|
||||
this.itemStateView = itemStateView;
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean isResult() {
|
||||
return resultItems != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCursorRow() {
|
||||
return cursorRow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Map<String,Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
attributes.put("name", getName());
|
||||
attributes.put("input", getInput());
|
||||
attributes.put("itemStates", getItemStates());
|
||||
attributes.put("itemStateView", getItemStateView());
|
||||
attributes.put("isResult", isResult());
|
||||
attributes.put("cursorRow", getCursorRow());
|
||||
return attributes;
|
||||
};
|
||||
|
||||
public void setCursorRow(Integer cursorRow) {
|
||||
this.cursorRow = cursorRow;
|
||||
};
|
||||
|
||||
@Override
|
||||
public List<I> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItems(List<I> items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<I> getResultItems() {
|
||||
return resultItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResultItems(List<I> resultItems) {
|
||||
this.resultItems = resultItems;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultSelectorComponentContext [cursorRow=" + cursorRow + "]";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class keeping item state.
|
||||
*/
|
||||
public static class ItemState<I extends Matchable> implements Matchable {
|
||||
I item;
|
||||
String name;
|
||||
boolean selected;
|
||||
boolean enabled;
|
||||
int index;
|
||||
|
||||
ItemState(I item, String name, int index, boolean enabled) {
|
||||
this.item = item;
|
||||
this.name = name;
|
||||
this.index = index;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean matches(String match) {
|
||||
return item.matches(match);
|
||||
};
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static <I extends Matchable> ItemState<I> of(I item, String name, int index, boolean enabled) {
|
||||
return new ItemState<I>(item, name, index, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.jline.utils.InfoCmp.Capability;
|
||||
|
||||
import org.springframework.shell.component.context.BaseComponentContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
|
||||
|
||||
import static org.jline.keymap.KeyMap.del;
|
||||
import static org.jline.keymap.KeyMap.key;
|
||||
|
||||
/**
|
||||
* Base class for components which work on a simple text input.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public abstract class AbstractTextComponent<T, C extends TextComponentContext<T, C>> extends AbstractComponent<C> {
|
||||
|
||||
private final String name;
|
||||
|
||||
public AbstractTextComponent(Terminal terminal) {
|
||||
this(terminal, null);
|
||||
}
|
||||
|
||||
public AbstractTextComponent(Terminal terminal, String name) {
|
||||
this(terminal, name, null);
|
||||
}
|
||||
|
||||
public AbstractTextComponent(Terminal terminal, String name, Function<C, List<AttributedString>> renderer) {
|
||||
super(terminal);
|
||||
this.name = name;
|
||||
setRenderer(renderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bindKeyMap(KeyMap<String> keyMap) {
|
||||
keyMap.bind(OPERATION_EXIT, "\r");
|
||||
keyMap.bind(OPERATION_BACKSPACE, del(), key(getTerminal(), Capability.key_backspace));
|
||||
// skip 127 - DEL
|
||||
for (char i = 32; i < KeyMap.KEYMAP_LENGTH - 1; i++) {
|
||||
keyMap.bind(OPERATION_CHAR, Character.toString(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected C runInternal(C context) {
|
||||
loop(context);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a name.
|
||||
*
|
||||
* @return a name
|
||||
*/
|
||||
protected String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public interface TextComponentContext<T, C extends TextComponentContext<T, C>> extends ComponentContext<C> {
|
||||
|
||||
/**
|
||||
* Gets a name.
|
||||
*
|
||||
* @return a name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Sets a name.
|
||||
*
|
||||
* @param name the name
|
||||
*/
|
||||
void setName(String name);
|
||||
|
||||
/**
|
||||
* Gets an input.
|
||||
*
|
||||
* @return an input
|
||||
*/
|
||||
String getInput();
|
||||
|
||||
/**
|
||||
* Sets an input.
|
||||
*
|
||||
* @param input the input
|
||||
*/
|
||||
void setInput(String input);
|
||||
|
||||
/**
|
||||
* Sets a result value.
|
||||
*
|
||||
* @return a result value
|
||||
*/
|
||||
T getResultValue();
|
||||
|
||||
/**
|
||||
* Sets a result value.
|
||||
*
|
||||
* @param resultValue the result value
|
||||
*/
|
||||
void setResultValue(T resultValue);
|
||||
|
||||
/**
|
||||
* Sets a message.
|
||||
*
|
||||
* @return a message
|
||||
*/
|
||||
String getMessage();
|
||||
|
||||
/**
|
||||
* Sets a message.
|
||||
*
|
||||
* @param message the message
|
||||
*/
|
||||
void setMessage(String message);
|
||||
|
||||
/**
|
||||
* Sets a message with level.
|
||||
*
|
||||
* @param message the message
|
||||
* @param level the message level
|
||||
*/
|
||||
void setMessage(String message, MessageLevel level);
|
||||
|
||||
/**
|
||||
* Gets a {@link MessageLevel}.
|
||||
*
|
||||
* @return a message level
|
||||
*/
|
||||
MessageLevel getMessageLevel();
|
||||
|
||||
/**
|
||||
* Sets a {@link MessageLevel}.
|
||||
*
|
||||
* @param level the message level
|
||||
*/
|
||||
void setMessageLevel(MessageLevel level);
|
||||
|
||||
/**
|
||||
* Message levels which can be used to alter how message is shown.
|
||||
*/
|
||||
public enum MessageLevel {
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
|
||||
public static class BaseTextComponentContext<T, C extends TextComponentContext<T, C>> extends BaseComponentContext<C>
|
||||
implements TextComponentContext<T, C> {
|
||||
|
||||
private String name;
|
||||
private String input;
|
||||
private T resultValue;
|
||||
private String message;
|
||||
private MessageLevel messageLevel = MessageLevel.INFO;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInput() {
|
||||
return input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInput(String input) {
|
||||
this.input = input;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getResultValue() {
|
||||
return resultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setResultValue(T resultValue) {
|
||||
this.resultValue = resultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessage(String message, MessageLevel level) {
|
||||
setMessage(message);
|
||||
setMessageLevel(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageLevel getMessageLevel() {
|
||||
return messageLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMessageLevel(MessageLevel messageLevel) {
|
||||
this.messageLevel = messageLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toTemplateModel() {
|
||||
Map<String, Object> attributes = super.toTemplateModel();
|
||||
attributes.put("resultValue", getResultValue() != null ? getResultValue().toString() : null);
|
||||
attributes.put("name", getName());
|
||||
attributes.put("message", getMessage());
|
||||
attributes.put("messageLevel", getMessageLevel());
|
||||
attributes.put("hasMessageLevelInfo", getMessageLevel() == MessageLevel.INFO);
|
||||
attributes.put("hasMessageLevelWarn", getMessageLevel() == MessageLevel.WARN);
|
||||
attributes.put("hasMessageLevelError", getMessageLevel() == MessageLevel.ERROR);
|
||||
attributes.put("input", getInput());
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
public interface Enableable {
|
||||
|
||||
boolean isEnabled();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
public interface Itemable<T> {
|
||||
|
||||
T getItem();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
public interface Matchable {
|
||||
|
||||
boolean matches(String match);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
public interface Nameable {
|
||||
|
||||
String getName();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.support;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
public interface SelectorItem<T> extends Nameable, Matchable, Enableable, Itemable<T> {
|
||||
|
||||
static <T> SelectorItem<T> of(String name, T item) {
|
||||
return of(name, item, true);
|
||||
}
|
||||
|
||||
static <T> SelectorItem<T> of(String name, T item, boolean enabled) {
|
||||
return new SelectorItemWrapper<T>(name, item, enabled);
|
||||
}
|
||||
|
||||
public static class SelectorItemWrapper<T> implements SelectorItem<T> {
|
||||
private String name;
|
||||
private boolean enabled;
|
||||
private T item;
|
||||
|
||||
public SelectorItemWrapper(String name, T item) {
|
||||
this(name, item, true);
|
||||
}
|
||||
|
||||
public SelectorItemWrapper(String name, T item, boolean enabled) {
|
||||
this.name = name;
|
||||
this.item = item;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String match) {
|
||||
if (!StringUtils.hasText(match)) {
|
||||
return true;
|
||||
};
|
||||
return name.toLowerCase().contains(match.toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public T getItem() {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// used to select style if item is selected/unselected
|
||||
selected_style(flag) ::= <%
|
||||
<if(flag)>item-selected<else>item-unselected<endif>
|
||||
%>
|
||||
|
||||
// selector rows
|
||||
select_item(item) ::= <%
|
||||
<if(item.onrow)>
|
||||
<("> "); format="item-selector">
|
||||
<else>
|
||||
<(" ")>
|
||||
<endif>
|
||||
|
||||
<if(item.enabled)>
|
||||
<if(item.selected)>
|
||||
<("[x]"); format=selected_style(item.selected)> <item.name>
|
||||
<else>
|
||||
<("[ ]"); format=selected_style(item.selected)> <item.name>
|
||||
<endif>
|
||||
<else>
|
||||
<("[ ]"); format="item-disabled"> <item.name; format="item-disabled">
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// start '? xxx' shows both running and result
|
||||
question_name(model) ::= <<
|
||||
<("?"); format="list-value"> <model.name; format="title">
|
||||
>>
|
||||
|
||||
// within info section, dedicated instructions for user
|
||||
info_filter(model) ::= <%
|
||||
<if(model.input)>
|
||||
, filtering '<model.input>'
|
||||
<else>
|
||||
, type to filter
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// info section after '? xxx'
|
||||
info(model) ::= <<
|
||||
[Use arrows to move]<info_filter(model)>
|
||||
>>
|
||||
|
||||
// get comma delited string
|
||||
comma_delimited(values) ::= <%
|
||||
<values; separator=",">
|
||||
%>
|
||||
|
||||
// component result
|
||||
result(model) ::= <<
|
||||
<question_name(model)> <(comma_delimited(model.values)); format="value">
|
||||
>>
|
||||
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
<model.rows:{x|<select_item(x)>}; separator="\n">
|
||||
>>
|
||||
|
||||
// main - hardcoded name
|
||||
// model - model built by MultiItemSelectorContext
|
||||
main(model) ::= <<
|
||||
<if(model.isResult)><result(model)><else><running(model)><endif>
|
||||
>>
|
||||
@@ -0,0 +1,38 @@
|
||||
// message
|
||||
message(model) ::= <%
|
||||
<if(model.message && model.hasMessageLevelError)>
|
||||
<(">>>"); format="level-error"> <model.message; format="level-error">
|
||||
<elseif(model.message && model.hasMessageLevelWarn)>
|
||||
<(">>"); format="level-warn"> <model.message; format="level-warn">
|
||||
<elseif(model.message && model.hasMessageLevelInfo)>
|
||||
<(">"); format="level-info"> <model.message; format="level-info">
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// info section after '? xxx'
|
||||
info(model) ::= <%
|
||||
<if(model.input)>
|
||||
<model.input>
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// start '? xxx' shows both running and result
|
||||
question_name(model) ::= <<
|
||||
<("?"); format="list-value"> <model.name; format="title">
|
||||
>>
|
||||
|
||||
// component result
|
||||
result(model) ::= <<
|
||||
<question_name(model)> <model.resultValue; format="value">
|
||||
>>
|
||||
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
<message(model)>
|
||||
>>
|
||||
|
||||
// main
|
||||
main(model) ::= <<
|
||||
<if(model.resultValue)><result(model)><else><running(model)><endif>
|
||||
>>
|
||||
@@ -0,0 +1,44 @@
|
||||
// selector rows
|
||||
select_item(item) ::= <%
|
||||
<if(item.selected)>
|
||||
<("> "); format="item-selector"><item.name; format="item-selector">
|
||||
<else>
|
||||
<(" ")><item.name>
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// start '? xxx' shows both running and result
|
||||
question_name(model) ::= <<
|
||||
<("?"); format="list-value"> <model.name; format="title">
|
||||
>>
|
||||
|
||||
// within info section, dedicated instructions for user
|
||||
info_filter(model) ::= <%
|
||||
<if(model.input)>
|
||||
, filtering '<model.input>'
|
||||
<else>
|
||||
, type to filter
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// info section after '? xxx'
|
||||
info(model) ::= <<
|
||||
[Use arrows to move]<info_filter(model)>
|
||||
>>
|
||||
|
||||
// component result
|
||||
result(model) ::= <<
|
||||
<question_name(model)> <model.value; format="value">
|
||||
>>
|
||||
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
<model.rows:{x|<select_item(x)>}; separator="\n">
|
||||
>>
|
||||
|
||||
// main - hardcoded name
|
||||
// model - model built by SingleItemSelectorContext
|
||||
main(model) ::= <<
|
||||
<if(model.isResult)><result(model)><else><running(model)><endif>
|
||||
>>
|
||||
@@ -0,0 +1,28 @@
|
||||
// info section after '? xxx'
|
||||
info(model) ::= <%
|
||||
<if(model.input)>
|
||||
<model.input>
|
||||
<else>
|
||||
<("[Default "); format="value"><model.defaultValue; format="value"><("]"); format="value">
|
||||
<endif>
|
||||
%>
|
||||
|
||||
// start '? xxx' shows both running and result
|
||||
question_name(model) ::= <<
|
||||
<("?"); format="list-value"> <model.name; format="title">
|
||||
>>
|
||||
|
||||
// component result
|
||||
result(model) ::= <<
|
||||
<question_name(model)> <model.resultValue; format="value">
|
||||
>>
|
||||
|
||||
// component is running
|
||||
running(model) ::= <<
|
||||
<question_name(model)> <info(model)>
|
||||
>>
|
||||
|
||||
// main
|
||||
main(model) ::= <<
|
||||
<if(model.resultValue)><result(model)><else><running(model)><endif>
|
||||
>>
|
||||
@@ -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;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.jline.keymap.KeyMap;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.jline.terminal.impl.DumbTerminal;
|
||||
import org.jline.utils.AttributedString;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.style.TemplateExecutor;
|
||||
import org.springframework.shell.style.Theme;
|
||||
import org.springframework.shell.style.ThemeRegistry;
|
||||
import org.springframework.shell.style.ThemeResolver;
|
||||
import org.springframework.shell.style.ThemeSettings;
|
||||
|
||||
import static org.jline.keymap.KeyMap.del;
|
||||
|
||||
public abstract class AbstractShellTests {
|
||||
|
||||
private ExecutorService executorService;
|
||||
private PipedInputStream pipedInputStream;
|
||||
private PipedOutputStream pipedOutputStream;
|
||||
private LinkedBlockingQueue<byte[]> bytesQueue;
|
||||
private ByteArrayOutputStream consoleOut;
|
||||
private Terminal terminal;
|
||||
private TemplateExecutor templateExecutor;
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
executorService = Executors.newFixedThreadPool(1);
|
||||
pipedInputStream = new PipedInputStream();
|
||||
pipedOutputStream = new PipedOutputStream();
|
||||
bytesQueue = new LinkedBlockingQueue<>();
|
||||
consoleOut = new ByteArrayOutputStream();
|
||||
|
||||
ThemeRegistry themeRegistry = new ThemeRegistry();
|
||||
themeRegistry.register(new Theme() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "default";
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThemeSettings getSettings() {
|
||||
return ThemeSettings.themeSettings();
|
||||
}
|
||||
});
|
||||
ThemeResolver themeResolver = new ThemeResolver(themeRegistry, "default");
|
||||
templateExecutor = new TemplateExecutor(themeResolver);
|
||||
|
||||
resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
pipedInputStream.connect(pipedOutputStream);
|
||||
terminal = new DumbTerminal("terminal", "ansi", pipedInputStream, consoleOut, StandardCharsets.UTF_8);
|
||||
|
||||
executorService.execute(() -> {
|
||||
try {
|
||||
while (true) {
|
||||
byte[] take = bytesQueue.take();
|
||||
pipedOutputStream.write(take);
|
||||
pipedOutputStream.flush();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
executorService.shutdown();
|
||||
}
|
||||
|
||||
protected void write(byte[] bytes) {
|
||||
bytesQueue.add(bytes);
|
||||
}
|
||||
|
||||
protected String consoleOut() {
|
||||
return AttributedString.fromAnsi(consoleOut.toString()).toString();
|
||||
}
|
||||
|
||||
protected Terminal getTerminal() {
|
||||
return terminal;
|
||||
}
|
||||
|
||||
protected ResourceLoader getResourceLoader() {
|
||||
return resourceLoader;
|
||||
}
|
||||
|
||||
protected TemplateExecutor getTemplateExecutor() {
|
||||
return templateExecutor;
|
||||
}
|
||||
|
||||
protected class TestBuffer {
|
||||
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
public TestBuffer() {
|
||||
}
|
||||
|
||||
public TestBuffer(String str) {
|
||||
append(str);
|
||||
}
|
||||
|
||||
public TestBuffer(char[] chars) {
|
||||
append(new String(chars));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
try {
|
||||
return out.toString(StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public TestBuffer cr() {
|
||||
return append("\r");
|
||||
}
|
||||
|
||||
public TestBuffer backspace() {
|
||||
return append(del());
|
||||
}
|
||||
|
||||
public TestBuffer backspace(int count) {
|
||||
TestBuffer buf = this;
|
||||
for (int i = 0; i < count; i++) {
|
||||
buf = backspace();
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
public TestBuffer down() {
|
||||
return append("\033[B");
|
||||
}
|
||||
|
||||
public TestBuffer ctrl(char let) {
|
||||
return append(KeyMap.ctrl(let));
|
||||
}
|
||||
|
||||
public TestBuffer ctrlE() {
|
||||
return ctrl('E');
|
||||
}
|
||||
|
||||
public TestBuffer ctrlY() {
|
||||
return ctrl('Y');
|
||||
}
|
||||
|
||||
public TestBuffer space() {
|
||||
return append(" ");
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
public TestBuffer append(final String str) {
|
||||
for (byte b : str.getBytes(StandardCharsets.UTF_8)) {
|
||||
append(b);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestBuffer append(final int i) {
|
||||
// consoleOut.reset();
|
||||
out.write((byte) i);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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 java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.SelectorItem;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.springframework.shell.component.ShellAssertions.assertStringOrderThat;
|
||||
|
||||
public class MultiItemSelectorTests extends AbstractShellTests {
|
||||
|
||||
private static SimplePojo SIMPLE_POJO_1 = SimplePojo.of("data1");
|
||||
private static SimplePojo SIMPLE_POJO_2 = SimplePojo.of("data2");
|
||||
private static SimplePojo SIMPLE_POJO_3 = SimplePojo.of("data3");
|
||||
private static SimplePojo SIMPLE_POJO_4 = SimplePojo.of("data4");
|
||||
private static SimplePojo SIMPLE_POJO_5 = SimplePojo.of("data5");
|
||||
private static SimplePojo SIMPLE_POJO_6 = SimplePojo.of("data6");
|
||||
private static SimplePojo SIMPLE_POJO_7 = SimplePojo.of("data7");
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_1 = SelectorItem.of("simplePojo1", SIMPLE_POJO_1);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_2 = SelectorItem.of("simplePojo2", SIMPLE_POJO_2);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_3 = SelectorItem.of("simplePojo3", SIMPLE_POJO_3);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_4 = SelectorItem.of("simplePojo4", SIMPLE_POJO_4);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_5 = SelectorItem.of("simplePojo5", SIMPLE_POJO_5);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_6 = SelectorItem.of("simplePojo6", SIMPLE_POJO_6);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_7 = SelectorItem.of("simplePojo7", SIMPLE_POJO_7, false);
|
||||
|
||||
private ExecutorService service;
|
||||
private CountDownLatch latch;
|
||||
private AtomicReference<List<SelectorItem<SimplePojo>>> result;
|
||||
|
||||
@BeforeEach
|
||||
public void setupMulti() {
|
||||
service = Executors.newFixedThreadPool(1);
|
||||
latch = new CountDownLatch(1);
|
||||
result = new AtomicReference<>();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanupMulti() {
|
||||
latch = null;
|
||||
result = null;
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testItemsShown() {
|
||||
scheduleSelect();
|
||||
await().atMost(Duration.ofSeconds(4))
|
||||
.untilAsserted(() -> assertStringOrderThat(consoleOut()).containsInOrder("simplePojo1", "simplePojo2",
|
||||
"simplePojo3", "simplePojo4"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxItems() {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4,
|
||||
SELECTOR_ITEM_5, SELECTOR_ITEM_6), 6);
|
||||
await().atMost(Duration.ofSeconds(4))
|
||||
.untilAsserted(() -> assertStringOrderThat(consoleOut()).containsInOrder("simplePojo1", "simplePojo2",
|
||||
"simplePojo3", "simplePojo4", "simplePojo5", "simplePojo6"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testItemsShownWithDisabled() {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_7));
|
||||
await().atMost(Duration.ofSeconds(4))
|
||||
.untilAsserted(() -> assertStringOrderThat(consoleOut()).containsInOrder("[ ] simplePojo1", "[ ] simplePojo7"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableIsNotSelectable() throws InterruptedException {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_7));
|
||||
TestBuffer testBuffer = new TestBuffer().space().ctrlE().space().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).hasSize(1);
|
||||
Stream<String> datas = selected.stream().map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).containsExactlyInAnyOrder("data1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoneSelected() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectFirst() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().space().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).hasSize(1);
|
||||
Stream<String> datas = selected.stream().map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).containsExactlyInAnyOrder("data1");
|
||||
assertThat(consoleOut()).contains("testSimple data1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectSecond() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().ctrlE().space().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).hasSize(1);
|
||||
Stream<String> datas = selected.stream().map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).containsExactlyInAnyOrder("data2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectSecondAndFourth() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().ctrlE().space().ctrlE().ctrlE().space().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).hasSize(2);
|
||||
Stream<String> datas = selected.stream().map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).containsExactlyInAnyOrder("data2", "data4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectLastBackwards() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().ctrlY().space().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
List<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).hasSize(1);
|
||||
Stream<String> datas = selected.stream().map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).containsExactlyInAnyOrder("data4");
|
||||
}
|
||||
|
||||
private void scheduleSelect() {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3,
|
||||
SELECTOR_ITEM_4));
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items) {
|
||||
scheduleSelect(items, null);
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
|
||||
MultiItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new MultiItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
selector.setResourceLoader(new DefaultResourceLoader());
|
||||
selector.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
selector.setPrintResults(true);
|
||||
if (maxItems != null) {
|
||||
selector.setMaxItems(maxItems);
|
||||
}
|
||||
service.execute(() -> {
|
||||
ComponentContext<?> context = ComponentContext.empty();
|
||||
result.set(selector.run(context).getResultItems());
|
||||
latch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
private void awaitLatch() throws InterruptedException {
|
||||
latch.await(4, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static class SimplePojo {
|
||||
String data;
|
||||
|
||||
SimplePojo(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
static SimplePojo of(String data) {
|
||||
return new SimplePojo(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
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 java.util.function.Function;
|
||||
|
||||
import com.google.common.jimfs.Jimfs;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.shell.component.PathInput.PathInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class PathInputTests extends AbstractShellTests {
|
||||
|
||||
private ExecutorService service;
|
||||
private CountDownLatch latch1;
|
||||
private AtomicReference<PathInputContext> result1;
|
||||
private FileSystem fileSystem;
|
||||
private Function<String, Path> pathProvider;
|
||||
|
||||
@BeforeEach
|
||||
public void setupTests() {
|
||||
service = Executors.newFixedThreadPool(1);
|
||||
latch1 = new CountDownLatch(1);
|
||||
result1 = new AtomicReference<>();
|
||||
fileSystem = Jimfs.newFileSystem();
|
||||
pathProvider = (path) -> fileSystem.getPath(path);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanupTests() throws IOException {
|
||||
latch1 = null;
|
||||
result1 = null;
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
service = null;
|
||||
if (fileSystem != null) {
|
||||
fileSystem.close();
|
||||
}
|
||||
fileSystem = null;
|
||||
pathProvider = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInput() throws InterruptedException, IOException {
|
||||
Path path = fileSystem.getPath("tmp");
|
||||
Files.createDirectories(path);
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
PathInput component1 = new PathInput(getTerminal(), "component1");
|
||||
component1.setPathProvider(pathProvider);
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
PathInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().append("tmp").cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
PathInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isNotNull();
|
||||
assertThat(run1Context.getResultValue().toString()).contains("tmp");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
|
||||
/**
|
||||
* Custom assertj assertions.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ShellAssertions {
|
||||
|
||||
public static StringOrderAssert assertStringOrderThat(String actual) {
|
||||
return new StringOrderAssert(actual);
|
||||
}
|
||||
|
||||
public static class StringOrderAssert extends AbstractAssert<StringOrderAssert, String> {
|
||||
|
||||
protected StringOrderAssert(String out) {
|
||||
super(out, StringOrderAssert.class);
|
||||
}
|
||||
|
||||
public StringOrderAssert containsInOrder(String... expected) {
|
||||
isNotNull();
|
||||
int[] indexes = new int[expected.length];
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
indexes[i] = actual.indexOf(expected[i]);
|
||||
}
|
||||
for (int i = 0; i < expected.length; i++) {
|
||||
if (indexes[i] < 0) {
|
||||
failWithMessage("Item [%s] not found from output [%s]", expected[i], actual);
|
||||
}
|
||||
}
|
||||
if (!isSorted(indexes)) {
|
||||
String expectedStr = Stream.of(expected).collect(Collectors.joining(","));
|
||||
String indexStr = IntStream.of(indexes).mapToObj(i -> ((Integer) i).toString())
|
||||
.collect(Collectors.joining(","));
|
||||
failWithMessage("Items [%s] are in wrong order, indexes are [%s], output is [%s]", expectedStr,
|
||||
indexStr, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean isSorted(int[] array) {
|
||||
for (int i = 0; i < array.length - 1; i++) {
|
||||
if (array[i] > array[i + 1])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
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.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
import org.springframework.shell.component.support.SelectorItem;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.springframework.shell.component.ShellAssertions.assertStringOrderThat;
|
||||
|
||||
public class SingleItemSelectorTests extends AbstractShellTests {
|
||||
|
||||
private static SimplePojo SIMPLE_POJO_1 = SimplePojo.of("data1");
|
||||
private static SimplePojo SIMPLE_POJO_2 = SimplePojo.of("data2");
|
||||
private static SimplePojo SIMPLE_POJO_3 = SimplePojo.of("data3");
|
||||
private static SimplePojo SIMPLE_POJO_4 = SimplePojo.of("data4");
|
||||
private static SimplePojo SIMPLE_POJO_5 = SimplePojo.of("data5");
|
||||
private static SimplePojo SIMPLE_POJO_6 = SimplePojo.of("data6");
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_1 = SelectorItem.of("simplePojo1", SIMPLE_POJO_1);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_2 = SelectorItem.of("simplePojo2", SIMPLE_POJO_2);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_3 = SelectorItem.of("simplePojo3", SIMPLE_POJO_3);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_4 = SelectorItem.of("simplePojo4", SIMPLE_POJO_4);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_5 = SelectorItem.of("simplePojo5", SIMPLE_POJO_5);
|
||||
private static SelectorItem<SimplePojo> SELECTOR_ITEM_6 = SelectorItem.of("simplePojo6", SIMPLE_POJO_6);
|
||||
|
||||
private ExecutorService service;
|
||||
private CountDownLatch latch;
|
||||
private AtomicReference<Optional<SelectorItem<SimplePojo>>> result;
|
||||
|
||||
@BeforeEach
|
||||
public void setupMulti() {
|
||||
service = Executors.newFixedThreadPool(1);
|
||||
latch = new CountDownLatch(1);
|
||||
result = new AtomicReference<>();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanupMulti() {
|
||||
latch = null;
|
||||
result = null;
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testItemsShownFirstHovered() {
|
||||
scheduleSelect();
|
||||
await().atMost(Duration.ofSeconds(4))
|
||||
.untilAsserted(() -> {
|
||||
assertStringOrderThat(consoleOut()).containsInOrder("> simplePojo1", "simplePojo2", "simplePojo3", "simplePojo4");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxItems() {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3, SELECTOR_ITEM_4,
|
||||
SELECTOR_ITEM_5, SELECTOR_ITEM_6), 6);
|
||||
await().atMost(Duration.ofSeconds(4))
|
||||
.untilAsserted(() -> assertStringOrderThat(consoleOut()).containsInOrder("simplePojo1", "simplePojo2",
|
||||
"simplePojo3", "simplePojo4", "simplePojo5", "simplePojo6"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectFirst() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
Optional<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).isNotEmpty();
|
||||
Optional<String> datas = selected.map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).contains("data1");
|
||||
assertThat(consoleOut()).contains("testSimple data1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectSecond() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().ctrlE().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
Optional<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).isNotEmpty();
|
||||
Optional<String> datas = selected.map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).contains("data2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectLastBackwards() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().ctrlY().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
awaitLatch();
|
||||
|
||||
Optional<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).isNotEmpty();
|
||||
Optional<String> datas = selected.map(SelectorItem::getItem).map(SimplePojo::getData);
|
||||
assertThat(datas).contains("data4");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterShowsNoneThenSelect() throws InterruptedException {
|
||||
scheduleSelect();
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().append("xxx").cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
assertThat(awaitLatch(1)).isFalse();
|
||||
|
||||
testBuffer = new TestBuffer().backspace(3).cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
assertThat(awaitLatch()).isTrue();
|
||||
|
||||
Optional<SelectorItem<SimplePojo>> selected = result.get();
|
||||
assertThat(selected).isNotEmpty();
|
||||
}
|
||||
|
||||
private void scheduleSelect() {
|
||||
scheduleSelect(Arrays.asList(SELECTOR_ITEM_1, SELECTOR_ITEM_2, SELECTOR_ITEM_3,
|
||||
SELECTOR_ITEM_4));
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items) {
|
||||
scheduleSelect(items, null);
|
||||
}
|
||||
|
||||
private void scheduleSelect(List<SelectorItem<SimplePojo>> items, Integer maxItems) {
|
||||
SingleItemSelector<SimplePojo, SelectorItem<SimplePojo>> selector = new SingleItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
selector.setResourceLoader(new DefaultResourceLoader());
|
||||
selector.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
selector.setPrintResults(true);
|
||||
if (maxItems != null) {
|
||||
selector.setMaxItems(maxItems);
|
||||
}
|
||||
service.execute(() -> {
|
||||
ComponentContext<?> context = ComponentContext.empty();
|
||||
result.set(selector.run(context).getResultItem());
|
||||
latch.countDown();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean awaitLatch() throws InterruptedException {
|
||||
return awaitLatch(4);
|
||||
}
|
||||
|
||||
private boolean awaitLatch(int seconds) throws InterruptedException {
|
||||
return latch.await(seconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private static class SimplePojo {
|
||||
String data;
|
||||
|
||||
SimplePojo(String data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
static SimplePojo of(String data) {
|
||||
return new SimplePojo(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.shell.component.StringInput.StringInputContext;
|
||||
import org.springframework.shell.component.context.ComponentContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class StringInputTests extends AbstractShellTests {
|
||||
|
||||
private ExecutorService service;
|
||||
private CountDownLatch latch1;
|
||||
private CountDownLatch latch2;
|
||||
private AtomicReference<StringInputContext> result1;
|
||||
private AtomicReference<StringInputContext> result2;
|
||||
|
||||
@BeforeEach
|
||||
public void setupTests() {
|
||||
service = Executors.newFixedThreadPool(1);
|
||||
latch1 = new CountDownLatch(1);
|
||||
latch2 = new CountDownLatch(1);
|
||||
result1 = new AtomicReference<>();
|
||||
result2 = new AtomicReference<>();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanupTests() {
|
||||
latch1 = null;
|
||||
latch2 = null;
|
||||
result1 = null;
|
||||
result2 = null;
|
||||
if (service != null) {
|
||||
service.shutdown();
|
||||
}
|
||||
service = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultBasic() throws InterruptedException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
StringInput component1 = new StringInput(getTerminal(), "component1", "component1ResultValue");
|
||||
component1.setPrintResults(true);
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
StringInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
StringInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isEqualTo("component1ResultValue");
|
||||
assertThat(consoleOut()).contains("component1 component1ResultValue");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResultUserInput() throws InterruptedException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
StringInput component1 = new StringInput(getTerminal(), "component1", "component1ResultValue");
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
service.execute(() -> {
|
||||
StringInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().append("test").cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
StringInputContext run1Context = result1.get();
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPassingViaContext() throws InterruptedException {
|
||||
ComponentContext<?> empty = ComponentContext.empty();
|
||||
StringInput component1 = new StringInput(getTerminal(), "component1", "component1ResultValue");
|
||||
StringInput component2 = new StringInput(getTerminal(), "component2", "component2ResultValue");
|
||||
component1.setResourceLoader(new DefaultResourceLoader());
|
||||
component1.setTemplateExecutor(getTemplateExecutor());
|
||||
component2.setResourceLoader(new DefaultResourceLoader());
|
||||
component2.setTemplateExecutor(getTemplateExecutor());
|
||||
|
||||
component1.addPostRunHandler(context -> {
|
||||
context.put("component1ResultValue", context.getResultValue());
|
||||
});
|
||||
|
||||
component2.addPreRunHandler(context -> {
|
||||
String component1ResultValue = context.get("component1ResultValue");
|
||||
context.setDefaultValue(component1ResultValue);
|
||||
});
|
||||
component2.addPostRunHandler(context -> {
|
||||
});
|
||||
|
||||
service.execute(() -> {
|
||||
StringInputContext run1Context = component1.run(empty);
|
||||
result1.set(run1Context);
|
||||
latch1.countDown();
|
||||
});
|
||||
|
||||
TestBuffer testBuffer = new TestBuffer().cr();
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch1.await(2, TimeUnit.SECONDS);
|
||||
|
||||
service.execute(() -> {
|
||||
StringInputContext run1Context = result1.get();
|
||||
StringInputContext run2Context = component2.run(run1Context);
|
||||
result2.set(run2Context);
|
||||
latch2.countDown();
|
||||
});
|
||||
|
||||
write(testBuffer.getBytes());
|
||||
|
||||
latch2.await(2, TimeUnit.SECONDS);
|
||||
|
||||
StringInputContext run1Context = result1.get();
|
||||
StringInputContext run2Context = result2.get();
|
||||
|
||||
assertThat(run1Context).isNotSameAs(run2Context);
|
||||
|
||||
assertThat(run1Context).isNotNull();
|
||||
assertThat(run2Context).isNotNull();
|
||||
assertThat(run1Context.getResultValue()).isEqualTo("component1ResultValue");
|
||||
assertThat(run2Context.getResultValue()).isEqualTo("component1ResultValue");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class ComponentContextTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unused")
|
||||
public void testBasics() {
|
||||
ComponentContext<?> context = ComponentContext.empty();
|
||||
assertThat(context.stream()).isEmpty();
|
||||
|
||||
context.put("foo", "bar");
|
||||
|
||||
assertThat(context.stream()).containsExactlyInAnyOrder(new AbstractMap.SimpleEntry<>("foo", "bar"));
|
||||
|
||||
String foo = context.get("foo");
|
||||
assertThat(foo).isEqualTo("bar");
|
||||
assertThatThrownBy(() -> {
|
||||
Integer unused = context.get("foo");
|
||||
}).isInstanceOf(ClassCastException.class);
|
||||
|
||||
assertThat(context.get("foo", String.class)).isEqualTo("bar");
|
||||
assertThatThrownBy(() -> {
|
||||
context.get("foo", Integer.class);
|
||||
}).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.component.MultiItemSelector;
|
||||
import org.springframework.shell.component.PathInput;
|
||||
import org.springframework.shell.component.SingleItemSelector;
|
||||
import org.springframework.shell.component.StringInput;
|
||||
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
|
||||
import org.springframework.shell.component.PathInput.PathInputContext;
|
||||
import org.springframework.shell.component.SingleItemSelector.SingleItemSelectorContext;
|
||||
import org.springframework.shell.component.StringInput.StringInputContext;
|
||||
import org.springframework.shell.component.support.SelectorItem;
|
||||
import org.springframework.shell.standard.AbstractShellComponent;
|
||||
import org.springframework.shell.standard.ShellComponent;
|
||||
import org.springframework.shell.standard.ShellMethod;
|
||||
import org.springframework.shell.style.TemplateExecutor;
|
||||
|
||||
@ShellComponent
|
||||
public class ComponentCommands extends AbstractShellComponent implements ResourceLoaderAware {
|
||||
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
@Autowired
|
||||
private TemplateExecutor templateExecutor;
|
||||
|
||||
@Override
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component string", value = "String input", group = "Components")
|
||||
public String stringInput() {
|
||||
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
|
||||
component.setResourceLoader(resourceLoader);
|
||||
component.setTemplateExecutor(templateExecutor);
|
||||
StringInputContext context = component.run(StringInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component path", value = "Path input", group = "Components")
|
||||
public String pathInput() {
|
||||
PathInput component = new PathInput(getTerminal(), "Enter value");
|
||||
component.setResourceLoader(resourceLoader);
|
||||
component.setTemplateExecutor(templateExecutor);
|
||||
PathInputContext context = component.run(PathInputContext.empty());
|
||||
return "Got value " + context.getResultValue();
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
|
||||
public String singleSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2"));
|
||||
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(resourceLoader);
|
||||
component.setTemplateExecutor(templateExecutor);
|
||||
SingleItemSelectorContext<String, SelectorItem<String>> context = component
|
||||
.run(SingleItemSelectorContext.empty());
|
||||
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
|
||||
return "Got value " + result;
|
||||
}
|
||||
|
||||
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
|
||||
public String multiSelector() {
|
||||
List<SelectorItem<String>> items = new ArrayList<>();
|
||||
items.add(SelectorItem.of("key1", "value1"));
|
||||
items.add(SelectorItem.of("key2", "value2", false));
|
||||
items.add(SelectorItem.of("key3", "value3"));
|
||||
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
|
||||
items, "testSimple", null);
|
||||
component.setResourceLoader(resourceLoader);
|
||||
component.setTemplateExecutor(templateExecutor);
|
||||
MultiItemSelectorContext<String, SelectorItem<String>> context = component
|
||||
.run(MultiItemSelectorContext.empty());
|
||||
String result = context.getResultItems().stream()
|
||||
.map(si -> si.getItem())
|
||||
.collect(Collectors.joining(","));
|
||||
return "Got value " + result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user