+ * 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:
+ *
+ *
+ * @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