Skip to content

Commit fe5e357

Browse files
committed
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 spring-projects#360
1 parent 65ff382 commit fe5e357

28 files changed

+3432
-1
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<jline.version>3.21.0</jline.version>
2626
<jcommander.version>1.81</jcommander.version>
2727
<antlr-st4.version>4.3.1</antlr-st4.version>
28+
<jimfs.version>1.2</jimfs.version>
2829
</properties>
2930

3031
<modules>
@@ -110,6 +111,11 @@
110111
<artifactId>ST4</artifactId>
111112
<version>${antlr-st4.version}</version>
112113
</dependency>
114+
<dependency>
115+
<groupId>com.google.jimfs</groupId>
116+
<artifactId>jimfs</artifactId>
117+
<version>${jimfs.version}</version>
118+
</dependency>
113119
</dependencies>
114120
</dependencyManagement>
115121

spring-shell-core/pom.xml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@
5252
<artifactId>assertj-core</artifactId>
5353
<scope>test</scope>
5454
</dependency>
55-
55+
<dependency>
56+
<groupId>org.awaitility</groupId>
57+
<artifactId>awaitility</artifactId>
58+
<scope>test</scope>
59+
</dependency>
60+
<dependency>
61+
<groupId>com.google.jimfs</groupId>
62+
<artifactId>jimfs</artifactId>
63+
<scope>test</scope>
64+
</dependency>
5665
</dependencies>
5766
</project>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component;
17+
18+
import java.util.Collections;
19+
import java.util.Comparator;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.function.Function;
24+
import java.util.stream.Collectors;
25+
26+
import org.jline.terminal.Terminal;
27+
import org.jline.utils.AttributedString;
28+
29+
import org.springframework.shell.component.context.ComponentContext;
30+
import org.springframework.shell.component.support.AbstractSelectorComponent;
31+
import org.springframework.shell.component.support.Enableable;
32+
import org.springframework.shell.component.support.Itemable;
33+
import org.springframework.shell.component.support.Matchable;
34+
import org.springframework.shell.component.support.Nameable;
35+
import org.springframework.shell.component.support.AbstractSelectorComponent.SelectorComponentContext;
36+
import org.springframework.shell.component.MultiItemSelector.MultiItemSelectorContext;
37+
38+
/**
39+
* Component able to pick multiple items.
40+
*
41+
* @author Janne Valkealahti
42+
*/
43+
public class MultiItemSelector<T, I extends Nameable & Matchable & Enableable & Itemable<T>>
44+
extends AbstractSelectorComponent<T, MultiItemSelectorContext<T, I>, I> {
45+
46+
private MultiItemSelectorContext<T, I> currentContext;
47+
48+
public MultiItemSelector(Terminal terminal, List<I> items, String name, Comparator<I> comparator) {
49+
super(terminal, name, items, false, comparator);
50+
setRenderer(new DefaultRenderer());
51+
setTemplateLocation("classpath:org/springframework/shell/component/multi-item-selector-default.stg");
52+
}
53+
54+
@Override
55+
protected MultiItemSelectorContext<T, I> getThisContext(ComponentContext<?> context) {
56+
if (context != null && currentContext == context) {
57+
return currentContext;
58+
}
59+
currentContext = MultiItemSelectorContext.empty(getItemMapper());
60+
currentContext.setName(name);
61+
if (currentContext.getItems() == null) {
62+
currentContext.setItems(getItems());
63+
}
64+
context.stream().forEach(e -> {
65+
currentContext.put(e.getKey(), e.getValue());
66+
});
67+
return currentContext;
68+
}
69+
70+
@Override
71+
protected MultiItemSelectorContext<T, I> runInternal(MultiItemSelectorContext<T, I> context) {
72+
super.runInternal(context);
73+
loop(context);
74+
return context;
75+
}
76+
77+
/**
78+
* Context {@link MultiItemSelector}.
79+
*/
80+
public interface MultiItemSelectorContext<T, I extends Nameable & Matchable & Itemable<T>>
81+
extends SelectorComponentContext<T, I, MultiItemSelectorContext<T, I>> {
82+
83+
/**
84+
* Gets a values.
85+
*
86+
* @return a values
87+
*/
88+
List<String> getValues();
89+
90+
/**
91+
* Creates an empty {@link MultiItemSelectorContext}.
92+
*
93+
* @return empty context
94+
*/
95+
static <T, I extends Nameable & Matchable & Itemable<T>> MultiItemSelectorContext<T, I> empty() {
96+
return new DefaultMultiItemSelectorContext<>();
97+
}
98+
99+
/**
100+
* Creates an {@link MultiItemSelectorContext}.
101+
*
102+
* @return context
103+
*/
104+
static <T, I extends Nameable & Matchable & Itemable<T>> MultiItemSelectorContext<T, I> empty(Function<T, String> itemMapper) {
105+
return new DefaultMultiItemSelectorContext<>(itemMapper);
106+
}
107+
}
108+
109+
private static class DefaultMultiItemSelectorContext<T, I extends Nameable & Matchable & Itemable<T>> extends
110+
BaseSelectorComponentContext<T, I, MultiItemSelectorContext<T, I>> implements MultiItemSelectorContext<T, I> {
111+
112+
private Function<T, String> itemMapper = item -> item.toString();
113+
114+
DefaultMultiItemSelectorContext() {
115+
}
116+
117+
DefaultMultiItemSelectorContext(Function<T, String> itemMapper) {
118+
this.itemMapper = itemMapper;
119+
}
120+
121+
@Override
122+
public List<String> getValues() {
123+
if (getResultItems() == null) {
124+
return Collections.emptyList();
125+
}
126+
return getResultItems().stream()
127+
.map(i -> i.getItem())
128+
.map(i -> itemMapper.apply(i))
129+
.collect(Collectors.toList());
130+
}
131+
132+
@Override
133+
public Map<String, Object> toTemplateModel() {
134+
Map<String, Object> attributes = super.toTemplateModel();
135+
attributes.put("values", getValues());
136+
List<Map<String, Object>> rows = getItemStateView().stream()
137+
.map(is -> {
138+
Map<String, Object> map = new HashMap<>();
139+
map.put("name", is.getName());
140+
map.put("selected", is.isSelected());
141+
map.put("onrow", getCursorRow().intValue() == is.getIndex());
142+
map.put("enabled", is.isEnabled());
143+
return map;
144+
})
145+
.collect(Collectors.toList());
146+
attributes.put("rows", rows);
147+
// finally wrap it into 'model' as that's what
148+
// we expect in stg template.
149+
Map<String, Object> model = new HashMap<>();
150+
model.put("model", attributes);
151+
return model;
152+
}
153+
}
154+
155+
private class DefaultRenderer implements Function<MultiItemSelectorContext<T, I>, List<AttributedString>> {
156+
157+
@Override
158+
public List<AttributedString> apply(MultiItemSelectorContext<T, I> context) {
159+
return renderTemplateResource(context.toTemplateModel());
160+
}
161+
}
162+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.shell.component;
17+
18+
import java.nio.file.Files;
19+
import java.nio.file.Path;
20+
import java.nio.file.Paths;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.function.Function;
25+
26+
import org.jline.keymap.BindingReader;
27+
import org.jline.keymap.KeyMap;
28+
import org.jline.terminal.Terminal;
29+
import org.jline.utils.AttributedString;
30+
31+
import org.springframework.shell.component.PathInput.PathInputContext;
32+
import org.springframework.shell.component.context.ComponentContext;
33+
import org.springframework.shell.component.support.AbstractTextComponent;
34+
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext;
35+
import org.springframework.shell.component.support.AbstractTextComponent.TextComponentContext.MessageLevel;
36+
import org.springframework.util.StringUtils;;
37+
38+
/**
39+
* Component for a simple path input.
40+
*
41+
* @author Janne Valkealahti
42+
*/
43+
public class PathInput extends AbstractTextComponent<Path, PathInputContext> {
44+
45+
private PathInputContext currentContext;
46+
private Function<String, Path> pathProvider = (path) -> Paths.get(path);
47+
48+
public PathInput(Terminal terminal) {
49+
this(terminal, null);
50+
}
51+
52+
public PathInput(Terminal terminal, String name) {
53+
this(terminal, name, null);
54+
}
55+
56+
public PathInput(Terminal terminal, String name, Function<PathInputContext, List<AttributedString>> renderer) {
57+
super(terminal, name, null);
58+
setRenderer(renderer != null ? renderer : new DefaultRenderer());
59+
setTemplateLocation("classpath:org/springframework/shell/component/path-input-default.stg");
60+
}
61+
62+
@Override
63+
protected PathInputContext getThisContext(ComponentContext<?> context) {
64+
if (context != null && currentContext == context) {
65+
return currentContext;
66+
}
67+
currentContext = PathInputContext.empty();
68+
currentContext.setName(getName());
69+
context.stream().forEach(e -> {
70+
currentContext.put(e.getKey(), e.getValue());
71+
});
72+
return currentContext;
73+
}
74+
75+
@Override
76+
protected boolean read(BindingReader bindingReader, KeyMap<String> keyMap, PathInputContext context) {
77+
String operation = bindingReader.readBinding(keyMap);
78+
String input;
79+
switch (operation) {
80+
case OPERATION_CHAR:
81+
String lastBinding = bindingReader.getLastBinding();
82+
input = context.getInput();
83+
if (input == null) {
84+
input = lastBinding;
85+
}
86+
else {
87+
input = input + lastBinding;
88+
}
89+
context.setInput(input);
90+
checkPath(input, context);
91+
break;
92+
case OPERATION_BACKSPACE:
93+
input = context.getInput();
94+
if (StringUtils.hasLength(input)) {
95+
input = input.length() > 1 ? input.substring(0, input.length() - 1) : null;
96+
}
97+
context.setInput(input);
98+
checkPath(input, context);
99+
break;
100+
case OPERATION_EXIT:
101+
if (StringUtils.hasText(context.getInput())) {
102+
context.setResultValue(Paths.get(context.getInput()));
103+
}
104+
return true;
105+
default:
106+
break;
107+
}
108+
return false;
109+
}
110+
111+
/**
112+
* Sets a path provider.
113+
*
114+
* @param pathProvider the path provider
115+
*/
116+
public void setPathProvider(Function<String, Path> pathProvider) {
117+
this.pathProvider = pathProvider;
118+
}
119+
120+
/**
121+
* Resolves a {@link Path} from a given raw {@code path}.
122+
*
123+
* @param path the raw path
124+
* @return a resolved path
125+
*/
126+
protected Path resolvePath(String path) {
127+
return this.pathProvider.apply(path);
128+
}
129+
130+
private void checkPath(String path, PathInputContext context) {
131+
if (!StringUtils.hasText(path)) {
132+
context.setMessage(null);
133+
return;
134+
}
135+
Path p = resolvePath(path);
136+
boolean isDirectory = Files.isDirectory(p);
137+
if (isDirectory) {
138+
context.setMessage("Directory exists", MessageLevel.ERROR);
139+
}
140+
else {
141+
context.setMessage("Path ok", MessageLevel.INFO);
142+
}
143+
}
144+
145+
public interface PathInputContext extends TextComponentContext<Path, PathInputContext> {
146+
147+
/**
148+
* Gets an empty {@link PathInputContext}.
149+
*
150+
* @return empty path input context
151+
*/
152+
public static PathInputContext empty() {
153+
return new DefaultPathInputContext();
154+
}
155+
}
156+
157+
private static class DefaultPathInputContext extends BaseTextComponentContext<Path, PathInputContext>
158+
implements PathInputContext {
159+
160+
@Override
161+
public Map<String, Object> toTemplateModel() {
162+
Map<String, Object> attributes = super.toTemplateModel();
163+
Map<String, Object> model = new HashMap<>();
164+
model.put("model", attributes);
165+
return model;
166+
}
167+
}
168+
169+
private class DefaultRenderer implements Function<PathInputContext, List<AttributedString>> {
170+
171+
@Override
172+
public List<AttributedString> apply(PathInputContext context) {
173+
return renderTemplateResource(context.toTemplateModel());
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)