Java tutorial
/* * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org> * * This file is part of the Kitodo project. * * It is licensed under GNU General Public License version 3 or later. * * For the full copyright and license information, please read the * GPL3-License.txt file that was distributed with this source code. */ package org.kitodo.production.helper.tasks; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.exception.ExceptionUtils; import org.joda.time.Duration; import org.kitodo.production.helper.Helper; /** * The class EmptyTask is the base class for worker threads that operate * independently to do the work in the background. The name empty task points * out that the task doesnt do anything sensible yet. It is here to be * extended. * * @author Matthias Ronge <matthias.ronge@zeutschel.de> */ public class EmptyTask extends Thread implements INameableTask { /** * The enum Actions lists the available instructions to the housekeeper what * to do with a terminated thread. These are: * * <dl> * <dt><code>DELETE_IMMEDIATELY</code></dt> * <dd>The thread shall be disposed of as soon as is has gracefully stopped. * </dd> * <dt><code>KEEP_FOR_A_WHILE</code></dt> * <dd>The default behaviour: A thread that terminated either normally or * abnormally is kept around in memory for a while and then removed * automatically. Numeric and temporary limits can be configured.</dd> * <dt><code>PREPARE_FOR_RESTART</code></dt> * <dd>If the thread was interrupted by a user, replace it by a new one, * passing in the state of the old one to be able to continue work.</dd> * </dl> * * @author Matthias Ronge <matthias.ronge@zeutschel.de> */ public enum Behaviour { DELETE_IMMEDIATELY, KEEP_FOR_A_WHILE, PREPARE_FOR_RESTART } /** * The constant CATCH_ALL holds an UncaughtExceptionHandler implementation * which will automatically be attached to all task threads. Otherwise * exceptions might get lost or even bring the runtime to crash. */ public static final Thread.UncaughtExceptionHandler CATCH_ALL = (origin, exception) -> { if (origin instanceof EmptyTask) { EmptyTask task = (EmptyTask) origin; task.setException(exception); } }; /** * The constant DEFAULT_BEHAVIOUR defines the default behaviour of the * TaskKeeper towards a task that terminated. The default behaviour is that * it will be kept in the front end as configured in the global * configuration file and then will be deleted. */ private static final Behaviour DEFAULT_BEHAVIOUR = Behaviour.KEEP_FOR_A_WHILE; /** * The field behaviour defines the behaviour of the TaskKeeper towards the * task if it has terminated. Setting this field to DELETE_IMMEDIATELY will * also result in the desired behaviour if the task has not yet been started * at all. */ private Behaviour behaviour; /** * The field detail holds a string giving some details about what the thread * is doing that do not require translation, i.e. which file is currently * processed. */ private String detail = null; /** * The field exception is designated to take an exception if one occurred. */ private Exception exception = null; /** * The field passedAway will be initialised with a time stamp as the thread * dies to be able to remove it a defined timespan after it died. */ private Long passedAway = null; /** * The field progress holds one out of 101 values, ranging from 0 to 100 to * indicate the progress of the work. This will be shown as a progress bar * in the front end. */ private int progress = 0; /** * Default constructor. Creates an empty thread. * * @param nameDetail * a detail that is helpful when being shown, may be null */ public EmptyTask(String nameDetail) { setDaemon(true); setNameDetail(nameDetail); } /** * Copy constructor. Required for cloning tasks. Cloning is required to be * able to restart a task. * * @param master * instance to make a copy from */ protected EmptyTask(EmptyTask master) { setDaemon(true); setName(master.getName()); this.behaviour = master.behaviour; this.detail = master.detail; this.exception = master.exception; this.passedAway = master.passedAway; this.progress = master.progress; } /** * Calls the copy constructor to create a not-yet-executed replacement copy * of that thread object. Every subclass must provide its own copy * constructorwhich must call super(objectToCopy)and overload this method * to call its own copy constructor. * * @return a not-yet-executed replacement of this thread */ public EmptyTask replace() { return new EmptyTask(this); } /** * The function getBehaviour() returns the instruction how the TaskSitter * shall behave towards this task. Usually, the behaviour isnt set while * the task is under normal execution. It can be set by calling * {@link #interrupt(Behaviour)}. It may also be set this way if the task is * still new and wasnt even started. The following instructions are * available: * * <dl> * <dt><code>DELETE_IMMEDIATELY</code></dt> * <dd>The thread shall be disposed of as soon as is has gracefully stopped. * </dd> * <dt><code>KEEP_FOR_A_WHILE</code></dt> * <dd>The default behaviour: A thread that terminated either normally or * abnormally is kept around in memory for a while and then removed * automatically. Numeric and temporary limits can be configured.</dd> * <dt><code>PREPARE_FOR_RESTART</code></dt> * <dd>If the thread was interrupted by a user, replace it by a new one, * passing in the state of the old one to be able to continue work.</dd> * </dl> * * @return how the TaskSitter shall behave towards this task */ Behaviour getBehaviour() { return behaviour; } /** * Returns the display name of the task to show to the user. * * @see org.kitodo.production.helper.tasks.INameableTask#getDisplayName() */ @Override public String getDisplayName() { return Helper.getTranslation("EmptyTask"); } /** * The function getDurationDead() returns the duration the task is dead. If * a time of death has not yet been recorded, null is returned. * * @return the duration since the task died */ Duration getDurationDead() { if (Objects.isNull(passedAway)) { return null; } long elapsed = System.nanoTime() - passedAway; return new Duration(TimeUnit.MILLISECONDS.convert(elapsed, TimeUnit.NANOSECONDS)); } /** * The function getException() provides access to the exception that * occurred if the thread died abnormally. If no exception has occurred yet * or it wasnt properly recorded, null is returned. * * @return the exception occurred, or null if no exception occurred yet */ public Exception getException() { return exception; } /** * The function getProgress() returns the progress of the task in percent, * i.e. in a range from 0 to 100. * * @return the progress of the task */ public int getProgress() { return progress; } /** * The function getStateDescription() returns a text string representing the * state of the current task as read-only property "stateDescription". * * @return a string representing the state of the task */ public String getStateDescription() { TaskState state = getTaskState(); String label = Helper.getTranslation(state.toString().toLowerCase()); switch (state) { case WORKING: if (Objects.nonNull(detail)) { return label + " (" + detail + ")"; } else { return label; } case CRASHED: if (Objects.nonNull(exception.getMessage())) { return label + " (" + exception.getMessage() + ")"; } else if (Objects.nonNull(detail)) { return label + " (" + detail + ")"; } else { return label + " (" + exception.getClass().getSimpleName() + ")"; } default: return label; } } /** * The function getTaskState() returns the task state. It can be one of * the followings: * * <dl> * <dt><code>CRASHED</code></dt> * <dd>The thread has terminated abnormally. The field exception? is * holding the exception that has occurred.</dd> * <dt><code>FINISHED</code></dt> * <dd>The thread has finished its work without errors and is available for * clean-up.</dd> * <dt><code>NEW</code></dt> * <dd>The thread has not yet been started.</dd> * <dt><code>STOPPED</code></dt> * <dd>The thread was stopped by a front end userresulting in a call to its * {@link #interrupt(Behaviour)} method with {@link Behaviour} * .PREPARE_FOR_RESTART and is able to restart after cloning and replacing * it.</dd> * <dt><code>STOPPING</code></dt> * <dd>The thread has received a request to interrupt but didnt stop * yet.</dd> * <dt><code>WORKING</code></dt> * <dd>The thread is in operation.</dd> * </dl> * * @return the task state */ TaskState getTaskState() { switch (getState()) { case NEW: return TaskState.NEW; case TERMINATED: if (Objects.isNull(behaviour)) { behaviour = DEFAULT_BEHAVIOUR; } if (Objects.nonNull(exception)) { return TaskState.CRASHED; } if (Behaviour.PREPARE_FOR_RESTART.equals(behaviour)) { return TaskState.STOPPED; } else { return TaskState.FINISHED; } default: if (isInterrupted()) { return TaskState.STOPPING; } else { return TaskState.WORKING; } } } /** * The function getLongMessage() returns the read-only field "longMessage" * which will be shown in a pop-up window. * * @return the stack trace of the exception, if any */ public String getLongMessage() { if (Objects.isNull(exception)) { return null; } return ExceptionUtils.getStackTrace(exception); } /** * The function interrupt() interrupts this thread and allows to set a * behaviour after interruption. * * @param mode * how to behave after interruption */ public void interrupt(Behaviour mode) { behaviour = mode; interrupt(); } /** * The function isStartable() returns wether the start button shall be shown * as read-only property "startable". A thread can be started as long as it * has not yet been started. * * @return whether the start button shall show */ public boolean isStartable() { return getState().equals(State.NEW); } /** * The function isStoppable() returns wether the stop button shall be shown * as read-only property "stopable". A thread can be stopped if it is * working. * * @return whether the stop button shall show */ public boolean isStoppable() { return getTaskState().equals(TaskState.WORKING); } /** * The function isDeleteable() returns whether the delete button shall be * shown as read-only property "deleteable". In our interpretation, a thread * is deleteable if it is either new or has terminated and is still lounging * around. * * @return whether the delete button shall show */ public boolean isDeleteable() { switch (getState()) { case NEW: case TERMINATED: return !Behaviour.DELETE_IMMEDIATELY.equals(behaviour); default: return false; } } /** * This is a sample implementation of run() which simulates a long running * task? but does nothing and just fills up the percentage gauge. It isnt * useful for anything but testing or demonstration purposes. * * @see java.lang.Thread#run() */ @Override public void run() { for (int i = progress + 1; i <= 100; i++) { // tell user some details what you are currently working on setWorkDetail(Integer.toString(i)); // do something try { sleep(1024); } catch (InterruptedException e) { this.interrupt(); } // set progress setProgress(i); // The thread may have been signaled to stop. If so, leave if (isInterrupted()) { return; } } // Were done. There is nothing more to do. } /** * The procedure setException can be used to save an exception that occurred * and show it in the front end. It will only record the first exception * (which is likely to be the source of all the misery) and it will not * record an InterruptedException if the thread has already been * interrupted. * * @param exception * exception to save */ public void setException(Throwable exception) { if (exception instanceof InterruptedException) { Thread.currentThread().interrupt(); } if (Objects.isNull(this.exception) && (!isInterrupted() || !(exception instanceof InterruptedException))) { if (exception instanceof Exception) { this.exception = (Exception) exception; } else { this.exception = new ExecutionException(exception.getMessage(), exception); } } } /** * The procedure setNameDetail() may be used to set the tasks name along * with a detail that doesnt require translation and is helpful when being * shown in the front end (such as the name of the entity the task is based * on). The name detail should be set once (in the constructor). You may * pass in null to reset the name and remove the detail. * * <p> * I.e., if your task is about creation of OCR for a process, the detail * here could be the process title. * </p> * * @param detail * a name detail, may be null */ protected void setNameDetail(String detail) { StringBuilder composer = new StringBuilder(119); composer.append(this.getDisplayName()); if (Objects.nonNull(detail)) { composer.append(": "); composer.append(detail); } super.setName(composer.toString()); } /** * The procedure setProgress() may be used to set the tasks progress in * percent (i.e., from 0 to 100). * * @param progress * the tasks progress */ public void setProgress(int progress) { if (progress < 0 || progress > 100) { throw new IllegalArgumentException("Progress out of range: " + progress); } this.progress = progress; } /** * The procedure setProgress() may be used to set the tasks progress in * percent (i.e., from 0 to 100). * * @param statusProgress * the tasks progress */ public void setProgress(double statusProgress) { setProgress((int) Math.ceil(statusProgress)); } /** * Sets the time of death of the task now. */ void setTimeOfDeath() { passedAway = System.nanoTime(); } /** * The procedure setWorkDetail() may be used to set some detail information * that dont require translation and are helpful when being shown in the * front end (such as the name of the entity that is currently being * processed by the task). The name detail should be set every time the * progress is determined. You may pass in null to remove the detail. * * <p> * I.e., if your task is about creation of OCR for a process, the detail * here could be the image file being processed right now. * </p> * * @param detail * a work detail, may be null */ public void setWorkDetail(String detail) { this.detail = detail; } /** * Causes this thread to begin execution; the Java Virtual Machine will * create a standalone thread and call the run method of this class. The * result is that two threads are running concurrently: the current thread * which returns from the call to the start method, and the other thread * which executes its run method. In addition, this method override ensures * that the thread is properly registered in the task manager and that its * uncaught exception handler has been properly set. * * @see java.lang.Thread#start() */ @Override public void start() { TaskManager.addTaskIfMissing(this); setUncaughtExceptionHandler(CATCH_ALL); super.start(); } }