package org.camunda.bpm.camel.component.externaltasks;
import static org.camunda.bpm.camel.component.CamundaBpmConstants.EXCHANGE_HEADER_ATTEMPTSSTARTED;
import static org.camunda.bpm.camel.component.CamundaBpmConstants.EXCHANGE_HEADER_RETRIESLEFT;
import static org.camunda.bpm.camel.component.CamundaBpmConstants.EXCHANGE_HEADER_TASK;
import static org.camunda.bpm.camel.component.CamundaBpmConstants.EXCHANGE_HEADER_TASKID;
import static org.camunda.bpm.camel.component.CamundaBpmConstants.EXCHANGE_RESPONSE_IGNORE;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.spi.Synchronization;
import org.camunda.bpm.camel.common.CamundaUtils;
import org.camunda.bpm.camel.component.CamundaBpmEndpoint;
import org.camunda.bpm.engine.ExternalTaskService;
import org.camunda.bpm.engine.externaltask.ExternalTask;
import org.camunda.bpm.engine.externaltask.LockedExternalTask;
public class TaskProcessor implements Processor {
private static final Logger logger = Logger.getLogger(TaskProcessor.class.getCanonicalName());
private final CamundaBpmEndpoint camundaEndpoint;
// parameters
private final int retries;
private final long retryTimeout;
private final long[] retryTimeouts;
private final boolean completeTask;
private final boolean onCompletion;
private final String topic;
private final String workerId;
public TaskProcessor(final CamundaBpmEndpoint endpoint, final String topic, final int retries,
final long retryTimeout, final long[] retryTimeouts, final boolean completeTask, final boolean onCompletion,
final String workerId) {
this.camundaEndpoint = endpoint;
this.retries = retries;
this.retryTimeout = retryTimeout;
this.retryTimeouts = retryTimeouts;
this.completeTask = completeTask;
this.onCompletion = onCompletion;
this.workerId = workerId;
this.topic = topic;
}
private ExternalTaskService getExternalTaskService() {
return camundaEndpoint.getProcessEngine().getExternalTaskService();
}
@Override
public void process(final Exchange exchange) {
if (!onCompletion) {
internalProcessing(exchange);
} else {
// set headers only for "on-completion" processing because
// otherwise the service doing something is already done
// and therefore cannot use this in-headers any more.
setInHeaders(exchange);
final TaskProcessor taskProcessor = this;
exchange.addOnCompletion(new Synchronization() {
@Override
public void onFailure(final Exchange exchange) {
taskProcessor.internalProcessing(exchange);
}
@Override
public void onComplete(final Exchange exchange) {
taskProcessor.internalProcessing(exchange);
}
});
}
}
private void setInHeaders(final Exchange exchange) {
final Message in = getInMessage(exchange);
final String taskId = getExternalTaskId(in);
final ExternalTask task = getExternalTask(taskId);
final SetExternalTaskRetries annotation = findAnnotationByException(exchange.getException());
final int retries = retriesLeft(task.getRetries(), annotation);
final int attemptsStarted = attemptsStarted(task.getRetries(), annotation);
in.setHeader(EXCHANGE_HEADER_RETRIESLEFT, retries);
in.setHeader(EXCHANGE_HEADER_ATTEMPTSSTARTED, attemptsStarted);
}
@SuppressWarnings("unchecked")
void internalProcessing(final Exchange exchange) {
final Message in = getInMessage(exchange);
final String taskId = getExternalTaskId(in);
final ExternalTask task = getExternalTask(taskId);
final ExternalTaskService externalTaskService = getExternalTaskService();
final Message out;
if (onCompletion) {
// 'process-externalTask' at the end of a route does not have any
// output
// - only input which was any proceeder's output.
out = exchange.getOut();
} else {
out = in;
}
// failure
if (exchange.isFailed()) {
if (task == null) {
logger.warning(
"Processing failed but the task seems to be already processed - will do nothing! Camnda external task id: '"
+ taskId + "'");
return;
}
final Exception exception = exchange.getException();
final SetExternalTaskRetries annotation = findAnnotationByException(exchange.getException());
final int retries = retriesLeft(task.getRetries(), annotation);
final long calculatedTimeout = calculateTimeout(task.getRetries(), annotation);
CamundaUtils.retryIfOptimisticLockingException(new Callable<Void>() {
@Override
public Void call() {
externalTaskService.handleFailure(task.getId(),
task.getWorkerId(),
exception != null ? exception.getMessage() : "task failed",
retries,
calculatedTimeout);
return null;
}
});
} else
// bpmn error
if ((out != null) && (out.getBody() != null) && (out.getBody() instanceof String)) {
final String errorCode = out.getBody(String.class);
if (task == null) {
logger.warning("Should complete the external task with BPM error '" + errorCode
+ "' but the task seems to be already processed - will do nothing! Camnda external task id: '"
+ taskId + "'");
return;
}
// Ignore if service advises us to do so.
if (errorCode.equals(EXCHANGE_RESPONSE_IGNORE)) {
return;
}
CamundaUtils.retryIfOptimisticLockingException(new Callable<Void>() {
@Override
public Void call() {
externalTaskService.handleBpmnError(task.getId(), task.getWorkerId(), errorCode);
return null;
}
});
} else
// success
if (completeTask) {
if (task == null) {
logger.warning("Should complete the external task but the task seems to be "
+ "already processed - will do nothing! Camnda external task id: '" + taskId + "'");
return;
}
final Map<String, Object> variablesToBeSet;
if ((out != null) && (out.getBody() != null) && (out.getBody() instanceof Map)) {
variablesToBeSet = out.getBody(Map.class);
} else {
variablesToBeSet = null;
}
CamundaUtils.retryIfOptimisticLockingException(new Callable<Void>() {
@Override
public Void call() {
if (variablesToBeSet != null) {
externalTaskService.complete(task.getId(), task.getWorkerId(), variablesToBeSet);
} else {
externalTaskService.complete(task.getId(), task.getWorkerId());
}
return null;
}
});
}
}
private Message getInMessage(final Exchange exchange) {
final Message in = exchange.getIn();
if (in == null) {
throw new RuntimeCamelException("Unexpected exchange: in is null!");
}
return in;
}
private ExternalTask getExternalTask(final String taskId) {
final ExternalTask task = getExternalTaskService().createExternalTaskQuery().externalTaskId(
taskId).singleResult();
if (task != null) {
if ((task.getWorkerId() != null) && (workerId != null) && !task.getWorkerId().equals(workerId)) {
throw new RuntimeCamelException(
"Unexpected exchange: the external task '" + taskId + "' is locked for worker '"
+ task.getWorkerId() + "' which differs from the configured worker '" + workerId + "!");
}
if ((task.getTopicName() != null) && (topic != null) && !task.getTopicName().equals(topic)) {
throw new RuntimeCamelException(
"Unexpected exchange: the external task '" + taskId + "' is from topic '" + task.getWorkerId()
+ "' which differs from the configured topic '" + topic + "!");
}
} else {
throw new NoSuchExternalTaskException("The referenced external task '" + taskId
+ "' is not available any more. For details see '"
+ "https://github.com/camunda/camunda-bpm-camel#camunda-bpmasync-externaltask-processing-outstanding-external-tasks'.");
}
return task;
}
private String getExternalTaskId(final Message in) {
final LockedExternalTask lockedTask = in.getHeader(EXCHANGE_HEADER_TASK, LockedExternalTask.class);
final String lockedTaskId = in.getHeader(EXCHANGE_HEADER_TASKID, String.class);
final String taskId;
if (lockedTask != null) {
taskId = lockedTask.getId();
} else if (lockedTaskId != null) {
taskId = lockedTaskId;
} else {
throw new RuntimeCamelException("Unexpected exchange: in-header '" + EXCHANGE_HEADER_TASK + "' and '"
+ EXCHANGE_HEADER_TASKID + "' is null!");
}
return taskId;
}
public int retriesLeft(final Integer taskRetries, final SetExternalTaskRetries annotation) {
final int currentRetries;
final boolean decreaseCurrentRetriesByOne;
if (taskRetries == null) {
currentRetries = this.retries;
decreaseCurrentRetriesByOne = false;
} else {
currentRetries = taskRetries;
decreaseCurrentRetriesByOne = true;
}
return findRetriesByAnnotation(annotation, currentRetries, decreaseCurrentRetriesByOne);
}
private SetExternalTaskRetries findAnnotationByException(final Throwable e) {
if (e == null) {
return null;
}
final SetExternalTaskRetries annotation = e.getClass().getAnnotation(SetExternalTaskRetries.class);
if (annotation != null) {
return annotation;
}
return findAnnotationByException(e.getCause());
}
private int findRetriesByAnnotation(final SetExternalTaskRetries annotation, final int currentRetries,
final boolean decreaseCurrentRetriesByOne) {
if (annotation == null) {
if (decreaseCurrentRetriesByOne) {
return currentRetries - 1;
} else {
return currentRetries;
}
}
if (annotation.relative()) {
return currentRetries + annotation.retries();
} else {
return annotation.retries();
}
}
public int attemptsStarted(final Integer taskRetries, final SetExternalTaskRetries annotation) {
final int retriesLeft = retriesLeft(taskRetries, annotation);
return this.retries - retriesLeft;
}
private long calculateTimeout(final Integer taskRetries, final SetExternalTaskRetries annotation) {
final int currentTry = attemptsStarted(taskRetries, annotation);
if (retries < 1) {
return 0;
} else if ((retryTimeouts != null) && (currentTry < retryTimeouts.length)) {
return retryTimeouts[currentTry];
} else {
return retryTimeout;
}
}
}