org.finra.herd.service.activiti.task.BaseJavaDelegate Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of herd-service Show documentation
Show all versions of herd-service Show documentation
This project contains the business service code. This is a classic service tier where business logic is defined along with it's associated
transaction management configuration.
/*
* Copyright 2015 herd contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.finra.herd.service.activiti.task;
import java.util.Collections;
import java.util.stream.Collectors;
import org.activiti.engine.delegate.BpmnError;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.JavaDelegate;
import org.activiti.engine.repository.ProcessDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.finra.herd.core.HerdStringUtils;
import org.finra.herd.dao.JobDefinitionDao;
import org.finra.herd.dao.helper.HerdStringHelper;
import org.finra.herd.dao.helper.JsonHelper;
import org.finra.herd.dao.helper.XmlHelper;
import org.finra.herd.model.ObjectNotFoundException;
import org.finra.herd.model.dto.ApplicationUser;
import org.finra.herd.model.dto.JobDefinitionAlternateKeyDto;
import org.finra.herd.model.dto.SecurityUserWrapper;
import org.finra.herd.model.jpa.JobDefinitionEntity;
import org.finra.herd.service.ActivitiService;
import org.finra.herd.service.activiti.ActivitiHelper;
import org.finra.herd.service.activiti.ActivitiRuntimeHelper;
import org.finra.herd.service.helper.ConfigurationDaoHelper;
import org.finra.herd.service.helper.HerdErrorInformationExceptionHandler;
import org.finra.herd.service.helper.JobDefinitionDaoHelper;
import org.finra.herd.service.helper.JobDefinitionHelper;
import org.finra.herd.service.helper.UserNamespaceAuthorizationHelper;
/**
* This class handles the core flow for our Activiti "JavaDelegate" tasks and calls back sub-classes for the actual task implementation. All of our custom tasks
* should extend this class.
*
* WARNING: When Java Delegates make service calls, those service calls should all take place within a new transaction to ensure Activiti can set workflow
* variables upon errors and have those workflow variables committed to the database. If they don't take place within a new transaction, it is possible that the
* calling code could roll back the entire transaction and the workflow variables wouldn't get updated. Service methods can occur within a new transaction by
* annotating the service method with "@Transactional(propagation = Propagation.REQUIRES_NEW)". Note that JUnit invocations of those same services don't require
* their own transaction since we usually want JUnits to roll back all their data. For those situations, we can provide an alternate service implementation that
* extends the normal service implementation and simply doesn't annotate the service method with the "requires new" annotation.
*/
public abstract class BaseJavaDelegate implements JavaDelegate
{
private static final Logger LOGGER = LoggerFactory.getLogger(BaseJavaDelegate.class);
// MDC property key. It can be referenced in a log4j.xml configuration.
private static final String ACTIVITI_PROCESS_INSTANCE_ID_KEY = "activitiProcessInstanceId";
private static final String USER_ID_KEY = "uid";
private static final String ACTIVITI_LOG_MESSAGE_PREFIX = "HerdTimingLog timingSource=Activiti";
@Autowired
protected ActivitiService activitiService;
@Autowired
protected ConfigurationDaoHelper configurationDaoHelper;
@Autowired
protected HerdStringHelper daoHelper;
@Autowired
protected HerdStringHelper herdStringHelper;
@Autowired
protected JobDefinitionDao jobDefinitionDao;
@Autowired
protected JobDefinitionDaoHelper jobDefinitionDaoHelper;
@Autowired
protected JobDefinitionHelper jobDefinitionHelper;
@Autowired
protected JsonHelper jsonHelper;
@Autowired
protected UserNamespaceAuthorizationHelper userNamespaceAuthorizationHelper;
@Autowired
protected XmlHelper xmlHelper;
/**
* Variable that is set in workflow for the json response.
*/
public static final String VARIABLE_JSON_RESPONSE = "jsonResponse";
// A variable we use to know whether this class (i.e. sub-classes) have had Spring initialized (e.g. auto-wiring) since we need to do it manually
// given that the delegate tasks are created by Activiti as non-Spring beans. The HerdDelegateInterceptor performs the initialization.
private boolean springInitialized;
@Autowired
protected ActivitiHelper activitiHelper;
@Autowired
protected ActivitiRuntimeHelper activitiRuntimeHelper;
@Autowired
@Qualifier("herdErrorInformationExceptionHandler") // This is to ensure we get the base class bean rather than any classes that extend it.
private HerdErrorInformationExceptionHandler errorInformationExceptionHandler;
/**
* The execution implementation. Sub-classes should override this method for their specific task implementation.
*
* @param execution the delegation execution.
*
* @throws Exception when a problem is encountered. A BpmnError should be thrown when there is a problem that should be handled by the workflow. All other
* errors will be considered system errors that will be logged.
*/
public abstract void executeImpl(DelegateExecution execution) throws Exception;
/**
* This is what Activiti will call to execute this task. Sub-classes should override the executeImpl method to supply the actual implementation.
*
* @param execution the execution information.
*
* @throws Exception if any errors were encountered.
*/
@Override
public final void execute(DelegateExecution execution) throws Exception
{
long taskBeginTimeMillis = 0;
boolean taskSuccessFlag = false;
try
{
// Set the task begin time
taskBeginTimeMillis = System.currentTimeMillis();
// Need to clear the security context here since the current thread may have been reused,
// which may might have left over its security context. If we do not clear the security
// context, any subsequent calls may be restricted by the permissions given
// to the previous thread's security context.
SecurityContextHolder.clearContext();
// Check if method is not allowed.
configurationDaoHelper.checkNotAllowedMethod(this.getClass().getCanonicalName());
// Set the security context per last updater of the current process instance's job definition.
ApplicationUser applicationUser = getApplicationUser(execution);
setSecurityContext(applicationUser);
// Set the MDC property for the Activiti process instance ID and user ID.
MDC.put(ACTIVITI_PROCESS_INSTANCE_ID_KEY, "activitiProcessInstanceId=" + execution.getProcessInstanceId());
MDC.put(USER_ID_KEY, "userId=" + (applicationUser.getUserId() == null ? "" : applicationUser.getUserId()));
// Log all input variables from the execution (before the execution starts).
logInputParameters(execution);
// Perform the execution implementation handled in the sub-class.
executeImpl(execution);
// Set a success status as a workflow variable.
activitiRuntimeHelper.setTaskSuccessInWorkflow(execution);
// Set the flag to true since there is no exception thrown
taskSuccessFlag = true;
}
catch (Exception ex)
{
handleException(execution, ex);
}
finally
{
// Log the task execution time
logTaskExecutionTime(taskBeginTimeMillis, taskSuccessFlag);
// Remove the MDC property to ensure they don't accidentally get used by anybody else.
MDC.remove(ACTIVITI_PROCESS_INSTANCE_ID_KEY);
MDC.remove(USER_ID_KEY);
// Clear up the security context.
SecurityContextHolder.clearContext();
}
}
/**
* Logs the Activiti task execution time
*
* @param taskBeginTimeMillis the task begin time in millisecond
* @param taskSuccessFlag the success flag for the task
*/
protected void logTaskExecutionTime(long taskBeginTimeMillis, boolean taskSuccessFlag)
{
StringBuilder message = new StringBuilder();
// Append the log message prefix.
message.append(ACTIVITI_LOG_MESSAGE_PREFIX);
// Append the Activiti task name
message.append(" task=" + this.getClass().getName());
// Append the task success flag
message.append(" success=").append(taskSuccessFlag);
// Append response time
message.append(" responseTimeMillis=").append(System.currentTimeMillis() - taskBeginTimeMillis);
LOGGER.info(message.toString());
}
/**
* Sets the security context per last updater of the current process instance's job definition.
*
* @param applicationUser the application user
*/
protected void setSecurityContext(ApplicationUser applicationUser)
{
userNamespaceAuthorizationHelper.buildNamespaceAuthorizations(applicationUser);
SecurityContextHolder.getContext().setAuthentication(new PreAuthenticatedAuthenticationToken(
new SecurityUserWrapper(applicationUser.getUserId(), "", true, true, true, true, Collections.emptyList(), applicationUser), null));
}
/**
* Retrieves application user per last updater of the current process instance's job definition.
*
* @param execution the delegate execution
*
* @return the application user
*/
protected ApplicationUser getApplicationUser(DelegateExecution execution)
{
String processDefinitionId = execution.getProcessDefinitionId();
// Get process definition by process definition ID from Activiti.
ProcessDefinition processDefinition = activitiService.getProcessDefinitionById(processDefinitionId);
// Validate that we retrieved the process definition from Activiti.
if (processDefinition == null)
{
throw new ObjectNotFoundException(String.format("Failed to find Activiti process definition for processDefinitionId=\"%s\".", processDefinitionId));
}
// Retrieve the process definition key.
String processDefinitionKey = processDefinition.getKey();
// Get the job definition key.
JobDefinitionAlternateKeyDto jobDefinitionKey = jobDefinitionHelper.getJobDefinitionKey(processDefinitionKey);
// Get the job definition from the Herd repository and validate that it exists.
JobDefinitionEntity jobDefinitionEntity = jobDefinitionDaoHelper.getJobDefinitionEntity(jobDefinitionKey.getNamespace(), jobDefinitionKey.getJobName());
// Set the security context per last updater of the job definition.
String updatedByUserId = jobDefinitionEntity.getUpdatedBy();
ApplicationUser applicationUser = new ApplicationUser(getClass());
applicationUser.setUserId(updatedByUserId);
return applicationUser;
}
/**
* Handles any exception thrown by an Activiti task.
*
* @param execution The execution which identifies the task.
* @param exception The exception that has been thrown
*
* @throws Exception Some exceptions may choose to bubble up the exception
*/
protected void handleException(DelegateExecution execution, Exception exception) throws Exception
{
// Set the error status and stack trace as workflow variables.
activitiRuntimeHelper.setTaskErrorInWorkflow(execution, exception.getMessage(), exception);
// Continue throwing the original exception and let workflow handle it with a Boundary event handler.
if (exception instanceof BpmnError)
{
throw exception;
}
// Log the error if the exception should be reported.
if (errorInformationExceptionHandler.isReportableError(exception))
{
LOGGER.error("{} Unexpected error occurred during task. activitiTaskName=\"{}\"", activitiHelper.getProcessIdentifyingInformation(execution),
getClass().getSimpleName(), exception);
}
}
/**
* Sets a JSON response object as a workflow variable.
*
* @param responseObject the JSON object.
* @param execution the delegate execution.
*
* @throws Exception if any problems were encountered.
*/
public void setJsonResponseAsWorkflowVariable(Object responseObject, DelegateExecution execution) throws Exception
{
String jsonResponse = jsonHelper.objectToJson(responseObject);
setTaskWorkflowVariable(execution, VARIABLE_JSON_RESPONSE, jsonResponse);
}
public void setJsonResponseAsWorkflowVariable(Object responseObject, String executionId, String activitiId) throws Exception
{
String jsonResponse = jsonHelper.objectToJson(responseObject);
setTaskWorkflowVariable(executionId, activitiId, VARIABLE_JSON_RESPONSE, jsonResponse);
}
public boolean isSpringInitialized()
{
return springInitialized;
}
public void setSpringInitialized(boolean springInitialized)
{
this.springInitialized = springInitialized;
}
/**
* Sets the workflow variable with task id prefixed.
*
* @param execution the delegate execution.
* @param variableName the variable name
* @param variableValue the variable value
*/
protected void setTaskWorkflowVariable(DelegateExecution execution, String variableName, Object variableValue)
{
activitiRuntimeHelper.setTaskWorkflowVariable(execution, variableName, variableValue);
}
protected void setTaskWorkflowVariable(String executionId, String activitiId, String variableName, Object variableValue)
{
activitiRuntimeHelper.setTaskWorkflowVariable(executionId, activitiId, variableName, variableValue);
}
/**
* Converts the request string to xsd object.
*
* @param contentType the content type "xml" or "json"
* @param requestString the request string
* @param xsdClass the xsd class of the object to convert to
* @param the type of the returned object.
*
* @return the request object.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
protected T getRequestObject(String contentType, String requestString, Class xsdClass)
{
T request;
if (contentType.equalsIgnoreCase("xml"))
{
try
{
request = (T) xmlHelper.unmarshallXmlToObject(xsdClass, requestString);
}
catch (Exception ex)
{
throw new IllegalArgumentException("\"" + xsdClass.getSimpleName() + "\" must be valid xml string.", ex);
}
}
else if (contentType.equalsIgnoreCase("json"))
{
try
{
request = (T) jsonHelper.unmarshallJsonToObject(xsdClass, requestString);
}
catch (Exception ex)
{
throw new IllegalArgumentException("\"" + xsdClass.getSimpleName() + "\" must be valid json string.", ex);
}
}
else
{
throw new IllegalArgumentException("\"ContentType\" must be a valid value of either \"xml\" or \"json\".");
}
return request;
}
/**
* Loops through all process variables and logs them.
*
* @param execution the execution information
*/
protected void logInputParameters(DelegateExecution execution)
{
String loggingText = execution.getVariables().entrySet().stream().map(entry -> entry.getKey() + "=" + jsonHelper.objectToJson(entry.getValue()))
.collect(Collectors.joining(" "));
LOGGER.info("{} Input parameters for {}: {}", activitiHelper.getProcessIdentifyingInformation(execution), this.getClass().getName(),
HerdStringUtils.sanitizeLogText(loggingText));
}
}