Skip to content

Commit 7fa953f

Browse files
committed
ViewComponent async execution
- Modifies ViewComponent with changed api's so that it can be executed with a thread allowing caller not to block. - Add ViewComponentBuilder concept and create is as a bean similar to TerminalUIBuilder. - Relates #997
1 parent abc4ffa commit 7fa953f

File tree

6 files changed

+298
-13
lines changed

6 files changed

+298
-13
lines changed

Diff for: spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/TerminalUIAutoConfiguration.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 the original author or authors.
2+
* Copyright 2023-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@
2323
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2424
import org.springframework.context.annotation.Bean;
2525
import org.springframework.context.annotation.Scope;
26+
import org.springframework.shell.component.ViewComponentBuilder;
27+
import org.springframework.shell.component.ViewComponentExecutor;
2628
import org.springframework.shell.component.view.TerminalUI;
2729
import org.springframework.shell.component.view.TerminalUIBuilder;
2830
import org.springframework.shell.component.view.TerminalUICustomizer;
@@ -45,4 +47,18 @@ public TerminalUIBuilder terminalUIBuilder(Terminal terminal, ThemeResolver them
4547
return builder;
4648
}
4749

50+
@Bean
51+
@Scope("prototype")
52+
@ConditionalOnMissingBean
53+
public ViewComponentBuilder viewComponentBuilder(TerminalUIBuilder terminalUIBuilder,
54+
ViewComponentExecutor viewComponentExecutor, Terminal terminal) {
55+
return new ViewComponentBuilder(terminalUIBuilder, viewComponentExecutor, terminal);
56+
}
57+
58+
@Bean
59+
@ConditionalOnMissingBean
60+
public ViewComponentExecutor viewComponentExecutor() {
61+
return new ViewComponentExecutor();
62+
}
63+
4864
}

Diff for: spring-shell-core/src/main/java/org/springframework/shell/component/ViewComponent.java

+53-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import org.jline.terminal.Size;
1919
import org.jline.terminal.Terminal;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
2022

2123
import org.springframework.shell.component.message.ShellMessageBuilder;
2224
import org.springframework.shell.component.view.TerminalUI;
@@ -33,31 +35,50 @@
3335
*/
3436
public class ViewComponent {
3537

38+
private final static Logger log = LoggerFactory.getLogger(ViewComponent.class);
3639
private final Terminal terminal;
3740
private final View view;
3841
private EventLoop eventLoop;
39-
private TerminalUI ui;
42+
private TerminalUI terminalUI;
4043
private boolean useTerminalWidth = true;
44+
private ViewComponentExecutor viewComponentExecutor;
4145

4246
/**
4347
* Construct view component with a given {@link Terminal} and {@link View}.
4448
*
4549
* @param terminal the terminal
4650
* @param view the main view
4751
*/
48-
public ViewComponent(Terminal terminal, View view) {
52+
public ViewComponent(TerminalUI terminalUI, Terminal terminal, ViewComponentExecutor viewComponentExecutor,
53+
View view) {
54+
Assert.notNull(terminalUI, "terminal ui must be set");
4955
Assert.notNull(terminal, "terminal must be set");
5056
Assert.notNull(view, "view must be set");
57+
this.terminalUI = terminalUI;
5158
this.terminal = terminal;
5259
this.view = view;
53-
this.ui = new TerminalUI(terminal);
54-
this.eventLoop = ui.getEventLoop();
60+
this.viewComponentExecutor = viewComponentExecutor;
61+
this.eventLoop = terminalUI.getEventLoop();
62+
}
63+
64+
/**
65+
* Run a component asyncronously. Returned state can be used to wait, cancel or
66+
* see its completion status.
67+
*
68+
* @return run state
69+
*/
70+
public ViewComponentRun runAsync() {
71+
ViewComponentRun run = viewComponentExecutor.start(() -> {
72+
runBlocking();
73+
});
74+
return run;
5575
}
5676

5777
/**
5878
* Run a view execution loop.
5979
*/
60-
public void run() {
80+
public void runBlocking() {
81+
log.debug("Start run()");
6182
eventLoop.onDestroy(eventLoop.viewEvents(ViewDoneEvent.class, view)
6283
.subscribe(event -> {
6384
exit();
@@ -69,8 +90,9 @@ public void run() {
6990
if (useTerminalWidth) {
7091
view.setRect(rect.x(), rect.y(), terminalSize.getColumns() - rect.x(), rect.height());
7192
}
72-
ui.setRoot(view, false);
73-
ui.run();
93+
terminalUI.setRoot(view, false);
94+
terminalUI.run();
95+
log.debug("End run()");
7496
}
7597

7698
/**
@@ -98,4 +120,28 @@ public void exit() {
98120
eventLoop.dispatch(ShellMessageBuilder.ofInterrupt());
99121
}
100122

123+
/**
124+
* Represent run state of an async run of a component.
125+
*/
126+
public interface ViewComponentRun {
127+
128+
/**
129+
* Await component termination.
130+
*/
131+
void await();
132+
133+
/**
134+
* Cancel component run.
135+
*/
136+
void cancel();
137+
138+
/**
139+
* Returns {@code true} if component run has completed.
140+
*
141+
* @return {@code true} if component run has completed
142+
*/
143+
boolean isDone();
144+
145+
}
146+
101147
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2024 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 org.jline.terminal.Terminal;
19+
20+
import org.springframework.shell.component.view.TerminalUIBuilder;
21+
import org.springframework.shell.component.view.control.View;
22+
23+
/**
24+
* Builder that can be used to configure and create a {@link ViewComponent}.
25+
*
26+
* @author Janne Valkealahti
27+
*/
28+
public class ViewComponentBuilder {
29+
30+
private final TerminalUIBuilder terminalUIBuilder;
31+
private final ViewComponentExecutor viewComponentExecutor;
32+
private final Terminal terminal;
33+
34+
public ViewComponentBuilder(TerminalUIBuilder terminalUIBuilder, ViewComponentExecutor viewComponentExecutor,
35+
Terminal terminal) {
36+
this.terminalUIBuilder = terminalUIBuilder;
37+
this.viewComponentExecutor = viewComponentExecutor;
38+
this.terminal = terminal;
39+
}
40+
41+
/**
42+
* Build a new {@link ViewComponent} instance and configure it using this builder.
43+
*
44+
* @param view the view to use with view component
45+
* @return a configured {@link ViewComponent} instance.
46+
*/
47+
public ViewComponent build(View view) {
48+
return new ViewComponent(terminalUIBuilder.build(), terminal, viewComponentExecutor, view);
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2024 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.concurrent.ExecutionException;
19+
import java.util.concurrent.Future;
20+
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
import org.springframework.core.task.SimpleAsyncTaskExecutor;
25+
import org.springframework.shell.component.ViewComponent.ViewComponentRun;
26+
27+
/**
28+
* Executor for {@code ViewComponent}. Purpose of this executor is to run
29+
* component in a thread so that it doesn't need to block from a command.
30+
*
31+
* @author Janne Valkealahti
32+
*/
33+
public class ViewComponentExecutor implements AutoCloseable {
34+
35+
private final Logger log = LoggerFactory.getLogger(ViewComponentExecutor.class);
36+
private final SimpleAsyncTaskExecutor executor;
37+
private Future<?> future;
38+
39+
public ViewComponentExecutor() {
40+
this.executor = new SimpleAsyncTaskExecutor();
41+
}
42+
43+
@Override
44+
public void close() throws Exception {
45+
this.executor.close();
46+
}
47+
48+
private static class FutureViewComponentRun implements ViewComponentRun {
49+
50+
private Future<?> future;
51+
52+
private FutureViewComponentRun(Future<?> future) {
53+
this.future = future;
54+
}
55+
56+
@Override
57+
public void await() {
58+
try {
59+
this.future.get();
60+
} catch (InterruptedException e) {
61+
} catch (ExecutionException e) {
62+
}
63+
}
64+
65+
@Override
66+
public void cancel() {
67+
this.future.cancel(true);
68+
}
69+
70+
@Override
71+
public boolean isDone() {
72+
return this.future.isDone();
73+
}
74+
75+
}
76+
77+
/**
78+
* Execute runnable and return state which can be used for further operations.
79+
*
80+
* @param runnable the runnable
81+
* @return run state
82+
*/
83+
public ViewComponentRun start(Runnable runnable) {
84+
if (future != null && !future.isDone()) {
85+
throw new IllegalStateException("Can run component as there is existing one in non stopped state");
86+
}
87+
future = executor.submit(() -> {
88+
log.debug("About to run component");
89+
runnable.run();
90+
log.debug("Finished run component");
91+
});
92+
return new FutureViewComponentRun(future);
93+
}
94+
95+
/**
96+
* Stop a {@code ViewComponent} which has been previously started with this
97+
* executor.
98+
*/
99+
public void stop() {
100+
if (future != null) {
101+
future.cancel(true);
102+
}
103+
future = null;
104+
}
105+
106+
}

Diff for: spring-shell-samples/spring-shell-sample-commands/src/main/java/org/springframework/shell/samples/standard/ComponentUiCommands.java

+62-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.messaging.support.MessageBuilder;
2424
import org.springframework.shell.command.annotation.Command;
2525
import org.springframework.shell.component.ViewComponent;
26+
import org.springframework.shell.component.ViewComponent.ViewComponentRun;
2627
import org.springframework.shell.component.message.ShellMessageBuilder;
2728
import org.springframework.shell.component.message.ShellMessageHeaderAccessor;
2829
import org.springframework.shell.component.message.StaticShellMessageHeaderAccessor;
@@ -90,14 +91,15 @@ public void tui3() {
9091
public String stringInput() {
9192
InputView view = new InputView();
9293
view.setRect(0, 0, 10, 1);
93-
ViewComponent component = new ViewComponent(getTerminal(), view);
94-
component.run();
94+
ViewComponent component = getViewComponentBuilder().build(view);
95+
component.runBlocking();
9596
String input = view.getInputText();
9697
return String.format("Input was '%s'", input);
9798
}
9899

99100
private void runProgress(ProgressView view) {
100-
ViewComponent component = new ViewComponent(getTerminal(), view);
101+
ViewComponent component = getViewComponentBuilder().build(view);
102+
101103
EventLoop eventLoop = component.getEventLoop();
102104

103105
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
@@ -118,7 +120,7 @@ private void runProgress(ProgressView view) {
118120
}
119121
}));
120122

121-
component.run();
123+
component.runAsync().await();
122124
}
123125

124126
@Command(command = "componentui progress1")
@@ -166,4 +168,60 @@ public void progress4() {
166168
runProgress(view);
167169
}
168170

171+
@Command(command = "componentui progress5")
172+
public void progress5() {
173+
ProgressView view = new ProgressView(0, 100,
174+
ProgressViewItem.ofText(10, HorizontalAlign.LEFT),
175+
ProgressViewItem.ofSpinner(3, HorizontalAlign.LEFT),
176+
ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT));
177+
178+
view.setDescription("name");
179+
view.setRect(0, 0, 20, 1);
180+
view.start();
181+
182+
ViewComponent component = getViewComponentBuilder().build(view);
183+
component.setUseTerminalWidth(false);
184+
185+
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
186+
Message<Long> message = MessageBuilder
187+
.withPayload(l)
188+
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
189+
.build();
190+
return message;
191+
});
192+
EventLoop eventLoop = component.getEventLoop();
193+
eventLoop.dispatch(ticks);
194+
eventLoop.onDestroy(eventLoop.events()
195+
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
196+
.subscribe(m -> {
197+
if (m.getPayload() instanceof Long) {
198+
view.tickAdvance(5);
199+
eventLoop.dispatch(ShellMessageBuilder.ofRedraw());
200+
}
201+
}));
202+
203+
ViewComponentRun run = component.runAsync();
204+
205+
for (int i = 0; i < 4; i++) {
206+
207+
if (run.isDone()) {
208+
break;
209+
}
210+
try {
211+
Thread.sleep(2000);
212+
} catch (InterruptedException e) {
213+
}
214+
if (run.isDone()) {
215+
break;
216+
}
217+
218+
String msg = String.format("%s ", i);
219+
getTerminal().writer().write(msg + System.lineSeparator());
220+
getTerminal().writer().flush();
221+
222+
}
223+
224+
run.cancel();
225+
}
226+
169227
}

0 commit comments

Comments
 (0)