Coverage Summary for Class: EmptyTask (org.kitodo.production.helper.tasks)
Class |
Method, %
|
Line, %
|
EmptyTask |
64%
(16/25)
|
42,5%
(45/106)
|
EmptyTask$1 |
100%
(1/1)
|
100%
(2/2)
|
EmptyTask$Behaviour |
100%
(1/1)
|
100%
(4/4)
|
Total |
66,7%
(18/27)
|
45,5%
(51/112)
|
/*
* (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.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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 doesn’t do anything sensible yet. It is here to be
* extended.
*/
public class EmptyTask extends Thread {
private static final Logger logger = LogManager.getLogger(EmptyTask.class);
/**
* The enum Actions lists the available instructions to the housekeeper what
* to do with a terminated thread. These are:
*
* <dl>
* <dt>{@code DELETE_IMMEDIATELY}</dt>
* <dd>The thread shall be disposed of as soon as is has gracefully stopped.
* </dd>
* <dt>{@code KEEP_FOR_A_WHILE}</dt>
* <dd>The default behavior: 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}</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>
*/
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 behavior of the
* TaskKeeper towards a task that terminated. The default behavior 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 behavior defines the behavior of the TaskKeeper towards the
* task if it has terminated. Setting this field to DELETE_IMMEDIATELY will
* also result in the desired behavior 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
* constructor—which 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);
}
/**
* Returns the instruction how the TaskSitter
* shall behave towards this task. Usually, the behavior isn’t 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 wasn’t even started. The following instructions are
* available:
*
* <dl>
* <dt>{@code DELETE_IMMEDIATELY}</dt>
* <dd>The thread shall be disposed of as soon as is has gracefully stopped.
* </dd>
* <dt>{@code KEEP_FOR_A_WHILE}</dt>
* <dd>The default behavior: 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}</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.
*/
public String getDisplayName() {
StringBuilder title = new StringBuilder(getClass().getSimpleName());
title.setCharAt(0, (char) (title.charAt(0) | 32));
return Helper.getTranslation(title.toString());
}
/**
* 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 Duration.of(elapsed, ChronoUnit.NANOS);
}
/**
* Provides access to the exception that
* occurred if the thread died abnormally. If no exception has occurred yet
* or it wasn’t properly recorded, null is returned.
*
* @return the exception occurred, or null if no exception occurred yet
*/
public Exception getException() {
return exception;
}
/**
* 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;
}
/**
* 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:
Throwable rootCause = exception;
while (Objects.nonNull(rootCause.getCause())) {
rootCause = rootCause.getCause();
}
StringBuilder stateDescription = new StringBuilder(255);
stateDescription.append(label);
stateDescription.append(" (");
if (Objects.nonNull(detail)) {
stateDescription.append(detail);
stateDescription.append(": ");
}
stateDescription.append(rootCause.getClass().getSimpleName());
if (Objects.nonNull(rootCause.getLocalizedMessage())) {
stateDescription.append(": ");
stateDescription.append(rootCause.getLocalizedMessage());
}
stateDescription.append(')');
return stateDescription.toString();
default:
return label;
}
}
/**
* Returns the task state. It can be one of
* the following:
*
* <dl>
* <dt>{@code CRASHED}</dt>
* <dd>The thread has terminated abnormally. The field “exception” is
* holding the exception that has occurred.</dd>
* <dt>{@code FINISHED}</dt>
* <dd>The thread has finished its work without errors and is available for
* clean-up.</dd>
* <dt>{@code NEW}</dt>
* <dd>The thread has not yet been started.</dd>
* <dt>{@code STOPPED}</dt>
* <dd>The thread was stopped by a front end user—resulting 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}</dt>
* <dd>The thread has received a request to interrupt but did not stop
* yet.</dd>
* <dt>{@code WORKING}</dt>
* <dd>The thread is in operation.</dd>
* </dl>
*
* @return the task state
*/
public 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;
}
}
}
/**
* 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);
}
/**
* Interrupts this thread and allows to set a
* behavior after interruption.
*
* @param mode
* how to behave after interruption
*/
public void interrupt(Behaviour mode) {
behaviour = mode;
interrupt();
}
/**
* 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);
}
/**
* 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);
}
/**
* 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 isn’t
* 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;
}
}
// We’re 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();
}
logger.error(exception.getLocalizedMessage(), exception);
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);
}
}
}
/**
* May be used to set the task’s name along
* with a detail that doesn’t 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.
*
* @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());
}
/**
* May be used to set the task’s 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;
}
/**
* May be used to set the task’s 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();
}
/**
* May be used to set some detail information
* that don’t 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.
*
* @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();
}
}