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
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
* Copyright 2023-2024 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.
|
||||
@@ -23,6 +23,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.shell.component.ViewComponentBuilder;
|
||||
import org.springframework.shell.component.ViewComponentExecutor;
|
||||
import org.springframework.shell.component.view.TerminalUI;
|
||||
import org.springframework.shell.component.view.TerminalUIBuilder;
|
||||
import org.springframework.shell.component.view.TerminalUICustomizer;
|
||||
@@ -45,4 +47,18 @@ public class TerminalUIAutoConfiguration {
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean
|
||||
public ViewComponentBuilder viewComponentBuilder(TerminalUIBuilder terminalUIBuilder,
|
||||
ViewComponentExecutor viewComponentExecutor, Terminal terminal) {
|
||||
return new ViewComponentBuilder(terminalUIBuilder, viewComponentExecutor, terminal);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ViewComponentExecutor viewComponentExecutor() {
|
||||
return new ViewComponentExecutor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.shell.component;
|
||||
|
||||
import org.jline.terminal.Size;
|
||||
import org.jline.terminal.Terminal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.shell.component.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.view.TerminalUI;
|
||||
@@ -33,11 +35,13 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class ViewComponent {
|
||||
|
||||
private final static Logger log = LoggerFactory.getLogger(ViewComponent.class);
|
||||
private final Terminal terminal;
|
||||
private final View view;
|
||||
private EventLoop eventLoop;
|
||||
private TerminalUI ui;
|
||||
private TerminalUI terminalUI;
|
||||
private boolean useTerminalWidth = true;
|
||||
private ViewComponentExecutor viewComponentExecutor;
|
||||
|
||||
/**
|
||||
* Construct view component with a given {@link Terminal} and {@link View}.
|
||||
@@ -45,19 +49,36 @@ public class ViewComponent {
|
||||
* @param terminal the terminal
|
||||
* @param view the main view
|
||||
*/
|
||||
public ViewComponent(Terminal terminal, View view) {
|
||||
public ViewComponent(TerminalUI terminalUI, Terminal terminal, ViewComponentExecutor viewComponentExecutor,
|
||||
View view) {
|
||||
Assert.notNull(terminalUI, "terminal ui must be set");
|
||||
Assert.notNull(terminal, "terminal must be set");
|
||||
Assert.notNull(view, "view must be set");
|
||||
this.terminalUI = terminalUI;
|
||||
this.terminal = terminal;
|
||||
this.view = view;
|
||||
this.ui = new TerminalUI(terminal);
|
||||
this.eventLoop = ui.getEventLoop();
|
||||
this.viewComponentExecutor = viewComponentExecutor;
|
||||
this.eventLoop = terminalUI.getEventLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a component asyncronously. Returned state can be used to wait, cancel or
|
||||
* see its completion status.
|
||||
*
|
||||
* @return run state
|
||||
*/
|
||||
public ViewComponentRun runAsync() {
|
||||
ViewComponentRun run = viewComponentExecutor.start(() -> {
|
||||
runBlocking();
|
||||
});
|
||||
return run;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a view execution loop.
|
||||
*/
|
||||
public void run() {
|
||||
public void runBlocking() {
|
||||
log.debug("Start run()");
|
||||
eventLoop.onDestroy(eventLoop.viewEvents(ViewDoneEvent.class, view)
|
||||
.subscribe(event -> {
|
||||
exit();
|
||||
@@ -69,8 +90,9 @@ public class ViewComponent {
|
||||
if (useTerminalWidth) {
|
||||
view.setRect(rect.x(), rect.y(), terminalSize.getColumns() - rect.x(), rect.height());
|
||||
}
|
||||
ui.setRoot(view, false);
|
||||
ui.run();
|
||||
terminalUI.setRoot(view, false);
|
||||
terminalUI.run();
|
||||
log.debug("End run()");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,4 +120,28 @@ public class ViewComponent {
|
||||
eventLoop.dispatch(ShellMessageBuilder.ofInterrupt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represent run state of an async run of a component.
|
||||
*/
|
||||
public interface ViewComponentRun {
|
||||
|
||||
/**
|
||||
* Await component termination.
|
||||
*/
|
||||
void await();
|
||||
|
||||
/**
|
||||
* Cancel component run.
|
||||
*/
|
||||
void cancel();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if component run has completed.
|
||||
*
|
||||
* @return {@code true} if component run has completed
|
||||
*/
|
||||
boolean isDone();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2024 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 org.jline.terminal.Terminal;
|
||||
|
||||
import org.springframework.shell.component.view.TerminalUIBuilder;
|
||||
import org.springframework.shell.component.view.control.View;
|
||||
|
||||
/**
|
||||
* Builder that can be used to configure and create a {@link ViewComponent}.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ViewComponentBuilder {
|
||||
|
||||
private final TerminalUIBuilder terminalUIBuilder;
|
||||
private final ViewComponentExecutor viewComponentExecutor;
|
||||
private final Terminal terminal;
|
||||
|
||||
public ViewComponentBuilder(TerminalUIBuilder terminalUIBuilder, ViewComponentExecutor viewComponentExecutor,
|
||||
Terminal terminal) {
|
||||
this.terminalUIBuilder = terminalUIBuilder;
|
||||
this.viewComponentExecutor = viewComponentExecutor;
|
||||
this.terminal = terminal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link ViewComponent} instance and configure it using this builder.
|
||||
*
|
||||
* @param view the view to use with view component
|
||||
* @return a configured {@link ViewComponent} instance.
|
||||
*/
|
||||
public ViewComponent build(View view) {
|
||||
return new ViewComponent(terminalUIBuilder.build(), terminal, viewComponentExecutor, view);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2024 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.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.shell.component.ViewComponent.ViewComponentRun;
|
||||
|
||||
/**
|
||||
* Executor for {@code ViewComponent}. Purpose of this executor is to run
|
||||
* component in a thread so that it doesn't need to block from a command.
|
||||
*
|
||||
* @author Janne Valkealahti
|
||||
*/
|
||||
public class ViewComponentExecutor implements AutoCloseable {
|
||||
|
||||
private final Logger log = LoggerFactory.getLogger(ViewComponentExecutor.class);
|
||||
private final SimpleAsyncTaskExecutor executor;
|
||||
private Future<?> future;
|
||||
|
||||
public ViewComponentExecutor() {
|
||||
this.executor = new SimpleAsyncTaskExecutor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
this.executor.close();
|
||||
}
|
||||
|
||||
private static class FutureViewComponentRun implements ViewComponentRun {
|
||||
|
||||
private Future<?> future;
|
||||
|
||||
private FutureViewComponentRun(Future<?> future) {
|
||||
this.future = future;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void await() {
|
||||
try {
|
||||
this.future.get();
|
||||
} catch (InterruptedException e) {
|
||||
} catch (ExecutionException e) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
this.future.cancel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return this.future.isDone();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute runnable and return state which can be used for further operations.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
* @return run state
|
||||
*/
|
||||
public ViewComponentRun start(Runnable runnable) {
|
||||
if (future != null && !future.isDone()) {
|
||||
throw new IllegalStateException("Can run component as there is existing one in non stopped state");
|
||||
}
|
||||
future = executor.submit(() -> {
|
||||
log.debug("About to run component");
|
||||
runnable.run();
|
||||
log.debug("Finished run component");
|
||||
});
|
||||
return new FutureViewComponentRun(future);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a {@code ViewComponent} which has been previously started with this
|
||||
* executor.
|
||||
*/
|
||||
public void stop() {
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
future = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.shell.command.annotation.Command;
|
||||
import org.springframework.shell.component.ViewComponent;
|
||||
import org.springframework.shell.component.ViewComponent.ViewComponentRun;
|
||||
import org.springframework.shell.component.message.ShellMessageBuilder;
|
||||
import org.springframework.shell.component.message.ShellMessageHeaderAccessor;
|
||||
import org.springframework.shell.component.message.StaticShellMessageHeaderAccessor;
|
||||
@@ -90,14 +91,15 @@ public class ComponentUiCommands extends AbstractShellComponent {
|
||||
public String stringInput() {
|
||||
InputView view = new InputView();
|
||||
view.setRect(0, 0, 10, 1);
|
||||
ViewComponent component = new ViewComponent(getTerminal(), view);
|
||||
component.run();
|
||||
ViewComponent component = getViewComponentBuilder().build(view);
|
||||
component.runBlocking();
|
||||
String input = view.getInputText();
|
||||
return String.format("Input was '%s'", input);
|
||||
}
|
||||
|
||||
private void runProgress(ProgressView view) {
|
||||
ViewComponent component = new ViewComponent(getTerminal(), view);
|
||||
ViewComponent component = getViewComponentBuilder().build(view);
|
||||
|
||||
EventLoop eventLoop = component.getEventLoop();
|
||||
|
||||
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
|
||||
@@ -118,7 +120,7 @@ public class ComponentUiCommands extends AbstractShellComponent {
|
||||
}
|
||||
}));
|
||||
|
||||
component.run();
|
||||
component.runAsync().await();
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress1")
|
||||
@@ -166,4 +168,60 @@ public class ComponentUiCommands extends AbstractShellComponent {
|
||||
runProgress(view);
|
||||
}
|
||||
|
||||
@Command(command = "componentui progress5")
|
||||
public void progress5() {
|
||||
ProgressView view = new ProgressView(0, 100,
|
||||
ProgressViewItem.ofText(10, HorizontalAlign.LEFT),
|
||||
ProgressViewItem.ofSpinner(3, HorizontalAlign.LEFT),
|
||||
ProgressViewItem.ofPercent(0, HorizontalAlign.RIGHT));
|
||||
|
||||
view.setDescription("name");
|
||||
view.setRect(0, 0, 20, 1);
|
||||
view.start();
|
||||
|
||||
ViewComponent component = getViewComponentBuilder().build(view);
|
||||
component.setUseTerminalWidth(false);
|
||||
|
||||
Flux<Message<?>> ticks = Flux.interval(Duration.ofMillis(100)).map(l -> {
|
||||
Message<Long> message = MessageBuilder
|
||||
.withPayload(l)
|
||||
.setHeader(ShellMessageHeaderAccessor.EVENT_TYPE, EventLoop.Type.USER)
|
||||
.build();
|
||||
return message;
|
||||
});
|
||||
EventLoop eventLoop = component.getEventLoop();
|
||||
eventLoop.dispatch(ticks);
|
||||
eventLoop.onDestroy(eventLoop.events()
|
||||
.filter(m -> EventLoop.Type.USER.equals(StaticShellMessageHeaderAccessor.getEventType(m)))
|
||||
.subscribe(m -> {
|
||||
if (m.getPayload() instanceof Long) {
|
||||
view.tickAdvance(5);
|
||||
eventLoop.dispatch(ShellMessageBuilder.ofRedraw());
|
||||
}
|
||||
}));
|
||||
|
||||
ViewComponentRun run = component.runAsync();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
|
||||
if (run.isDone()) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
if (run.isDone()) {
|
||||
break;
|
||||
}
|
||||
|
||||
String msg = String.format("%s ", i);
|
||||
getTerminal().writer().write(msg + System.lineSeparator());
|
||||
getTerminal().writer().flush();
|
||||
|
||||
}
|
||||
|
||||
run.cancel();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2021-2022 the original author or authors.
|
||||
* Copyright 2021-2024 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.
|
||||
@@ -29,6 +29,7 @@ import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.shell.Shell;
|
||||
import org.springframework.shell.command.CommandCatalog;
|
||||
import org.springframework.shell.completion.CompletionResolver;
|
||||
import org.springframework.shell.component.ViewComponentBuilder;
|
||||
import org.springframework.shell.style.TemplateExecutor;
|
||||
import org.springframework.shell.style.ThemeResolver;
|
||||
|
||||
@@ -55,6 +56,8 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
|
||||
private ObjectProvider<ThemeResolver> themeResolverProvider;
|
||||
|
||||
private ObjectProvider<ViewComponentBuilder> viewComponentBuilderProvider;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
@@ -73,6 +76,7 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
completionResolverProvider = applicationContext.getBeanProvider(CompletionResolver.class);
|
||||
templateExecutorProvider = applicationContext.getBeanProvider(TemplateExecutor.class);
|
||||
themeResolverProvider = applicationContext.getBeanProvider(ThemeResolver.class);
|
||||
viewComponentBuilderProvider = applicationContext.getBeanProvider(ViewComponentBuilder.class);
|
||||
}
|
||||
|
||||
protected ApplicationContext getApplicationContext() {
|
||||
@@ -106,4 +110,8 @@ public abstract class AbstractShellComponent implements ApplicationContextAware,
|
||||
protected ThemeResolver getThemeResolver() {
|
||||
return themeResolverProvider.getObject();
|
||||
}
|
||||
|
||||
protected ViewComponentBuilder getViewComponentBuilder() {
|
||||
return viewComponentBuilderProvider.getObject();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user