jaxx.runtime.swing.AbstractActionThread Maven / Gradle / Ivy
package jaxx.runtime.swing;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuiton.util.StringUtil;
import javax.swing.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* An abstract action thread to consume actions in a non Swing event thread.
*
* Implements the method {@code onActionXXX(ActionWorker)} to hook on action
* status.
*
* To consume an action, use the method {@link #addAction(String, Runnable)}.
*
* TODO Make this multi-action enable
*
* @author tchemit
* @since 2.0
*/
public abstract class AbstractActionThread extends Thread {
/** Logger */
private static final Log log =
LogFactory.getLog(AbstractActionThread.class);
/** l'état du thread si annulé */
private boolean canceled;
/**
* un lock pour permettre la suspension et la reprise du thread lors du mode
* interactif.
*/
private final Object LOCK = new Object();
/** current worker to execute action */
protected ActionWorker worker;
/** the listener of running action */
protected final PropertyChangeListener workerListener;
/** State of a running action */
enum ActionStatus {
OK,
CANCEL,
FAIL
}
/** Action worker to execute a incoming action. */
public static class ActionWorker extends SwingWorker {
protected final String actionLabel;
protected final Runnable target;
protected ActionStatus status;
protected Exception error;
protected long startTime;
protected long endTime;
public ActionWorker(String actionLabel, Runnable target) {
this.target = target;
this.actionLabel = actionLabel;
}
@Override
protected Void doInBackground() throws Exception {
startTime = System.nanoTime();
if (log.isDebugEnabled()) {
log.debug("Action [" + getActionLabel() + "] is starting...");
}
try {
target.run();
} catch (Exception e) {
error = e;
} finally {
if (log.isDebugEnabled()) {
log.debug("Action [" + getActionLabel() + "] is ending...");
}
}
return null;
}
public boolean isFailed() {
return (isDone() || isCancelled()) && error != null;
}
public Exception getError() {
return error;
}
public ActionStatus getStatus() {
return status;
}
public String getActionLabel() {
return actionLabel;
}
@Override
protected void done() {
super.done();
endTime = System.nanoTime();
if (error != null) {
status = ActionStatus.FAIL;
} else if (isCancelled()) {
status = ActionStatus.CANCEL;
} else {
status = ActionStatus.OK;
}
if (log.isDebugEnabled()) {
log.debug("Action [" + getActionLabel() + "] ends with status : " + status + " in " + getTime());
}
}
public String getTime() {
return StringUtil.convertTime(endTime - startTime);
}
public long getStartTime() {
return startTime;
}
public long getEndTime() {
return endTime;
}
}
protected AbstractActionThread(String name) {
super(name);
workerListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (log.isDebugEnabled()) {
log.debug("action " + evt.getSource() + " property " +
evt.getPropertyName() + " changed <" +
evt.getOldValue() + " - " + evt.getNewValue() +
'>');
}
if ("state".equals(evt.getPropertyName())) {
ActionWorker source = (ActionWorker) evt.getSource();
SwingWorker.StateValue state =
(SwingWorker.StateValue) evt.getNewValue();
if (state == SwingWorker.StateValue.STARTED) {
// starting new action
onActionStart(source);
return;
}
if (state == SwingWorker.StateValue.DONE) {
// on rend la main au thread pour qu'il attende une
// prochaine operation
ActionStatus status = source.getStatus();
if (log.isDebugEnabled()) {
log.debug("Action [" + source.getActionLabel() +
"] status = " + status);
}
try {
switch (status) {
case OK:
onActionEnd(source);
break;
case CANCEL:
onActionCancel(source);
break;
case FAIL:
onActionFail(source);
break;
}
} finally {
// release thread
setWaiting(false);
}
}
}
}
};
}
/**
* Creates a runnable instance (via a Proxy) to a method given by his name
* ({@code methodName}) to invoke on {@code methodcontainer} with given
* {@code arguments}.
*
* This is a great feature to create runnable code with a real context.
*
* @param methodContainer the container of the method to invoke
* @param methodName the name of the method to invoke
* @param arguments parameters to pass to method to invke.
* @return the proxy instance
*/
public Runnable createRunnable(final Object methodContainer,
String methodName,
final Object... arguments) {
// find method
Class> klass = methodContainer.getClass();
Method mFound = null;
for (Method m : klass.getDeclaredMethods()) {
if (!methodName.equals(m.getName())) {
continue;
}
//same method name
Class>[] types = m.getParameterTypes();
if (arguments.length != types.length) {
continue;
}
// same number arguments
mFound = m;
break;
}
if (mFound == null) {
throw new IllegalArgumentException(
"could not find method " + methodName + " on type " +
klass.getName());
}
//TODO Test arguments are on good type...
final Method targetMethod = mFound;
targetMethod.setAccessible(true);
Runnable result;
// create runnable proxy
result = (Runnable) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class>[]{Runnable.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy,
Method method,
Object[] args) {
String methodName = method.getName();
if ("run".equals(methodName)) {
try {
if (log.isDebugEnabled()) {
log.debug("will invoke run method");
}
return targetMethod.invoke(methodContainer, arguments);
} catch (IllegalAccessException e) {
throw new RuntimeException(
"could not invoke on container " +
methodContainer, e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
if (methodName.equals("toString")) {
return toString();
}
if (methodName.equals("equals")) {
return equals(args[0]);
}
if (methodName.equals("hashCode")) {
return hashCode();
}
return null;
}
}
);
return result;
}
/**
* Add an new action to perform.
*
* @param actionLabel the name of the action to perform
* @param action the action to perform
* @return the worker that will launch the action
*/
public ActionWorker addAction(String actionLabel, Runnable action) {
if (worker != null && !worker.isDone()) {
// on ne peut traiter qu'une seule opération à la fois
throw new IllegalStateException(
"can not add a operation when thread is busy, or has" +
" another operation to be done");
}
if (action instanceof ActionWorker) {
worker = (ActionWorker) action;
} else {
worker = new ActionWorker(actionLabel, action);
}
// on libere le thread pour qu'il execute l'opération
setWaiting(false);
return worker;
}
/**
* Hook when a action is about to start.
*
* @param source the action worker containing the action to perform
*/
public abstract void onActionStart(ActionWorker source);
/**
* Hook when a action has failed.
*
* @param source the action worker containing the action to perform
*/
public abstract void onActionFail(ActionWorker source);
/**
* Hook when a action has been canceled.
*
* @param source the action worker containing the action to perform
*/
public abstract void onActionCancel(ActionWorker source);
/**
* Hook when a action has end with no failure or cancel.
*
* @param source the action worker containing the action to perform
*/
public abstract void onActionEnd(ActionWorker source);
@Override
public void run() {
if (log.isInfoEnabled()) {
log.info("starting... " + this);
}
try {
while (!canceled) {
if (canceled) {
// une annulation a été demandé
// donc même si une opération est demandée, on ne la traite
// pas
break;
}
// en attente qu'une opération
// le block est bloqué jusqu'à arrivée d'une opération
// ou une demande d'annulation
setWaiting(true);
// le thread a repris la main, donc plus en attente
log.trace("no more waiting " + this);
if (!canceled) {
// une opération a été demandée
try {
// le thread écoute les modifications de l'action
worker.addPropertyChangeListener(workerListener);
// démarrage de l'opération dans un worker
worker.execute();
// le thread est bloqué jusqu'à la fin de l'opération
// ou une demande d'annulation
setWaiting(true);
// le thread reprend la main des que l'operation
// est terminée ou a été annulée, on passera alors
// dans la méthode onPropertyChanged
} finally {
if (worker != null) {
// le thread n'écoute plus l'action car elle est terminée
// ou annulée
worker.removePropertyChangeListener(workerListener);
// suppression de l'action
worker = null;
}
}
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unlockThread();
if (log.isInfoEnabled()) {
log.info(this + " will close...");
}
close();
}
}
/**
* Cancel the thread, this will release any lock of the tread.
*
* As a side effect, this will close the thread.
*/
public void cancel() {
log.info("cancel " + this);
canceled = true;
// on rend la main au thread
setWaiting(false);
}
/** La méthode pour nettoyer le thread, a la fermeture. */
protected void close() {
// par defaut, on ne fait rien
log.info(this);
}
/**
* Mutates the waiting state of the thread.
*
* If parameter {@code waiting} is to {@code true}, then will lock the
* thread, otherwise will unlock the thread.
*
* @param waiting {@code true} if a lock is required
*/
protected void setWaiting(boolean waiting) {
if (waiting && !canceled) {
// locking thread
try {
lockThread();
} catch (InterruptedException ex) {
log.error(ex.getMessage(), ex);
canceled = true;
}
}
if (!waiting) {
// release lock
unlockThread();
}
}
/**
* To lock the thread.
*
* @throws InterruptedException if locking was interruped
*/
protected void lockThread() throws InterruptedException {
synchronized (LOCK) {
log.trace(this);
// lock
LOCK.wait();
}
}
/** To unlock the thread. */
protected void unlockThread() {
synchronized (LOCK) {
log.trace(this);
// unlock
LOCK.notify();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy