org.jdesktop.application.Task Maven / Gradle / Ivy
Show all versions of tink-app Show documentation
/*
* Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
* subject to license terms.
*/
package org.jdesktop.application;
import java.awt.Component;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.annotation.Target;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jdesktop.swingworker.SwingWorker;
import org.jdesktop.swingworker.SwingWorker.StateValue;
/**
* A type of {@link SwingWorker} that represents an application
* background task. Tasks add descriptive properties that can
* be shown to the user, a new set of methods for customizing
* task completion, support for blocking input to the GUI while
* the Task is executing, and a {@code TaskListener} that
* enables one to monitor the three key SwingWorker methods: {@code
* doInBackground}, {@code process} and {@code done}.
*
*
* When a Task completes, the {@code final done} method invokes
* one of {@code succeeded}, {@code cancelled}, {@code interrupted},
* or {@code failed}. The {@code final done} method invokes
* {@code finished} when the completion method returns or throws
* an exception.
*
*
* Tasks should provide localized values for the {@code title},
* {@code description}, and {@code message} properties in a
* ResourceBundle for the Task subclass. A
* {@link ResourceMap} is
* loaded automatically using the Task subclass as the
* {@code startClass} and Task.class the {@code stopClass}.
* This ResourceMap is also used to look up format strings used in
* calls to {@link #message message}, which is used to set
* the {@code message} property.
*
*
* For example: given a Task called {@code MyTask} defined like this:
*
* class MyTask extends Task {
* protected MyResultType doInBackground() {
* message("startMessage", getPlannedSubtaskCount());
* // do the work ... if an error is encountered:
* message("errorMessage");
* message("finishedMessage", getActualSubtaskCount(), getFailureCount());
* // .. return the result
* }
* }
*
* Typically the resources for this class would be defined in the
* MyTask ResourceBundle, @{code resources/MyTask.properties}:
*
* title = My Task
* description = A task of mine for my own purposes.
* startMessage = Starting: working on %s subtasks...
* errorMessage = An unexpected error occurred, skipping subtask
* finishedMessage = Finished: completed %1$s subtasks, %2$s failures
*
*
*
* Task subclasses can override resource values in their own ResourceBundles:
*
* class MyTaskSubclass extends MyTask {
* }
* # resources/MyTaskSubclass.properties
* title = My Task Subclass
* description = An appropriate description
* # ... all other resources are inherited
*
*
*
* Tasks can specify that input to the GUI is to be blocked while
* they're being executed. The {@code inputBlocker} property specifies
* what part of the GUI is to be blocked and how that's accomplished.
* The {@code inputBlocker} is set automatically when an {@code @Action}
* method that returns a Task specifies a {@link BlockingScope} value
* for the {@code block} annotation parameter. To customize the way
* blocking is implemented you can define your own {@code Task.InputBlocker}.
* For example, assume that {@code busyGlassPane} is a component
* that consumes (and ignores) keyboard and mouse input:
*
* class MyInputBlocker extends InputBlocker {
* BusyIndicatorInputBlocker(Task task) {
* super(task, Task.BlockingScope.WINDOW, myGlassPane);
* }
* protected void block() {
* myFrame.setGlassPane(myGlassPane);
* busyGlassPane.setVisible(true);
* }
* protected void unblock() {
* busyGlassPane.setVisible(false);
* }
* }
* // ...
* myTask.setInputBlocker(new MyInputBlocker(myTask));
*
*
* All of the settable properties in this class are bound, i.e. a
* PropertyChangeEvent is fired when the value of the property changes.
* As with the {@code SwingWorker} superclass, all
* {@code PropertyChangeListeners} run on the event dispatching thread.
* This is also true of {@code TaskListeners}.
*
*
* Unless specified otherwise specified, this class is thread-safe.
* All of the Task properties can be get/set on any thread.
*
*
* @author Hans Muller ([email protected])
* @see ApplicationContext
* @see ResourceMap
* @see TaskListener
* @see TaskEvent
*/
public abstract class Task extends SwingWorker {
private static final Logger logger = Logger.getLogger(Task.class.getName());
private final Application application;
private String resourcePrefix;
private ResourceMap resourceMap;
private List> taskListeners;
private InputBlocker inputBlocker;
private String name = null;
private String title = null;
private String description = null;
private long messageTime = -1L;
private String message = null;
private long startTime = -1L;
private long doneTime = -1L;
private boolean userCanCancel = true;
private boolean progressPropertyIsValid = false;
private TaskService taskService = null;
/**
* Specifies to what extent the GUI should be blocked a Task
* is executed by a TaskService. Input blocking is carried
* out by the Task's {@link #getInputBlocker inputBlocker}.
*
* @see Task.InputBlocker
* @see Action#block
*/
public enum BlockingScope {
/**
* Don't block the GUI while this Task is executing.
*/
NONE,
/**
* Block an {@link ApplicationAction Action} while the
* task is executing, typically by temporarily disabling it.
*/
ACTION,
/**
* Block a component while the
* task is executing, typically by temporarily disabling it.
*/
COMPONENT,
/**
* Block a top level window while the task is executing,
* typically by showing a window-modal dialog.
*/
WINDOW,
/**
* Block all of the application's top level windows,
* typically by showing a application-modal dialog.
*/
APPLICATION
};
private void initTask(ResourceMap resourceMap, String prefix) {
this.resourceMap = resourceMap;
if ((prefix == null) || (prefix.length() == 0)) {
resourcePrefix = "";
}
else if (prefix.endsWith(".")) {
resourcePrefix = prefix;
}
else {
resourcePrefix = prefix + ".";
}
if (resourceMap != null) {
title = resourceMap.getString(resourceName("title"));
description = resourceMap.getString(resourceName("description"));
message = resourceMap.getString(resourceName("message"));
if (message != null) {
messageTime = System.currentTimeMillis();
}
}
addPropertyChangeListener(new StatePCL());
taskListeners = new CopyOnWriteArrayList>();
}
private ResourceMap defaultResourceMap(Application application) {
return application.getContext().getResourceMap(getClass(), Task.class);
}
/**
* Warning: This constructor is deprecated. It will be removed
* in a future release. This constructor was a way for developers
* to initialize a Task's title/description/message properties,
* and it's InputBlocker's visual properties, from an alternative
* ResourceMap. This feature is now supported with the
* InputBlocker's resourceMap property.
*
* Construct a {@code Task}. If the {@code resourceMap} parameter
* is not null, then the {@code title}, {@code description}, and
* {@code message} properties are initialized from resources. The
* {@code resourceMap} is also used to lookup localized messages
* defined with the {@link #message message} method. In both
* cases, if the value of {@code resourcePrefix} is not null or an
* empty string {@code ""}, resource names must have the name of
* the {@code resourcePrefix} parameter, followed by a ".", as a
* prefix
*
* @param resourceMap the ResourceMap for the Task's user properties, can be null
* @param resourcePrefix prefix for resource names, can be null
* @see #getResourceMap
* @see #setTitle
* @see #setDescription
* @see #setMessage
* @see #resourceName
* @see ApplicationContext#getResourceMap
*/
@Deprecated
public Task(Application application, ResourceMap resourceMap, String resourcePrefix) {
this.application = application;
initTask(resourceMap, resourcePrefix);
}
/**
* Warning: This constructor is deprecated. It will be removed
* in a future release. This constructor was a way for developers
* to initialize a Task's title/description/message properties,
* and it's InputBlocker's visual properties, from an alternative
* ResourceMap. This feature is now supported with the
* InputBlocker's resourceMap property.
*
* Construct a {@code Task} with the specified resource name
* prefix, whose ResourceMap is the value of
* ApplicationContext.getInstance().getResourceMap(this.getClass(), Task.class)
*
. The {@code resourcePrefix} is used to construct
* the resource names for the intial values of the
* {@code title}, {@code description}, and {@code message} Task properties
* and for message {@link java.util.Formatter format} strings.
*
* @param resourcePrefix prefix for resource names, can be null
* @see #getResourceMap
* @see #setTitle
* @see #setDescription
* @see #setMessage
* @see #resourceName
* @see ApplicationContext#getResourceMap
*/
@Deprecated
public Task(Application application, String resourcePrefix) {
this.application = application;
initTask(defaultResourceMap(application), resourcePrefix);
}
/**
* Construct a {@code Task} with an empty (""
) resource name
* prefix, whose ResourceMap is the value of
* ApplicationContext.getInstance().getResourceMap(this.getClass(),
* Task.class)
.
*/
public Task(Application application) {
this.application = application;
initTask(defaultResourceMap(application), "");
}
public final Application getApplication() {
return application;
}
public final ApplicationContext getContext() {
return getApplication().getContext();
}
/**
* Returns the TaskService that this Task has been submitted to,
* or null. This property is set when a task is executed by a
* TaskService, cleared when the task is done and all of its
* completion methods have run.
*
* This is a read-only bound property.
*
* @return the value of the taskService property.
* @see TaskService#execute
* @see #done
*/
public synchronized TaskService getTaskService() {
return taskService;
}
/**
* Set when a task is executed by a TaskService, cleared when
* the task is done and all of its completion methods have run.
*/
synchronized void setTaskService(TaskService taskService) {
TaskService oldTaskService, newTaskService;
synchronized(this) {
oldTaskService = this.taskService;
this.taskService = taskService;
newTaskService = this.taskService;
}
firePropertyChange("taskService", oldTaskService, newTaskService);
}
/**
* Returns a Task resource name with the specified suffix. Task resource
* names are the simple name of the constructor's {@code resourceClass}
* parameter, followed by ".", followed by {@code suffix}. If the
* resourceClass parameter was null, then this method returns an empty string.
*
* This method would only be of interest to subclasses that wanted to look
* up additional Task resources (beyond {@code title}, {@code message}, etc..)
* using the same naming convention.
*
* @param suffix the resource name's suffix
* @see #getResourceMap
* @see #message
*/
protected final String resourceName(String suffix) {
return resourcePrefix + suffix;
}
/**
* Returns the {@code ResourceMap} used by the constructor to
* initialize the {@code title}, {@code message}, etc properties,
* and by the {@link #message message} method to look up format
* strings.
*
* @return this Task's {@code ResourceMap}
* @see #resourceName
*/
public final ResourceMap getResourceMap() {
return resourceMap;
}
/**
* Return the value of the {@code title} property.
* The default value of this property is the value of the
* {@link #getResourceMap resourceMap's} {@code title} resource.
*
* Returns a brief one-line description of the this Task that would
* be useful for describing this task to the user. The default
* value of this property is null.
*
* @return the value of the {@code title} property.
* @see #setTitle
* @see #setDescription
* @see #setMessage
*/
public synchronized String getTitle() {
return title;
}
/**
* Set the {@code title} property.
* The default value of this property is the value of the
* {@link #getResourceMap resourceMap's} {@code title} resource.
*
* The title is a brief one-line description of the this Task that
* would be useful for describing it to the user. The {@code
* title} should be specific to this Task, for example "Loading
* image sunset.png" is better than "Image Loader". Similarly the
* title isn't intended for ephemeral messages, like "Loaded 27.3%
* of sunset.png". The {@link #setMessage message} property is
* for reporting the Task's current status.
*
* @param title a brief one-line description of the this Task.
* @see #getTitle
* @see #setDescription
* @see #setMessage
*/
protected void setTitle(String title) {
String oldTitle, newTitle;
synchronized(this) {
oldTitle = this.title;
this.title = title;
newTitle = this.title;
}
firePropertyChange("title", oldTitle, newTitle);
}
/**
* Return the value of the {@code description} property.
* The default value of this property is the value of the
* {@link #getResourceMap resourceMap's} {@code description} resource.
*
* A longer version of the Task's title; a few sentences that
* describe what the Task is for in terms that an application
* user would understand.
*
* @return the value of the {@code description} property.
* @see #setDescription
* @see #setTitle
* @see #setMessage
*/
public synchronized String getDescription() {
return description;
}
/**
* Set the {@code description} property.
* The default value of this property is the value of the
* {@link #getResourceMap resourceMap's} {@code description} resource.
*
* The description is a longer version of the Task's title.
* It should be a few sentences that describe what the Task is for,
* in terms that an application user would understand.
*
* @param description a few sentences that describe what this Task is for.
* @see #getDescription
* @see #setTitle
* @see #setMessage
*/
protected void setDescription(String description) {
String oldDescription, newDescription;
synchronized(this) {
oldDescription = this.description;
this.description = description;
newDescription = this.description;
}
firePropertyChange("description", oldDescription, newDescription);
}
/**
* Returns the length of time this Task has run. If the task hasn't
* started yet (i.e. if its state is still {@code StateValue.PENDING}),
* then this method returns 0. Otherwise it returns the duration in the
* specified time units. For example, to learn how many seconds a
* Task has run so far:
*
* long nSeconds = myTask.getExecutionDuration(TimeUnit.SECONDS);
*
*
* @param unit the time unit of the return value
* @return the length of time this Task has run.
* @see #execute
*/
public long getExecutionDuration(TimeUnit unit) {
long startTime, doneTime, dt;
synchronized(this) {
startTime = this.startTime;
doneTime = this.doneTime;
}
if (startTime == -1L) {
dt = 0L;
}
else if (doneTime == -1L) {
dt = System.currentTimeMillis() - startTime;
}
else {
dt = doneTime - startTime;
}
return unit.convert(Math.max(0L, dt), TimeUnit.MILLISECONDS);
}
/**
* Return the value of the {@code message} property.
* The default value of this property is the value of the
* {@link #getResourceMap resourceMap's} {@code message} resource.
*
* Returns a short, one-line, message that explains what the task
* is up to in terms appropriate for an application user.
*
* @return a short one-line status message.
* @see #setMessage
* @see #getMessageDuration
*/
public String getMessage() {
return message;
}
/**
* Set the {@code message} property.
* The default value of this property is the value of the
* {@link #getResourceMap resourceMap's} {@code message} resource.
*
* Returns a short, one-line, message that explains what the task is
* up to in terms appropriate for an application user. This message
* should reflect that Task's dynamic state and can be reset as
* frequently one could reasonably expect a user to understand.
* It should not repeat the information in the Task's title and
* should not convey any information that the user shouldn't ignore.
*
* For example, a Task whose {@code doInBackground} method loaded
* a photo from a web service might set this property to a new
* value each time a new internal milestone was reached, e.g.:
*
* loadTask.setTitle("Loading photo from http://photos.com/sunset");
* // ...
* loadTask.setMessage("opening connection to photos.com");
* // ...
* loadTask.setMessage("reading thumbnail image file sunset.png");
* // ... etc
*
*
* Each time this property is set, the {@link #getMessageDuration
* messageDuration} property is reset. Since status messages are
* intended to be ephemeral, application GUI elements like status
* bars may want to clear messages after 20 or 30 seconds have
* elapsed.
*
* Localized messages that require paramters can be constructed
* with the {@link #message message} method.
*
* @param message a short one-line status message.
* @see #getMessage
* @see #getMessageDuration
* @see #message
*/
protected void setMessage(String message) {
String oldMessage, newMessage;
synchronized(this) {
oldMessage = this.message;
this.message = message;
newMessage = this.message;
messageTime = System.currentTimeMillis();
}
firePropertyChange("message", oldMessage, newMessage);
}
/**
* Set the message property to a string generated with {@code
* String.format} and the specified arguments. The {@code
* formatResourceKey} names a resource whose value is a format
* string. See the Task class javadoc for an example.
*
* Note that if the no arguments are specified, this method is
* comparable to:
*
* setMessage(getResourceMap().getString(resourceName(formatResourceKey)));
*
*
* If a {@code ResourceMap} was not specified for this Task,
* then set the {@code message} property to {@code formatResourceKey}.
*
* @param formatResourceKey the suffix of the format string's resource name.
* @param args the arguments referred to by the placeholders in the format string
* @see #setMessage
* @see ResourceMap#getString(String, Object...)
* @see java.text.MessageFormat
*/
protected final void message(String formatResourceKey, Object... args) {
ResourceMap resourceMap = getResourceMap();
if (resourceMap != null) {
setMessage(resourceMap.getString(resourceName(formatResourceKey), args));
}
else {
setMessage(formatResourceKey);
}
}
/**
* Returns the length of time that has elapsed since the {@code
* message} property was last set.
*
* @param unit units for the return value
* @return elapsed time since the {@code message} property was last set.
* @see #setMessage
*/
public long getMessageDuration(TimeUnit unit) {
long messageTime;
synchronized(this) {
messageTime = this.messageTime;
}
long dt = (messageTime == -1L) ? 0L : Math.max(0L, System.currentTimeMillis() - messageTime);
return unit.convert(dt, TimeUnit.MILLISECONDS);
}
/**
* Returns the value of the {@code userCanCancel} property.
* The default value of this property is true.
*
* Generic GUI components, like a Task progress dialog, can use this
* property to decide if they should provide a way for the
* user to cancel this task.
*
* @return true if the user can cancel this Task.
* @see #setUserCanCancel
*/
public synchronized boolean getUserCanCancel() {
return userCanCancel;
}
/**
* Sets the {@code userCanCancel} property.
* The default value of this property is true.
*
* Generic GUI components, like a Task progress dialog, can use this
* property to decide if they should provide a way for the
* user to cancel this task. For example, the value of this
* property might be bound to the enabled property of a
* cancel button.
*
* This property has no effect on the {@link #cancel} cancel
* method. It's just advice for GUI components that display
* this Task.
*
* @param userCanCancel true if the user should be allowed to cancel this Task.
* @see #getUserCanCancel
*/
protected void setUserCanCancel(boolean userCanCancel) {
boolean oldValue, newValue;
synchronized(this) {
oldValue = this.userCanCancel;
this.userCanCancel = userCanCancel;
newValue = this.userCanCancel;
}
firePropertyChange("userCanCancel", oldValue, newValue);
}
/**
* Returns true if the {@link #setProgress progress} property has
* been set. Some Tasks don't update the progress property
* because it's difficult or impossible to determine how what
* percentage of the task has been completed. GUI elements that
* display Task progress, like an application status bar, can use this
* property to set the @{link JProgressBar#indeterminate
* indeterminate} @{code JProgressBar} property.
*
* A task that does keep the progress property up to
* date should initialize it to 0, to ensure that {@code
* isProgressPropertyValid} is always true.
*
* @return true if the {@link #setProgress progress} property has been set.
* @see #setProgress
*/
public synchronized boolean isProgressPropertyValid() {
return progressPropertyIsValid;
}
/**
* A convenience method that sets the {@code progress} property to the following
* ratio normalized to 0 .. 100.
*
* value - min / max - min
*
*
* @param value a value in the range min ... max, inclusive
* @param min the minimum value of the range
* @param max the maximum value of the range
* @see #setProgress(int)
*/
protected final void setProgress(int value, int min, int max) {
if (min >= max) {
throw new IllegalArgumentException("invalid range: min >= max");
}
if ((value < min) || (value > max)) {
throw new IllegalArgumentException("invalid value");
}
float percentage = (float)(value - min) / (float)(max - min);
setProgress(Math.round(percentage * 100.0f));
}
/**
* A convenience method that sets the {@code progress} property to
* percentage * 100
.
*
* @param percentage a value in the range 0.0 ... 1.0 inclusive
* @see #setProgress(int)
*/
protected final void setProgress(float percentage) {
if ((percentage < 0.0) || (percentage > 1.0)) {
throw new IllegalArgumentException("invalid percentage");
}
setProgress(Math.round(percentage * 100.0f));
}
/**
* A convenience method that sets the {@code progress} property to the following
* ratio normalized to 0 .. 100.
*
* value - min / max - min
*
*
* @param value a value in the range min ... max, inclusive
* @param min the minimum value of the range
* @param max the maximum value of the range
* @see #setProgress(int)
*/
protected final void setProgress(float value, float min, float max) {
if (min >= max) {
throw new IllegalArgumentException("invalid range: min >= max");
}
if ((value < min) || (value > max)) {
throw new IllegalArgumentException("invalid value");
}
float percentage = (value - min) / (max - min);
setProgress(Math.round(percentage * 100.0f));
}
/**
* Equivalent to {@code getState() == StateValue.PENDING}.
*
* When a pending Task's state changes to {@code StateValue.STARTED}
* a PropertyChangeEvent for the "started" property is fired. Similarly
* when a started Task's state changes to {@code StateValue.DONE}, a
* "done" PropertyChangeEvent is fired.
*/
public final boolean isPending() {
return getState() == StateValue.PENDING;
}
/**
* Equivalent to {@code getState() == StateValue.STARTED}.
*
* When a pending Task's state changes to {@code StateValue.STARTED}
* a PropertyChangeEvent for the "started" property is fired. Similarly
* when a started Task's state changes to {@code StateValue.DONE}, a
* "done" PropertyChangeEvent is fired.
*/
public final boolean isStarted() {
return getState() == StateValue.STARTED;
}
/**
* {@inheritDoc}
*
* This method fires the TaskListeners' {@link TaskListener#process process}
* method. If you override {@code process} and do not call
* {@code super.process(values)}, then the TaskListeners will not run.
*
* @param values @{inheritDoc}
*/
@Override protected void process(List values) {
fireProcessListeners(values);
}
@Override protected final void done() {
try {
if (isCancelled()) {
cancelled();
}
else {
try {
succeeded(get());
}
catch (InterruptedException e) {
interrupted(e);
}
catch (ExecutionException e) {
failed(e.getCause());
}
}
}
finally {
try {
finished();
}
finally {
setTaskService(null);
}
}
}
/**
* Called when this Task has been cancelled by {@link #cancel(boolean)}.
*
* This method runs on the EDT. It does nothing by default.
*
* @see #done
*/
protected void cancelled() {
}
/**
* Called when this Task has successfully completed, i.e. when
* its {@code get} method returns a value. Tasks that compute
* a value should override this method.
*
*
* This method runs on the EDT. It does nothing by default.
*
* @param result the value returned by the {@code get} method
* @see #done
* @see #get
* @see #failed
*/
protected void succeeded(T result) {
}
/**
* Called if the Task's Thread is interrupted but not
* explicitly cancelled.
*
* This method runs on the EDT. It does nothing by default.
*
* @param e the {@code InterruptedException} thrown by {@code get}
* @see #cancel
* @see #done
* @see #get
*/
protected void interrupted(InterruptedException e) {
}
/**
* Called when an execution of this Task fails and an
* {@code ExecutionExecption} is thrown by {@code get}.
*
* This method runs on the EDT. It Logs an error message by default.
*
* @param cause the {@link Throwable#getCause cause} of the {@code ExecutionException}
* @see #done
* @see #get
* @see #failed
*/
protected void failed(Throwable cause) {
String msg = String.format("%s failed: %s", this, cause);
logger.log(Level.SEVERE, msg, cause);
}
/**
* Called unconditionally (in a {@code finally} clause) after one
* of the completion methods, {@code succeeded}, {@code failed},
* {@code cancelled}, or {@code interrupted}, runs. Subclasses
* can override this method to cleanup before the {@code done}
* method returns.
*
* This method runs on the EDT. It does nothing by default.
*
* @see #done
* @see #get
* @see #failed
*/
protected void finished() {
}
/**
* Adds a {@code TaskListener} to this Task. The listener will
* be notified when the Task's state changes to {@code STARTED},
* each time the {@code process} method is called, and when the
* Task's state changes to {@code DONE}. All of the listener
* methods will run on the event dispatching thread.
*
* @param listener the {@code TaskListener} to be added
* @see #removeTaskListener
*/
public void addTaskListener(TaskListener listener) {
if (listener == null) {
throw new IllegalArgumentException("null listener");
}
taskListeners.add(listener);
}
/**
* Removes a {@code TaskListener} from this Task. If the specified
* listener doesn't exist, this method does nothing.
*
* @param listener the {@code TaskListener} to be added
* @see #addTaskListener
*/
public void removeTaskListener(TaskListener listener) {
if (listener == null) {
throw new IllegalArgumentException("null listener");
}
taskListeners.remove(listener);
}
/**
* Returns a copy of this Task's {@code TaskListeners}.
*
* @return a copy of this Task's {@code TaskListeners}.
* @see #addTaskListener
* @see #removeTaskListener
*/
public TaskListener[] getTaskListeners() {
return taskListeners.toArray(new TaskListener[taskListeners.size()]);
}
/* This method is guaranteed to run on the EDT, it's called
* from SwingWorker.process().
*/
private void fireProcessListeners(List values) {
TaskEvent> event = new TaskEvent(this, values);
for(TaskListener listener : taskListeners) {
listener.process(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireDoInBackgroundListeners() {
TaskEvent event = new TaskEvent(this, null);
for(TaskListener listener : taskListeners) {
listener.doInBackground(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireSucceededListeners(T result) {
TaskEvent event = new TaskEvent(this, result);
for(TaskListener listener : taskListeners) {
listener.succeeded(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireCancelledListeners() {
TaskEvent event = new TaskEvent(this, null);
for(TaskListener listener : taskListeners) {
listener.cancelled(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireInterruptedListeners(InterruptedException e) {
TaskEvent event = new TaskEvent(this, e);
for(TaskListener listener : taskListeners) {
listener.interrupted(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireFailedListeners(Throwable e) {
TaskEvent event = new TaskEvent(this, e);
for(TaskListener listener : taskListeners) {
listener.failed(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireFinishedListeners() {
TaskEvent event = new TaskEvent(this, null);
for(TaskListener listener : taskListeners) {
listener.finished(event);
}
}
/* This method runs on the EDT because it's called from
* StatePCL (see below).
*/
private void fireCompletionListeners() {
try {
if (isCancelled()) {
fireCancelledListeners();
}
else {
try {
fireSucceededListeners(get());
}
catch (InterruptedException e) {
fireInterruptedListeners(e);
}
catch (ExecutionException e) {
fireFailedListeners(e.getCause());
}
}
}
finally {
fireFinishedListeners();
}
}
private class StatePCL implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent e) {
String propertyName = e.getPropertyName();
if ("state".equals(propertyName)) {
StateValue state = (StateValue)(e.getNewValue());
Task task = (Task)(e.getSource());
switch (state) {
case STARTED: taskStarted(task); break;
case DONE: taskDone(task); break;
}
}
else if ("progress".equals(propertyName)) {
synchronized(Task.this) {
progressPropertyIsValid = true;
}
}
}
private void taskStarted(Task task) {
synchronized(Task.this) {
startTime = System.currentTimeMillis();
}
firePropertyChange("started", false, true);
fireDoInBackgroundListeners();
}
private void taskDone(Task task) {
synchronized(Task.this) {
doneTime = System.currentTimeMillis();
}
try {
task.removePropertyChangeListener(this);
firePropertyChange("done", false, true);
fireCompletionListeners();
}
finally {
firePropertyChange("completed", false, true);
}
}
}
/**
* Return this task's InputBlocker.
*
* This is a bound property.
*
* @see #setInputBlocker
*/
public final InputBlocker getInputBlocker() {
return inputBlocker;
}
/**
* Set this task's InputBlocker. The InputBlocker defines
* to what extent the GUI should be blocked while the Task
* is executed by a TaskService. It is not used by the Task
* directly, it's used by the TaskService that executes the Task.
*
* This property may only be set before the Task is
* {@link TaskService#execute submitted} to a TaskService for
* execution. If it's called afterwards, an IllegalStateException
* is thrown.
*
* This is a bound property.
*
* @see #getInputBlocker
*/
public final void setInputBlocker(InputBlocker inputBlocker) {
if (getTaskService() != null) {
throw new IllegalStateException("task already being executed");
}
InputBlocker oldInputBlocker, newInputBlocker;
synchronized(this) {
oldInputBlocker = this.inputBlocker;
this.inputBlocker = inputBlocker;
newInputBlocker = this.inputBlocker;
}
firePropertyChange("inputBlocker", oldInputBlocker, newInputBlocker);
}
/**
* Specifies to what extent input to the Application's GUI should
* be blocked while this Task is being executed and provides
* a pair of methods, {@code block} and {@code unblock} that
* do the work of blocking the GUI. For the sake of input blocking,
* a Task begins executing when it's {@link TaskService#execute submitted}
* to a {@code TaskService}, and it finishes executing after
* the Task's completion methods have been called.
*
* The InputBlocker's {@link Task.BlockingScope
* BlockingScope} and the blocking {@code target} object define
* what part of the GUI's input will be blocked:
*
* Task.BlockingScope.NONE
- Don't block input. The blocking
* target is ignored in this case.
*
Task.BlockingScope.ACTION
- Disable the target
* {@link javax.swing.Action Action} while the Task is executing.
*
Task.BlockingScope.COMPONENT
- Disable the target
* {@link java.awt.Component} Component while the Task is executing.
*
Task.BlockingScope.WINDOW
- Block the Window ancestor of the
* target Component while the Task is executing.
*
Task.BlockingScope.Application
- Block the entire Application
* while the Task is executing. The blocking target is ignored
* in this case.
*
*
* Input blocking begins when the {@code block} method is called and
* ends when {@code unblock} is called. Each method is only
* called once, typically by the {@code TaskService}.
*
* @see Task#getInputBlocker
* @see Task#setInputBlocker
* @see TaskService
* @see Action
*/
public static abstract class InputBlocker extends AbstractBean {
private final Task task;
private final BlockingScope scope;
private final Object target;
private final ApplicationAction action;
/**
* Construct an InputBlocker with four immutable properties. If
* the Task is null or if the Task has already been
* executed by a TaskService, then an exception is thrown.
* If scope is {@code BlockingScope.ACTION} then target must be
* a {@link javax.swing.Action Action}. If scope is
* {@code BlockingScope.WINDOW} or {@code BlockingScope.COMPONENT}
* then target must be a Component.
*
* @param task block input while this Task is executing
* @param scope how much of the GUI will be blocked
* @param target the GUI element that will be blocked
* @param action the {@code @Action} that triggered running the task, or null
* @see TaskService#execute
*/
public InputBlocker(Task task, BlockingScope scope, Object target, ApplicationAction action) {
if (task == null) {
throw new IllegalArgumentException("null task");
}
if (task.getTaskService() != null) {
throw new IllegalStateException("task already being executed");
}
switch (scope) {
case ACTION:
if (!(target instanceof javax.swing.Action)) {
throw new IllegalArgumentException("target not an Action");
}
break;
case COMPONENT:
case WINDOW:
if (!(target instanceof Component)) {
throw new IllegalArgumentException("target not a Component");
}
break;
}
this.task = task;
this.scope = scope;
this.target = target;
this.action = action;
}
/**
* Construct an InputBlocker. If {@code target} is an
* {@code ApplicationAction}, it becomes the InputBlocker's
* {@code action}. If the Task is null or if the Task has already been
* executed by a TaskService, then an exception is thrown.
*
* @param task block input while this Task is executing
* @param scope how much of the GUI will be blocked
* @param target the GUI element that will be blocked
* @see TaskService#execute
*/
public InputBlocker(Task task, BlockingScope scope, Object target) {
this(task, scope, target, (target instanceof ApplicationAction) ? (ApplicationAction)target : null);
}
/**
* The {@code block} method will block input while this Task
* is being executed by a TaskService.
*
* @return the value of the read-only Task property
* @see #block
* @see #unblock
*/
public final Task getTask() { return task; }
/**
* Defines the extent to which the GUI is blocked while
* the task is being executed.
*
* @return the value of the read-only blockingScope property
* @see #block
* @see #unblock
*/
public final BlockingScope getScope() { return scope; }
/**
* Specifies the GUI element that will be blocked while
* the task is being executed.
*
* This property may be null.
*
* @return the value of the read-only target property
* @see #getScope
* @see #block
* @see #unblock
*/
public final Object getTarget() { return target; }
/**
* The ApplicationAction ({@code @Action}) that caused
* the task to be executed. The DefaultInputBlocker uses
* the action's {@code name} and {@code resourceMap} to
* configure its blocking dialog if {@code scope} is
* {@code BlockingScope.WINDOW}.
*
* This property may be null.
*
* @return the value of the read-only action property
* @see #getScope
* @see #block
* @see #unblock
* @see ApplicationAction#getName
* @see ApplicationAction#getResourceMap
*/
public final ApplicationAction getAction() { return action; }
/**
* Block input to the GUI per the {@code scope} and {@code target}
* properties. This method will only be called once.
*
* @see #unblock
* @see TaskService#execute
*/
protected abstract void block();
/**
* Unblock input to the GUI by undoing whatever the {@code block}
* method did. This method will only be called once.
*
* @see #block
* @see TaskService#execute
*/
protected abstract void unblock();
}
}