diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/Dialogs.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/Dialogs.java index d7c2ca5d47..3571857c2b 100644 --- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/Dialogs.java +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/Dialogs.java @@ -24,12 +24,12 @@ import io.jmix.flowui.app.inputdialog.InputDialog; import io.jmix.flowui.app.inputdialog.InputParameter; import io.jmix.flowui.component.validation.ValidationErrors; +import io.jmix.flowui.backgroundtask.BackgroundTask; import io.jmix.flowui.kit.action.Action; import io.jmix.flowui.view.DialogWindow; import io.jmix.flowui.view.View; import javax.annotation.Nullable; -import java.util.Locale; import java.util.function.Consumer; import java.util.function.Function; @@ -97,6 +97,25 @@ public interface Dialogs { */ InputDialogBuilder createInputDialog(View origin); + /** + * Creates background task dialog builder. + *
+ * Example of showing a background task dialog: + *
+     * dialogs.createBackgroundTaskDialog(backgroundTask)
+     *         .withHeader("Task")
+     *         .withText("My Task is Running")
+     *         .withTotal(10)
+     *         .withShowProgressInPercentage(true)
+     *         .withCancelAllowed(true)
+     *         .open();
+     * 
+ * + * @param backgroundTask background task to run + * @return builder + */ + BackgroundTaskDialogBuilder createBackgroundTaskDialog(BackgroundTask backgroundTask); + interface OptionDialogBuilder extends DialogBuilder, HasText, HasContent, @@ -326,6 +345,67 @@ enum LabelsPosition { } } + /** + * Builder of background task dialog. + */ + interface BackgroundTaskDialogBuilder extends + HasHeader>, + HasText>, + HasTheme>, + HasStyle>, + Draggable>, + Resizable> { + + /** + * Determines whether the dialog can be closed. + *

+ * The default value is {@code false}. + * + * @param cancelAllowed {@code true} if dialog is closeable + * @return builder + */ + BackgroundTaskDialogBuilder withCancelAllowed(boolean cancelAllowed); + + /** + * @return {@code true} if the dialog can be closed + */ + boolean isCancelAllowed(); + + /** + * Sets amount of items to be processed by background task. + *
+ * Use {@link io.jmix.flowui.backgroundtask.TaskLifeCycle#publish(Object[])} to notify the dialog about progress + * completion. + * + * @param total amount of items to be processed by background task, + * @return builder + */ + BackgroundTaskDialogBuilder withTotal(Number total); + + /** + * @return amount of items to be processed by background task + */ + Number getTotal(); + + /** + * Sets whether progress should be represented as percentage (rather than as raw number). + * + * @param percentProgress {@code true} to show progress in percents + * @return builder + */ + BackgroundTaskDialogBuilder withShowProgressInPercentage(boolean percentProgress); + + /** + * @return {@code true} if progress should is shown in percents + */ + boolean isShowProgressInPercentage(); + + /** + * Opens the dialog. + */ + void open(); + } + /** * Base class for all Dialog Builders. * diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/FlowuiScheduleConfiguration.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/FlowuiScheduleConfiguration.java new file mode 100644 index 0000000000..a3c3ff3cd1 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/FlowuiScheduleConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui; + +import io.jmix.core.TimeSource; +import io.jmix.flowui.backgroundtask.FlowuiBackgroundTaskProperties; +import io.jmix.flowui.backgroundtask.BackgroundTaskWatchDog; +import io.jmix.flowui.backgroundtask.impl.BackgroundTaskWatchDogImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration(proxyBeanMethods = false) +public class FlowuiScheduleConfiguration { + + @Bean("flowui_ThreadPoolTaskScheduler") + public TaskScheduler threadPoolTaskScheduler() { + ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); + threadPoolTaskScheduler.setThreadNamePrefix("flowui_backgroundScheduler-"); + threadPoolTaskScheduler.setPoolSize(1); + threadPoolTaskScheduler.setDaemon(true); + return threadPoolTaskScheduler; + } + + @Bean("flowui_BackgroundWorkerWatchDog") + public BackgroundTaskWatchDog watchDog(FlowuiBackgroundTaskProperties properties, TimeSource timeSource) { + return new BackgroundTaskWatchDogImpl(properties, timeSource); + } +} diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTask.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTask.java new file mode 100644 index 0000000000..34e091e880 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTask.java @@ -0,0 +1,277 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import io.jmix.flowui.view.View; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Background task for execute by {@link BackgroundWorker}. + *
+ * If the task is associated with a view through "view" constructor parameter, it will be canceled when + * the view is closed. + *
+ * If timeout passed to constructor is exceeded, the task is canceled by special {@link BackgroundTaskWatchDog} thread. + *
+ * Simplest usage example: + *

+ *    BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(10, this) {
+ *        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
+ *            for (int i = 0; i < 5; i++) {
+ *                TimeUnit.SECONDS.sleep(1);
+ *            }
+ *            return null;
+ *        }
+ *    };
+ *    BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
+ *    taskHandler.execute();
+ * 
+ * + * @param task progress measurement unit + * @param result type + */ +@SuppressWarnings("unused") +public abstract class BackgroundTask { + + private final View view; + + private final long timeoutMilliseconds; + + private final List> progressListeners = Collections.synchronizedList(new ArrayList<>()); + + /** + * Creates a task with timeout. + * + * @param timeout timeout + * @param timeUnit timeout time unit + * @param view owner view + */ + protected BackgroundTask(long timeout, TimeUnit timeUnit, View view) { + this.view = view; + this.timeoutMilliseconds = timeUnit.toMillis(timeout); + } + + /** + * Creates a task with timeout. + *

+ * The task will not be associated with any {@link View}. + * + * @param timeout timeout + * @param timeUnit timeout time unit + */ + protected BackgroundTask(long timeout, TimeUnit timeUnit) { + this.view = null; + this.timeoutMilliseconds = timeUnit.toMillis(timeout); + } + + /** + * Creates a task with timeout in default {@link TimeUnit#SECONDS} unit. + *
+ * The task will not be associated with any {@link View}. + * + * @param timeoutSeconds timeout in seconds + */ + protected BackgroundTask(long timeoutSeconds) { + this.view = null; + this.timeoutMilliseconds = TimeUnit.SECONDS.toMillis(timeoutSeconds); + } + + /** + * Create a task with timeout in default {@link TimeUnit#SECONDS} unit. + * + * @param timeoutSeconds timeout in seconds + * @param view owner view + */ + protected BackgroundTask(long timeoutSeconds, View view) { + this.view = view; + this.timeoutMilliseconds = TimeUnit.SECONDS.toMillis(timeoutSeconds); + } + + /** + * Main method that performs a task. + *
Called by the execution environment in a separate working thread. + * + *
Implementation of this method should support interruption: + *

    + *
  • In long loops check {@link TaskLifeCycle#isInterrupted()} and return if it is true
  • + *
  • Don't swallow {@link InterruptedException} - return from the method or don't catch it at all
  • + *
+ * + * @param taskLifeCycle lifecycle object that allows the main method to interact with the execution environment + * @return task result + * @throws Exception exception in working thread + */ + public abstract V run(TaskLifeCycle taskLifeCycle) throws Exception; + + /** + * Called by the execution environment in UI thread when the task is completed. + * + * @param result result of execution returned by {@link #run(TaskLifeCycle)} method + */ + public void done(V result) { + } + + /** + * Called by the execution environment in UI thread if the task is canceled by + * {@link BackgroundTaskHandler#cancel()} invocation. + *
+ * This method is not called in case of timeout expiration or owner view closing. + */ + public void canceled() { + } + + /** + * Called by the execution environment in UI thread if the task timeout is exceeded. + * + * @return true if this method implementation actually handles this event. Used for chaining handlers. + */ + public boolean handleTimeoutException() { + return false; + } + + /** + * Called by the execution environment in UI thread if the task {@link #run(TaskLifeCycle)} method raised an + * exception. + * + * @param ex exception + * @return true if this method implementation actually handles the exception. Used for chaining handlers. + */ + public boolean handleException(Exception ex) { + return false; + } + + /** + * Called by the execution environment in UI thread on progress change. + * + * @param changes list of changes since previous invocation + */ + public void progress(List changes) { + } + + /** + * Called by the execution environment in UI thread to prepare some execution parameters. These parameters can be + * requested by the working thread inside the {@link #run(TaskLifeCycle)} method by calling + * {@link TaskLifeCycle#getParams()}. + * + * @return parameters map or null if parameters are not needed + */ + @Nullable + public Map getParams() { + return null; + } + + /** + * @return owner view + */ + @Nullable + public final View getOwnerView() { + return view; + } + + /** + * @return timeout in ms + */ + public final long getTimeoutMilliseconds() { + return timeoutMilliseconds; + } + + /** + * @return timeout in sec + */ + public final long getTimeoutSeconds() { + return TimeUnit.MILLISECONDS.toSeconds(timeoutMilliseconds); + } + + /** + * Add additional progress listener. + * + * @param progressListener listener + */ + public final void addProgressListener(ProgressListener progressListener) { + if (!progressListeners.contains(progressListener)) + progressListeners.add(progressListener); + } + + /** + * Additional progress listeners. + * + * @return copy of the progress listeners collection + */ + public final List> getProgressListeners() { + return new ArrayList<>(progressListeners); + } + + /** + * Removes a progress listener. + * + * @param progressListener listener + */ + public final void removeProgressListener(ProgressListener progressListener) { + progressListeners.remove(progressListener); + } + + /** + * Listener of the task life cycle events, complementary to the tasks own methods: + * {@link BackgroundTask#progress(List)}, {@link BackgroundTask#done(Object)}, + * {@link BackgroundTask#canceled()}. + * + * @param progress measurement unit + * @param result type + */ + public interface ProgressListener { + + /** + * Called by the execution environment in UI thread on progress change. + * + * @param changes list of changes since previous invocation + */ + void onProgress(List changes); + + /** + * Called by the execution environment in UI thread when the task is completed. + * + * @param result result of execution returned by {@link #run(TaskLifeCycle)} method + */ + void onDone(V result); + + /** + * Called by the execution environment in UI thread if the task is canceled. + */ + void onCancel(); + } + + public static class ProgressListenerAdapter implements ProgressListener { + + @Override + public void onProgress(List changes) { + } + + @Override + public void onDone(V result) { + } + + @Override + public void onCancel() { + } + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskHandler.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskHandler.java new file mode 100644 index 0000000000..c704a50e74 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import javax.annotation.Nullable; + +/** + * Task handler for {@link BackgroundTask}. + * + * @param type of task's result + */ +public interface BackgroundTaskHandler { + + /** + * Executes the {@link BackgroundTask}. + *
+ * This method must be called only once for a handler instance. + */ + @ExecutedOnUIThread + void execute(); + + /** + * Cancels task. + * + * @return {@code true} if canceled, {@code false} if the task was not started or is already stopped + */ + @ExecutedOnUIThread + boolean cancel(); + + /** + * Waits for the task completion and return its result. + * + * @return task's result returned from {@link BackgroundTask#run(TaskLifeCycle)} method + */ + @ExecutedOnUIThread + @Nullable + V getResult(); + + /** + * @return {@code true} if the task is completed + */ + boolean isDone(); + + /** + * @return {@code true} if the task has been canceled + */ + boolean isCancelled(); + + /** + * @return {@code true} if the task is running + */ + boolean isAlive(); +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskManager.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskManager.java new file mode 100644 index 0000000000..85b863a430 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskManager.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import com.vaadin.flow.server.VaadinSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.Future; + +public class BackgroundTaskManager { + private static final Logger log = LoggerFactory.getLogger(BackgroundTaskManager.class); + + private transient Set> taskSet; + + public BackgroundTaskManager() { + taskSet = Collections.synchronizedSet(new LinkedHashSet<>()); + } + + /** + * Adds task to task set. + * + * @param task Task + */ + public void addTask(Future task) { + taskSet.add(task); + } + + /** + * Stops manage of stopped task. + * + * @param task Task + */ + public void removeTask(Future task) { + taskSet.remove(task); + } + + /** + * Interrupts all tasks. + */ + public void cleanupTasks() { + int count = 0; + // Stop threads + for (Future taskThread : taskSet) { + if (!taskThread.isDone()) { + taskThread.cancel(true); + } + count++; + } + // Clean task set + taskSet.clear(); + + if (count > 0) { + log.debug("Interrupted {} background tasks", count); + } + } + + /** + * @return background task manager instance corresponding current {@link VaadinSession}. Can be invoked anywhere + * in application code. + * @throws IllegalStateException if no background task manager instance is bound to the current + * {@link VaadinSession} + */ + public static BackgroundTaskManager getInstance() { + VaadinSession vSession = VaadinSession.getCurrent(); + if (vSession == null) { + throw new IllegalStateException("No VaadinSession found"); + } + if (!vSession.hasLock()) { + throw new IllegalStateException("VaadinSession is not owned by the current thread"); + } + BackgroundTaskManager backgroundTaskManager = vSession.getAttribute(BackgroundTaskManager.class); + if (backgroundTaskManager == null) { + throw new IllegalStateException("No BackgroundTaskManager is bound to the current VaadinSession"); + } + return backgroundTaskManager; + } + + /** + * @return {@code true} if an {@link BackgroundTaskManager} instance is currently bound and can be safely obtained + * by {@link #getInstance()} + */ + public static boolean isBound() { + VaadinSession vSession = VaadinSession.getCurrent(); + return vSession != null + && vSession.hasLock() + && vSession.getAttribute(BackgroundTaskManager.class) != null; + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskWatchDog.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskWatchDog.java new file mode 100644 index 0000000000..1708380a7c --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskWatchDog.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import io.jmix.flowui.backgroundtask.impl.TaskHandlerImpl; + +@SuppressWarnings("unused") +public interface BackgroundTaskWatchDog { + + /** + * Adds task under {@link BackgroundTaskWatchDog} control. + * + * @param taskHandler task handler + */ + void manageTask(TaskHandlerImpl taskHandler); + + /** + * Task completed, remove it from watches. + * + * @param taskHandler task handler + */ + void removeTask(TaskHandlerImpl taskHandler); + + /** + * Removes finished, canceled or hangup tasks. + */ + void cleanupTasks(); + + /** + * Stops execution of all background tasks. + */ + void stopTasks(); + + /** + * @return active tasks count + */ + int getActiveTasksCount(); +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskWatchDogScheduleConfigurer.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskWatchDogScheduleConfigurer.java new file mode 100644 index 0000000000..391bb8bb35 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundTaskWatchDogScheduleConfigurer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Component; + +@Component("flowui_BackgroundTaskWatchDogScheduleConfigurer") +public class BackgroundTaskWatchDogScheduleConfigurer { + + private TaskScheduler taskScheduler; + + private BackgroundTaskWatchDog backgroundTaskWatchDog; + + private FlowuiBackgroundTaskProperties backgroundTaskProperties; + + public BackgroundTaskWatchDogScheduleConfigurer(@Qualifier("flowui_ThreadPoolTaskScheduler") TaskScheduler taskScheduler, + BackgroundTaskWatchDog backgroundTaskWatchDog, + FlowuiBackgroundTaskProperties backgroundTaskProperties) { + this.taskScheduler = taskScheduler; + this.backgroundTaskWatchDog = backgroundTaskWatchDog; + this.backgroundTaskProperties = backgroundTaskProperties; + } + + @EventListener + public void onContextRefreshedEvent(ContextRefreshedEvent event) { + taskScheduler.scheduleWithFixedDelay( + () -> backgroundTaskWatchDog.cleanupTasks(), + backgroundTaskProperties.getTimeoutExpirationCheckInterval()); + } +} diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundWorker.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundWorker.java new file mode 100644 index 0000000000..e566d0728e --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/BackgroundWorker.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +/** + * Entry point to {@link BackgroundTask} execution functionality. + */ +public interface BackgroundWorker { + + /** + * Creates handler for a background task. The handler is used to control the task execution. + * + * @param progress measure unit + * @param task result type + * @param task background task instance + * @return task handler + * @throws IllegalConcurrentAccessException in case of call from non UI thread + */ + @ExecutedOnUIThread + BackgroundTaskHandler handle(BackgroundTask task); + + /** + * Obtains UI access for later use from background thread. Can be invoked only from UI thread. + * + * @return ui accessor object that allows to read/write state of UI + * @throws IllegalConcurrentAccessException in case of call from non UI thread + */ + @ExecutedOnUIThread + UIAccessor getUIAccessor(); + + /** + * @throws IllegalConcurrentAccessException in case of call from non UI thread + */ + void checkUIAccess(); +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/ExecutedOnUIThread.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/ExecutedOnUIThread.java new file mode 100644 index 0000000000..67f3f47bb7 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/ExecutedOnUIThread.java @@ -0,0 +1,31 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for method that operates on UI thread and can change shared UI state. + * Used as documentation annotation. + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface ExecutedOnUIThread { +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/FlowuiBackgroundTaskProperties.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/FlowuiBackgroundTaskProperties.java new file mode 100644 index 0000000000..a78c2c2557 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/FlowuiBackgroundTaskProperties.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.boot.context.properties.bind.DefaultValue; +import org.springframework.boot.convert.DurationUnit; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +@ConfigurationProperties(prefix = "jmix.flowui.background-task") +@ConstructorBinding +public class FlowuiBackgroundTaskProperties { + + /** + * Number of background task threads. + */ + int threadsCount; + + /** + * Tasks that do not update their status are killed after the timeout (task's timeout plus latency timout). + * If a duration suffix is not specified, seconds will be used. + */ + Duration taskKillingLatency; + + /** + * Interval for checking timeout of the {@link BackgroundTask}. If a duration suffix is not specified, + * milliseconds will be used. + */ + Duration timeoutExpirationCheckInterval; + + public FlowuiBackgroundTaskProperties( + @DefaultValue("10") int threadsCount, + @DefaultValue("60") @DurationUnit(ChronoUnit.SECONDS) Duration taskKillingLatency, + @DefaultValue("5000") Duration timeoutExpirationCheckInterval + ) { + this.threadsCount = threadsCount; + this.taskKillingLatency = taskKillingLatency; + this.timeoutExpirationCheckInterval = timeoutExpirationCheckInterval; + } + + /** + * @see #threadsCount + */ + public int getThreadsCount() { + return threadsCount; + } + + /** + * @see #taskKillingLatency + */ + public Duration getTaskKillingLatency() { + return taskKillingLatency; + } + + /** + * @see #timeoutExpirationCheckInterval + */ + public Duration getTimeoutExpirationCheckInterval() { + return timeoutExpirationCheckInterval; + } +} diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/IllegalConcurrentAccessException.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/IllegalConcurrentAccessException.java new file mode 100644 index 0000000000..dc456805f4 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/IllegalConcurrentAccessException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +/** + * Exception that is thrown in case of incorrect access to a shared data from a thread that does not own necessary lock. + */ +public class IllegalConcurrentAccessException extends RuntimeException { + public IllegalConcurrentAccessException() { + super("UI Shared state was accessed from a background thread"); + } + + public IllegalConcurrentAccessException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/LocalizedTaskWrapper.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/LocalizedTaskWrapper.java new file mode 100644 index 0000000000..ad376612a7 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/LocalizedTaskWrapper.java @@ -0,0 +1,192 @@ +/* + * Copyright 2020 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.dialog.Dialog; +import io.jmix.core.Messages; +import io.jmix.core.annotation.Internal; +import io.jmix.flowui.Notifications; +import io.jmix.flowui.impl.DialogsImpl; +import io.jmix.flowui.view.View; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * INTERNAL! + * Supposed to use when showing {@link Dialog} or some {@link View}. For instance + * if we need to show some dialog when task is running (e.g. updating the progress). + *

+ * See example in {@link DialogsImpl.BackgroundTaskDialogBuilderImpl}. + * + * @param task progress measurement unit + * @param result type + */ +@Internal +@Component("flowui_LocalizedTaskWrapper") +@Scope(BeanDefinition.SCOPE_PROTOTYPE) +public class LocalizedTaskWrapper extends BackgroundTask { + + private static final Logger log = LoggerFactory.getLogger(BackgroundWorker.class); + + protected Messages messages; + protected Notifications notifications; + + protected BackgroundTask wrappedTask; + protected Consumer closeViewHandler; + + public LocalizedTaskWrapper(BackgroundTask wrappedTask) { + super(wrappedTask.getTimeoutSeconds()); + this.wrappedTask = wrappedTask; + } + + @Autowired + public void setMessages(Messages messages) { + this.messages = messages; + } + + @Autowired + public void setNotifications(Notifications notifications) { + this.notifications = notifications; + } + + @Nullable + public Consumer getCloseViewHandler() { + return closeViewHandler; + } + + public void setCloseViewHandler(@Nullable Consumer closeViewHandler) { + this.closeViewHandler = closeViewHandler; + } + + @Override + public Map getParams() { + return wrappedTask.getParams(); + } + + @Override + public V run(TaskLifeCycle lifeCycle) throws Exception { + return wrappedTask.run(lifeCycle); + } + + @Override + public boolean handleException(Exception ex) { + boolean handled = wrappedTask.handleException(ex); + + notifyCloseViewHandler(); + + if (!handled && wrappedTask.getOwnerView() != null) { + showExecutionError(ex); + + log.error("Exception occurred in background task", ex); + + handled = true; + } + return handled; + } + + @Override + public boolean handleTimeoutException() { + boolean handled = wrappedTask.handleTimeoutException(); + + notifyCloseViewHandler(); + + if (!handled && wrappedTask.getOwnerView() != null) { + if (UI.getCurrent() != null) { + notifications.create( + messages.getMessage("localizedTaskWrapper.timeout.notification.title"), + messages.getMessage("localizedTaskWrapper.timeout.notification.message")) + .withType(Notifications.Type.WARNING) + .show(); + } + handled = true; + } + return handled; + } + + @Override + public void done(V result) { + notifyCloseViewHandler(); + + try { + wrappedTask.done(result); + } catch (Exception ex) { + // we should show exception messages immediately + showExecutionError(ex); + } + } + + @Override + public void canceled() { + try { + wrappedTask.canceled(); + } catch (Exception ex) { + // we should show exception messages immediately + showExecutionError(ex); + } + } + + @Override + public void progress(List changes) { + wrappedTask.progress(changes); + } + + protected void showExecutionError(Exception ex) { + View ownerView = wrappedTask.getOwnerView(); + if (ownerView != null && UI.getCurrent() != null) { + notifications.create(messages.getMessage("localizedTaskWrapper.executionError.message")) + .withType(Notifications.Type.ERROR) + .show(); + + /* todo ExceptionDialog */ + /*Dialogs dialogs = Instantiator.get(UI.getCurrent()).getOrCreate(Dialogs.class); + dialogs.createExceptionDialog() + .withThrowable(ex) + .withCaption(messages.getMessage("localizedTaskWrapper.executionError.message")) + .withMessage(ex.getLocalizedMessage()) + .show();*/ + } + } + + protected void notifyCloseViewHandler() { + if (closeViewHandler != null) { + closeViewHandler.accept(new CloseViewContext(this)); + } + } + + public static class CloseViewContext { + + protected LocalizedTaskWrapper taskWrapper; + + public CloseViewContext(LocalizedTaskWrapper taskWrapper) { + this.taskWrapper = taskWrapper; + } + + public LocalizedTaskWrapper getSource() { + return taskWrapper; + } + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/TaskExecutor.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/TaskExecutor.java new file mode 100644 index 0000000000..2d66ff5c9f --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/TaskExecutor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import javax.annotation.Nullable; + +public interface TaskExecutor { + /** + * Executes background task. + */ + @ExecutedOnUIThread + void startExecution(); + + /** + * Cancels the execution and removes task form {@link BackgroundTaskWatchDog} and from {@link BackgroundTaskManager}. + * @return {@code true} if the canceling was successful + */ + @ExecutedOnUIThread + boolean cancelExecution(); + + /** + * Joins task thread to current and waits if task is not finished. + * @return result of the task + */ + @ExecutedOnUIThread + @Nullable + V getResult(); + + /** + * @return the task + */ + BackgroundTask getTask(); + + boolean isCancelled(); + + boolean isDone(); + + boolean inProgress(); + + /** + * Sets done handler for clear resources. + * + * @param finalizer Runnable handler + */ + void setFinalizer(Runnable finalizer); + + Runnable getFinalizer(); + + /** + * Handles changes from working thread. + * + * @param changes Changes + */ + @SuppressWarnings({"unchecked"}) + void handleProgress(T... changes); +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/TaskLifeCycle.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/TaskLifeCycle.java new file mode 100644 index 0000000000..24c2b93c1a --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/TaskLifeCycle.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +import java.util.Map; + +/** + * Lifecycle object that is passed to {@link BackgroundTask#run(TaskLifeCycle)} method to allow working thread to + * interact with the execution environment. + * + * @param task progress measurement unit + */ +public interface TaskLifeCycle { + /** + * Publishes changes to show progress. + * + * @param changes Changes + * @throws InterruptedException if task was interrupted by calling {@link BackgroundTaskHandler#cancel()} + */ + @SuppressWarnings({"unchecked"}) + void publish(T... changes) throws InterruptedException; + + /** + * @return {@code true} if the working thread has been interrupted + */ + boolean isInterrupted(); + + /** + * @return {@code true} if a task was interrupted by calling the "cancel" method + */ + boolean isCancelled(); + + /** + * @return execution parameters that was set by {@link BackgroundTask#getParams()} + */ + Map getParams(); +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/UIAccessor.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/UIAccessor.java new file mode 100644 index 0000000000..50ce6c83d5 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/UIAccessor.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask; + +/** + * Interface that allows to read/write state of UI from background threads. + * + * @see BackgroundWorker#getUIAccessor() + */ +public interface UIAccessor { + /** + * Provides exclusive access to UI state from outside a UI event handling thread. + *

+ * The given runnable is executed while holding the UI lock to ensure + * exclusive access to UI state. + *

+ * Please note that the runnable might be invoked on a different thread or + * later on the current thread, which means that custom thread locals might + * not have the expected values when the runnable is executed. + * + * @param runnable runnable + */ + void access(Runnable runnable); + + /** + * Locks the UI and runs the provided Runnable right away. + *

+ * The given runnable is executed while holding the UI lock to ensure + * exclusive access to UI state. + * + * @param runnable runnable + */ + void accessSynchronously(Runnable runnable); +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/BackgroundTaskWatchDogImpl.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/BackgroundTaskWatchDogImpl.java new file mode 100644 index 0000000000..91e8ecddb0 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/BackgroundTaskWatchDogImpl.java @@ -0,0 +1,157 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask.impl; + +import io.jmix.core.TimeSource; +import io.jmix.flowui.backgroundtask.BackgroundWorker; +import io.jmix.flowui.backgroundtask.FlowuiBackgroundTaskProperties; +import io.jmix.flowui.backgroundtask.BackgroundTaskWatchDog; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.ContextStartedEvent; +import org.springframework.context.event.ContextStoppedEvent; +import org.springframework.context.event.EventListener; + +import javax.annotation.concurrent.ThreadSafe; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * Implementation of {@link BackgroundTaskWatchDog} for {@link BackgroundWorker}. + */ +@ThreadSafe +public class BackgroundTaskWatchDogImpl implements BackgroundTaskWatchDog { + private static final Logger log = LoggerFactory.getLogger(BackgroundTaskWatchDogImpl.class); + + public enum ExecutionStatus { + NORMAL, + TIMEOUT_EXCEEDED, + SHOULD_BE_KILLED + } + + protected TimeSource timeSource; + protected FlowuiBackgroundTaskProperties properties; + + private final Set watches = new LinkedHashSet<>(); + + protected volatile boolean initialized; + + public BackgroundTaskWatchDogImpl(FlowuiBackgroundTaskProperties properties, + TimeSource timeSource) { + this.properties = properties; + this.timeSource = timeSource; + } + + @EventListener(classes = {ContextRefreshedEvent.class, ContextStartedEvent.class}) + public void onContextRefreshed() { + initialized = true; + } + + @EventListener(ContextStoppedEvent.class) + public void onContextStopped() { + initialized = false; + } + + @Override + public synchronized void cleanupTasks() { + if (!initialized) { + return; + } + + long actual = timeSource.currentTimestamp().getTime(); + + List forRemove = new ArrayList<>(); + // copy watches since task.kill tries to remove task handler from watches + for (TaskHandlerImpl task : new ArrayList<>(watches)) { + if (task.isCancelled() || task.isDone()) { + forRemove.add(task); + } else { + ExecutionStatus status = getExecutionStatus(actual, task); + + switch (status) { + case TIMEOUT_EXCEEDED: + task.closeByTimeout(); + task.timeoutExceeded(); + forRemove.add(task); + break; + + case SHOULD_BE_KILLED: + task.kill(); + forRemove.add(task); + break; + + default: + break; + } + } + } + + watches.removeAll(forRemove); + } + + @Override + public synchronized void stopTasks() { + // copy watches since task.kill tries to remove task handler from watches + ArrayList taskHandlers = new ArrayList<>(watches); + watches.clear(); + for (TaskHandlerImpl task : taskHandlers) { + task.kill(); + } + } + + @Override + public synchronized int getActiveTasksCount() { + return watches.size(); + } + + @Override + public synchronized void manageTask(TaskHandlerImpl taskHandler) { + watches.add(taskHandler); + } + + @Override + public synchronized void removeTask(TaskHandlerImpl taskHandler) { + watches.remove(taskHandler); + } + + protected ExecutionStatus getExecutionStatus(long actualTimeMs, TaskHandlerImpl taskHandler) { + long timeout = taskHandler.getTimeoutMs(); + + // kill tasks, which do not update status for latency milliseconds + long latencyMs = properties.getTaskKillingLatency().toMillis(); + if (timeout > 0 && (actualTimeMs - taskHandler.getStartTimeStamp()) > timeout + latencyMs) { + if (log.isTraceEnabled()) { + log.trace("Latency timeout exceeded for task: {}", taskHandler.getTask()); + } + return ExecutionStatus.SHOULD_BE_KILLED; + } + + if (timeout > 0 && (actualTimeMs - taskHandler.getStartTimeStamp()) > timeout) { + if (log.isTraceEnabled()) { + log.trace("Timeout exceeded for task: {}", taskHandler.getTask()); + } + return ExecutionStatus.TIMEOUT_EXCEEDED; + } + + return ExecutionStatus.NORMAL; + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/BackgroundWorkerImpl.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/BackgroundWorkerImpl.java new file mode 100644 index 0000000000..5ca5782c53 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/BackgroundWorkerImpl.java @@ -0,0 +1,445 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask.impl; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.UIDetachedException; +import com.vaadin.flow.server.VaadinSession; +import io.jmix.core.TimeSource; +import io.jmix.core.security.CurrentAuthentication; +import io.jmix.core.security.SecurityContextHelper; +import io.jmix.flowui.event.BackgroundTaskUnhandledExceptionEvent; +import io.jmix.flowui.backgroundtask.*; +import io.jmix.flowui.backgroundtask.BackgroundTaskWatchDog; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; + +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Implementation of {@link BackgroundWorker}. + */ +@Component("flowui_BackgroundWorkerImpl") +public class BackgroundWorkerImpl implements BackgroundWorker { + + private static final Logger log = LoggerFactory.getLogger(BackgroundWorkerImpl.class); + + private static final String THREAD_NAME_PREFIX = "BackgroundTask-"; + private static final Pattern THREAD_NAME_PATTERN = Pattern.compile("BackgroundTask-([0-9]+)"); + + @Autowired + protected BackgroundTaskWatchDog backgroundTaskWatchDog; + @Autowired + protected CurrentAuthentication currentAuthentication; + @Autowired + protected ApplicationEventPublisher applicationEventPublisher; + @Autowired + protected TimeSource timeSource; + + protected FlowuiBackgroundTaskProperties properties; + + protected ExecutorService executorService; + + public BackgroundWorkerImpl() { + } + + @Autowired + public void setProperties(FlowuiBackgroundTaskProperties properties) { + this.properties = properties; + + createThreadPoolExecutor(); + } + + protected void createThreadPoolExecutor() { + if (executorService != null) { + return; + } + + this.executorService = new ThreadPoolExecutor( + properties.getThreadsCount(), + properties.getThreadsCount(), + 10L, TimeUnit.MINUTES, + new LinkedBlockingQueue<>(), + new ThreadFactoryBuilder() + .setNameFormat(THREAD_NAME_PREFIX + "%d") + .build() + ); + ((ThreadPoolExecutor) this.executorService).allowCoreThreadTimeOut(true); + } + + @PreDestroy + public void destroy() { + executorService.shutdownNow(); + } + + @Override + public BackgroundTaskHandler handle(BackgroundTask task) { + checkNotNull(task); + checkUIAccess(); + + BackgroundTaskManager taskManager; + try { + taskManager = BackgroundTaskManager.getInstance(); + } catch (IllegalStateException e) { + log.error("Couldn't handle task", e); + throw e; + } + + UI ui = UI.getCurrent(); + + // create task executor + TaskExecutorImpl taskExecutor = new TaskExecutorImpl<>(ui, taskManager, task); + + // add thread to taskSet + taskManager.addTask(taskExecutor.getFuture()); + + // create task handler + TaskHandlerImpl taskHandler = new TaskHandlerImpl<>( + getUIAccessor(), taskExecutor, backgroundTaskWatchDog, applicationEventPublisher, + currentAuthentication.getUser(), timeSource); + taskExecutor.setTaskHandler(taskHandler); + + return taskHandler; + } + + @Override + public UIAccessor getUIAccessor() { + checkUIAccess(); + + return new UIAccessorImpl(UI.getCurrent()); + } + + @Override + public void checkUIAccess() { + VaadinSession vaadinSession = VaadinSession.getCurrent(); + + if (vaadinSession == null || !vaadinSession.hasLock()) { + throw new IllegalConcurrentAccessException(); + } + } + + private class TaskExecutorImpl implements TaskExecutor, Callable { + + private UI ui; + private BackgroundTaskManager taskManager; + + private FutureTask future; + + private BackgroundTask runnableTask; + private Runnable finalizer; + + private volatile boolean isClosed = false; + private volatile boolean doneHandled = false; + + private Authentication authentication; + private String username; + + private Map params; + private TaskHandlerImpl taskHandler; + + private TaskExecutorImpl(UI ui, BackgroundTaskManager taskManager, BackgroundTask runnableTask) { + this.runnableTask = runnableTask; + this.ui = ui; + this.taskManager = taskManager; + + this.params = runnableTask.getParams() != null ? + Collections.unmodifiableMap(runnableTask.getParams()) : + Collections.emptyMap(); + + authentication = SecurityContextHelper.getAuthentication(); + + this.username = currentAuthentication.getUser().getUsername(); + + this.future = new FutureTask<>(this) { + @Override + protected void done() { + Authentication previousAuth = SecurityContextHelper.getAuthentication(); + + SecurityContextHelper.setAuthentication(authentication); + try { + TaskExecutorImpl.this.ui.access(() -> + handleDone() + ); + } catch (UIDetachedException e) { + log.debug("Cannot handle 'Done' statement because UI is detached from session. It may be due " + + "to canceling task after session is invalidated"); + cancelExecution(); + } finally { + SecurityContextHelper.setAuthentication(previousAuth); + } + } + }; + } + + @Override + public final V call() throws Exception { + String threadName = Thread.currentThread().getName(); + Matcher matcher = THREAD_NAME_PATTERN.matcher(threadName); + if (matcher.find()) { + Thread.currentThread().setName(THREAD_NAME_PREFIX + matcher.group(1) + "-" + username); + } + + SecurityContextHelper.setAuthentication(authentication); + try { + // do not run any activity if canceled before start + return runnableTask.run(new TaskLifeCycle<>() { + @SafeVarargs + @Override + public final void publish(T... changes) throws InterruptedException { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException("Task is interrupted and is trying to publish changes"); + } + + handleProgress(changes); + } + + @Override + public boolean isInterrupted() { + return Thread.currentThread().isInterrupted(); + } + + @Override + public boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public Map getParams() { + return params; + } + }); + } finally { + SecurityContextHelper.setAuthentication(null); + } + } + + @SafeVarargs + @Override + public final void handleProgress(T... changes) { + ui.access(() -> process(Arrays.asList(changes))); + } + + @ExecutedOnUIThread + protected final void process(List chunks) { + runnableTask.progress(chunks); + // Notify listeners + for (BackgroundTask.ProgressListener listener : runnableTask.getProgressListeners()) { + listener.onProgress(chunks); + } + } + + @ExecutedOnUIThread + protected final void handleDone() { + if (isCancelled()) { + // handle cancel from EDT before execution start + log.trace("Done statement is not processed because it is canceled task"); + return; + } + + if (isClosed) { + log.trace("Done statement is not processed because it is already closed"); + return; + } + + log.debug("Done task. User: {}", username); + + // do not allow to cancel task from done listeners and exception handler + isClosed = true; + + unregister(); + + // As "handleDone()" can be processed under BackgroundTask thread or under UI thread from which + // the task starts, we should save previous security context (that can be null) + // to restore it when "done()" is finished. + Authentication previousAuth = SecurityContextHelper.getAuthentication(); + + try { + SecurityContextHelper.setAuthentication(authentication); + + V result = future.get(); + + runnableTask.done(result); + // Notify listeners + for (BackgroundTask.ProgressListener listener : runnableTask.getProgressListeners()) { + listener.onDone(result); + } + } catch (CancellationException e) { + log.debug("Cancellation exception in background task", e); + } catch (InterruptedException e) { + log.debug("Interrupted exception in background task", e); + } catch (ExecutionException e) { + // do not call log.error, exception may be handled later + log.debug("Exception in background task", e); + if (!future.isCancelled()) { + boolean handled = false; + + if (e.getCause() instanceof Exception) { + handled = runnableTask.handleException((Exception) e.getCause()); + } + + if (!handled) { + log.error("Unhandled exception in background task", e); + applicationEventPublisher.publishEvent(new BackgroundTaskUnhandledExceptionEvent(this, runnableTask, e)); + } + } + } finally { + SecurityContextHelper.setAuthentication(previousAuth); + + if (finalizer != null) { + finalizer.run(); + finalizer = null; + } + + doneHandled = true; + } + } + + @ExecutedOnUIThread + @Override + public final boolean cancelExecution() { + if (isClosed) { + return false; + } + + unregister(); + + log.debug("Cancel task. User: {}", username); + + boolean isCanceledNow = future.cancel(true); + if (isCanceledNow) { + log.trace("Task was cancelled. User: {}", username); + } else { + log.trace("Cancellation of task isn't processed. User: {}", username); + } + + if (!doneHandled) { + log.trace("Done was not handled. Return 'true' as canceled status. User: {}", username); + + this.isClosed = true; + return true; + } + + return isCanceledNow; + } + + @ExecutedOnUIThread + protected void unregister() { + log.trace("Unregister task"); + + taskManager.removeTask(future); + backgroundTaskWatchDog.removeTask(taskHandler); + } + + @ExecutedOnUIThread + @Nullable + @Override + public final V getResult() { + V result; + try { + result = future.get(); + } catch (InterruptedException | ExecutionException | CancellationException e) { + log.debug("{} exception in background task", e.getClass().getName(), e); + return null; + } + + this.handleDone(); + + return result; + } + + @Override + public final BackgroundTask getTask() { + return runnableTask; + } + + @ExecutedOnUIThread + @Override + public final void startExecution() { + // Start thread + executorService.execute(() -> + future.run() + ); + } + + @Override + public final boolean isCancelled() { + return future.isCancelled(); + } + + @Override + public final boolean isDone() { + return future.isDone(); + } + + @Override + public final boolean inProgress() { + return !isClosed; + } + + @ExecutedOnUIThread + @Override + public final void setFinalizer(Runnable finalizer) { + this.finalizer = finalizer; + } + + @Override + public final Runnable getFinalizer() { + return finalizer; + } + + public void setTaskHandler(TaskHandlerImpl taskHandler) { + this.taskHandler = taskHandler; + } + + public FutureTask getFuture() { + return future; + } + } + + private static class UIAccessorImpl implements UIAccessor { + private UI ui; + + public UIAccessorImpl(UI ui) { + this.ui = ui; + } + + @Override + public void access(Runnable runnable) { + ui.access(runnable::run); + } + + @Override + public void accessSynchronously(Runnable runnable) { + ui.accessSynchronously(runnable::run); + } + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/TaskHandlerImpl.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/TaskHandlerImpl.java new file mode 100644 index 0000000000..98b78b3289 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/TaskHandlerImpl.java @@ -0,0 +1,258 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.backgroundtask.impl; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.shared.Registration; +import io.jmix.core.TimeSource; +import io.jmix.flowui.event.BackgroundTaskTimeoutEvent; +import io.jmix.flowui.backgroundtask.*; +import io.jmix.flowui.backgroundtask.BackgroundTaskWatchDog; +import io.jmix.flowui.view.View; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.security.core.userdetails.UserDetails; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkState; + +public class TaskHandlerImpl implements BackgroundTaskHandler { + + private static final Logger log = LoggerFactory.getLogger(BackgroundWorker.class); + + private UIAccessor uiAccessor; + private final TaskExecutor taskExecutor; + private final BackgroundTaskWatchDog backgroundTaskWatchDog; + private ApplicationEventPublisher applicationEventPublisher; + private TimeSource timeSource; + + private volatile boolean started = false; + private volatile boolean timeoutHappens = false; + + private long startTimeStamp; + + private Registration viewDetachRegistration; + private final UserDetails user; + + public TaskHandlerImpl(UIAccessor uiAccessor, + TaskExecutor taskExecutor, + BackgroundTaskWatchDog backgroundTaskWatchDog, + ApplicationEventPublisher applicationEventPublisher, + UserDetails user, + TimeSource timeSource) { + this.uiAccessor = uiAccessor; + this.taskExecutor = taskExecutor; + this.backgroundTaskWatchDog = backgroundTaskWatchDog; + this.applicationEventPublisher = applicationEventPublisher; + this.timeSource = timeSource; + + this.user = user; + BackgroundTask task = taskExecutor.getTask(); + if (task.getOwnerView() != null) { + View ownerView = task.getOwnerView(); + + viewDetachRegistration = ownerView.addDetachListener(e -> onOwnerViewRemoved(e.getSource())); + + // remove close listener on done + taskExecutor.setFinalizer(() -> { + log.trace("Start task finalizer. Task: {}", taskExecutor.getTask()); + + removeViewDetachListener(); + + log.trace("Finish task finalizer. Task: {}", taskExecutor.getTask()); + }); + } + } + + protected void onOwnerViewRemoved(Component ownerView) { + if (log.isTraceEnabled()) { + String viewClass = ownerView.getClass().getCanonicalName(); + log.trace("View removed. User: {}. View: {}", user.getUsername(), viewClass); + } + + taskExecutor.cancelExecution(); + } + + @Override + public final void execute() { + checkState(!started, "Task is already started. Task: " + taskExecutor.getTask()); + + this.started = true; + + this.startTimeStamp = timeSource.currentTimestamp().getTime(); + + this.backgroundTaskWatchDog.manageTask(this); + + log.trace("Run task: {}. User: {}", taskExecutor.getTask(), user.getUsername()); + + taskExecutor.startExecution(); + } + + @Override + public final boolean cancel() { + checkState(started, "Task is not running. Task: " + taskExecutor.getTask()); + + boolean canceled = taskExecutor.cancelExecution(); + if (canceled) { + removeViewDetachListener(); + + BackgroundTask task = taskExecutor.getTask(); + task.canceled(); + + // Notify listeners + for (BackgroundTask.ProgressListener listener : task.getProgressListeners()) { + listener.onCancel(); + } + + if (log.isTraceEnabled()) { + View ownerView = getTask().getOwnerView(); + if (ownerView != null) { + String viewClass = ownerView.getClass().getCanonicalName(); + + log.trace("Task was cancelled. Task: {}. User: {}. View: {}", taskExecutor.getTask(), user.getUsername(), viewClass); + } else { + log.trace("Task was cancelled. Task: {}. User: {}", taskExecutor.getTask(), user.getUsername()); + } + } + } else { + log.trace("Task wasn't cancelled. Execution is already cancelled. Task: {}", taskExecutor.getTask()); + } + + return canceled; + } + + protected void removeViewDetachListener() { + if (viewDetachRegistration != null) { + viewDetachRegistration.remove(); + viewDetachRegistration = null; + } + } + + /** + * Join task thread to current + *
+ * Caution! + * Call this method only from synchronous gui action; + * + * @return Task result + */ + @Nullable + @Override + public final V getResult() { + checkState(started, "Task is not running"); + + return taskExecutor.getResult(); + } + + /** + * Cancels without events for tasks. Need to execute {@link #timeoutExceeded} after this method. + */ + public final void closeByTimeout() { + timeoutHappens = true; + kill(); + } + + /** + * Cancels without events for tasks. + */ + public final void kill() { + uiAccessor.access(() -> { + removeViewDetachListener(); + + if (log.isTraceEnabled()) { + View ownerView = getTask().getOwnerView(); + if (ownerView != null) { + String viewClass = ownerView.getClass().getCanonicalName(); + log.trace("Task killed. Task: {}. User: {}. View: {}", taskExecutor.getTask(), user.getUsername(), viewClass); + } else { + log.trace("Task killed. Task: {}. User: {}", taskExecutor.getTask(), user.getUsername()); + } + } + + taskExecutor.cancelExecution(); + }); + } + + /** + * Cancels with timeout exceeded event. + */ + public final void timeoutExceeded() { + uiAccessor.access(() -> { + View ownerView = getTask().getOwnerView(); + if (log.isTraceEnabled()) { + if (ownerView != null) { + String viewClass = ownerView.getClass().getCanonicalName(); + log.trace("Task timeout exceeded. Task: {}. View: {}", taskExecutor.getTask(), viewClass); + } else { + log.trace("Task timeout exceeded. Task: {}", taskExecutor.getTask()); + } + } + + checkState(started, "Task is not running"); + + boolean canceled = taskExecutor.cancelExecution(); + if (canceled || timeoutHappens) { + removeViewDetachListener(); + + BackgroundTask task = taskExecutor.getTask(); + boolean handled = task.handleTimeoutException(); + if (!handled) { + log.error("Unhandled timeout exception in background task. Task: " + task); + applicationEventPublisher.publishEvent(new BackgroundTaskTimeoutEvent(this, task)); + } + } + + if (log.isTraceEnabled()) { + if (ownerView != null) { + String viewClass = ownerView.getClass().getCanonicalName(); + log.trace("Timeout was processed. Task: {}. View: {}", taskExecutor.getTask(), viewClass); + } else { + log.trace("Timeout was processed. Task: {}", taskExecutor.getTask()); + } + } + }); + } + + @Override + public final boolean isDone() { + return taskExecutor.isDone(); + } + + @Override + public final boolean isCancelled() { + return taskExecutor.isCancelled(); + } + + @Override + public final boolean isAlive() { + return taskExecutor.inProgress() && started; + } + + public final BackgroundTask getTask() { + return taskExecutor.getTask(); + } + + public long getStartTimeStamp() { + return startTimeStamp; + } + + public long getTimeoutMs() { + return taskExecutor.getTask().getTimeoutMilliseconds(); + } +} \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/package-info.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/package-info.java new file mode 100644 index 0000000000..ca03851b2f --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/impl/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Haulmont. + * + * 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 + * + * http://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. + */ + +@Internal +@NonNullApi +package io.jmix.flowui.backgroundtask.impl; + +import io.jmix.core.annotation.Internal; +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/package-info.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/package-info.java new file mode 100644 index 0000000000..064882cac4 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/backgroundtask/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Haulmont. + * + * 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 + * + * http://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. + */ + +@NonNullApi +package io.jmix.flowui.backgroundtask; + +import org.springframework.lang.NonNullApi; \ No newline at end of file diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/AbstractBackgroundTaskEvent.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/AbstractBackgroundTaskEvent.java new file mode 100644 index 0000000000..0b3b01aae3 --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/AbstractBackgroundTaskEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.event; + +import io.jmix.flowui.backgroundtask.BackgroundTask; +import org.springframework.context.ApplicationEvent; + +/** + * Base class for events that contain information about {@link BackgroundTask}. + */ +public abstract class AbstractBackgroundTaskEvent extends ApplicationEvent { + private BackgroundTask task; + private boolean stopPropagation; + + public AbstractBackgroundTaskEvent(Object source, BackgroundTask task) { + super(source); + this.task = task; + } + + public BackgroundTask getTask() { + return task; + } + + public boolean isStopPropagation() { + return stopPropagation; + } + + public void setStopPropagation(boolean stopPropagation) { + this.stopPropagation = stopPropagation; + } +} diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/BackgroundTaskTimeoutEvent.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/BackgroundTaskTimeoutEvent.java new file mode 100644 index 0000000000..5178697a3c --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/BackgroundTaskTimeoutEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.event; + +import io.jmix.flowui.backgroundtask.BackgroundTask; + +/** + * Event is published if the task does not handle timeout in {@link BackgroundTask#handleTimeoutException()}. + */ +public class BackgroundTaskTimeoutEvent extends AbstractBackgroundTaskEvent { + + public BackgroundTaskTimeoutEvent(Object source, BackgroundTask task) { + super(source, task); + } +} diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/BackgroundTaskUnhandledExceptionEvent.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/BackgroundTaskUnhandledExceptionEvent.java new file mode 100644 index 0000000000..fa9b0f533d --- /dev/null +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/event/BackgroundTaskUnhandledExceptionEvent.java @@ -0,0 +1,37 @@ +/* + * Copyright 2019 Haulmont. + * + * 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 + * + * http://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 io.jmix.flowui.event; + +import io.jmix.flowui.backgroundtask.BackgroundTask; + +/** + * Event is published if the {@link BackgroundTask#done(Object)} throws an exception and executor + * cannot handle it. + */ +public class BackgroundTaskUnhandledExceptionEvent extends AbstractBackgroundTaskEvent { + + private Exception exception; + + public BackgroundTaskUnhandledExceptionEvent(Object source, BackgroundTask task, Exception exception) { + super(source, task); + this.exception = exception; + } + + public Exception getException() { + return exception; + } +} diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/impl/DialogsImpl.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/impl/DialogsImpl.java index d157234a3d..95ed36f899 100644 --- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/impl/DialogsImpl.java +++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/impl/DialogsImpl.java @@ -16,35 +16,44 @@ package io.jmix.flowui.impl; +import com.vaadin.flow.component.ClickEvent; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.Focusable; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.component.dialog.Dialog; -import com.vaadin.flow.component.formlayout.FormLayout; import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.icon.Icon; +import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.FlexComponent; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.progressbar.ProgressBar; import io.jmix.core.Messages; import io.jmix.flowui.DialogWindows; import io.jmix.flowui.Dialogs; import io.jmix.flowui.FlowuiViewProperties; +import io.jmix.flowui.UiComponents; import io.jmix.flowui.action.DialogAction; import io.jmix.flowui.action.inputdialog.InputDialogAction; import io.jmix.flowui.app.inputdialog.DialogActions; import io.jmix.flowui.app.inputdialog.InputDialog; import io.jmix.flowui.app.inputdialog.InputParameter; import io.jmix.flowui.component.validation.ValidationErrors; +import io.jmix.flowui.backgroundtask.*; import io.jmix.flowui.kit.action.Action; import io.jmix.flowui.kit.component.KeyCombination; import io.jmix.flowui.view.DialogWindow; import io.jmix.flowui.view.View; import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationContext; import javax.annotation.Nullable; import java.util.Collection; import java.util.EnumSet; +import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -56,11 +65,19 @@ public class DialogsImpl implements Dialogs { protected Messages messages; protected FlowuiViewProperties flowUiViewProperties; protected DialogWindows dialogWindows; + protected UiComponents uiComponents; + protected BackgroundWorker backgroundWorker; + protected ApplicationContext applicationContext; - public DialogsImpl(Messages messages, FlowuiViewProperties flowUiViewProperties, DialogWindows dialogWindows) { + public DialogsImpl(ApplicationContext applicationContext, Messages messages, + FlowuiViewProperties flowUiViewProperties, + DialogWindows dialogWindows, UiComponents uiComponents, BackgroundWorker backgroundWorker) { this.messages = messages; this.flowUiViewProperties = flowUiViewProperties; this.dialogWindows = dialogWindows; + this.uiComponents = uiComponents; + this.backgroundWorker = backgroundWorker; + this.applicationContext = applicationContext; } @Override @@ -78,6 +95,12 @@ public InputDialogBuilder createInputDialog(View origin) { return new InputDialogBuilderImpl(origin); } + @Override + public BackgroundTaskDialogBuilder createBackgroundTaskDialog( + BackgroundTask backgroundTask) { + return new BackgroundTaskDialogBuilderImpl<>(backgroundTask); + } + protected Button createButton(Action action, Dialog dialog) { Button button = new Button(); @@ -730,4 +753,284 @@ public DialogWindow build() { return dialogBuild; } } + + public class BackgroundTaskDialogBuilderImpl implements BackgroundTaskDialogBuilder { + + protected Dialog dialog; + + protected Span messageSpan; + protected Span progressTextSpan; + protected ProgressBar progressBar; + protected Button cancelButton; + + protected BackgroundTask backgroundTask; + protected String messageText; + protected Number total; + protected boolean showProgressInPercentage; + protected boolean cancelAllowed = false; + + protected BackgroundTaskHandler taskHandler; + + public BackgroundTaskDialogBuilderImpl(BackgroundTask backgroundTask) { + this.backgroundTask = backgroundTask; + + dialog = createDialog(); + initDialog(dialog); + initDialogContent(dialog); + } + + protected Dialog createDialog() { + return new Dialog(); + } + + protected void initDialog(Dialog dialog) { + dialog.setHeaderTitle(messages.getMessage("backgroundWorkProgressDialog.headerTitle")); + dialog.setDraggable(true); + dialog.setCloseOnOutsideClick(false); + dialog.setCloseOnEsc(false); + dialog.setWidth(WIDTH); + } + + protected void initDialogContent(Dialog dialog) { + VerticalLayout content = new VerticalLayout(); + content.setPadding(false); + + messageSpan = uiComponents.create(Span.class); + messageSpan.setText(messages.getMessage("backgroundWorkProgressDialog.messageSpan.text")); + content.add(messageSpan); + + progressTextSpan = uiComponents.create(Span.class); + content.add(progressTextSpan); + + progressBar = uiComponents.create(ProgressBar.class); + content.add(progressBar); + + dialog.add(content); + + cancelButton = uiComponents.create(Button.class); + cancelButton.setText(messages.getMessage("actions.Cancel")); + cancelButton.setIcon(new Icon(VaadinIcon.BAN)); + cancelButton.addClickListener(this::onCancelButtonClick); + } + + @Override + public BackgroundTaskDialogBuilder withCancelAllowed(boolean cancelAllowed) { + if (this.cancelAllowed != cancelAllowed) { + this.cancelAllowed = cancelAllowed; + if (cancelAllowed) { + dialog.getFooter().add(cancelButton); + } else { + dialog.getFooter().remove(cancelButton); + } + } + return this; + } + + @Override + public boolean isCancelAllowed() { + return cancelAllowed; + } + + @Override + public BackgroundTaskDialogBuilder withTotal(Number total) { + this.total = total; + return this; + } + + @Override + public Number getTotal() { + return total; + } + + @Override + public BackgroundTaskDialogBuilder withShowProgressInPercentage(boolean percentProgress) { + this.showProgressInPercentage = percentProgress; + return this; + } + + @Override + public boolean isShowProgressInPercentage() { + return showProgressInPercentage; + } + + @Override + public BackgroundTaskDialogBuilder withHeader(String header) { + dialog.setHeaderTitle(header); + return this; + } + + @Nullable + @Override + public String getHeader() { + return dialog.getHeaderTitle(); + } + + @Override + public BackgroundTaskDialogBuilder withText(String text) { + this.messageText = text; + return this; + } + + @Nullable + @Override + public String getText() { + return messageText; + } + + @Override + public BackgroundTaskDialogBuilder withThemeName(String themeName) { + dialog.setThemeName(themeName); + return this; + } + + @Nullable + @Override + public String getThemeName() { + return dialog.getThemeName(); + } + + @Override + public BackgroundTaskDialogBuilder withClassName(@Nullable String className) { + dialog.setClassName(className); + return this; + } + + @Nullable + @Override + public String getClassName() { + return dialog.getClassName(); + } + + @Override + public BackgroundTaskDialogBuilder withDraggable(boolean draggable) { + dialog.setDraggable(draggable); + return this; + } + + @Override + public boolean isDraggable() { + return dialog.isDraggable(); + } + + @Override + public BackgroundTaskDialogBuilder withResizable(boolean resizable) { + dialog.setResizable(resizable); + return this; + } + + @Override + public boolean isResizable() { + return dialog.isResizable(); + } + + @Override + public BackgroundTaskDialogBuilder withMinWidth(String minWidth) { + dialog.setMinWidth(minWidth); + return this; + } + + @Override + public String getMinWidth() { + return dialog.getMinWidth(); + } + + @Override + public BackgroundTaskDialogBuilder withMinHeight(String minHeight) { + dialog.setMinHeight(minHeight); + return this; + } + + @Override + public String getMinHeight() { + return dialog.getMinHeight(); + } + + @Override + public BackgroundTaskDialogBuilder withMaxWidth(String maxWidth) { + dialog.setMaxWidth(maxWidth); + return this; + } + + @Override + public String getMaxWidth() { + return dialog.getMaxWidth(); + } + + @Override + public BackgroundTaskDialogBuilder withMaxHeight(String maxHeight) { + dialog.setMaxHeight(maxHeight); + return this; + } + + @Override + public String getMaxHeight() { + return dialog.getMaxHeight(); + } + + @Override + public void open() { + if (messageText != null) { + messageSpan.setText(messageText); + } + if (isIndeterminateMode()) { + progressTextSpan.setVisible(false); + progressBar.setIndeterminate(true); + } else { + progressTextSpan.setVisible(true); + } + updateProgress(0); + + dialog.open(); + + startExecution(); + } + + protected boolean isIndeterminateMode() { + return total == null; + } + + @SuppressWarnings("unchecked") + protected void startExecution() { + LocalizedTaskWrapper taskWrapper = applicationContext.getBean(LocalizedTaskWrapper.class, backgroundTask); + taskWrapper.setCloseViewHandler(this::handleTaskWrapperCloseView); + taskWrapper.addProgressListener(new BackgroundTask.ProgressListenerAdapter<>() { + @Override + public void onProgress(List changes) { + if (!changes.isEmpty()) { + Number lastProcessedValue = changes.get(changes.size() - 1); + updateProgress(lastProcessedValue); + } + } + }); + + taskHandler = backgroundWorker.handle(taskWrapper); + taskHandler.execute(); + } + + protected void updateProgress(Number processedValue) { + if (isIndeterminateMode()) { + return; + } + + double currentProgressValue = processedValue.doubleValue() / total.doubleValue(); + progressBar.setValue(currentProgressValue); + if (!showProgressInPercentage) { + progressTextSpan.setText(messages.formatMessage(StringUtils.EMPTY, + "backgroundWorkProgressDialog.progressTextSpan.textFormat", processedValue, total)); + } else { + int percentValue = (int) Math.ceil(currentProgressValue * 100); + progressTextSpan.setText(messages.formatMessage(StringUtils.EMPTY, + "backgroundWorkProgressDialog.progressTextSpan.percentFormat", percentValue)); + } + } + + protected void onCancelButtonClick(ClickEvent