org.finra.herd.service.impl.JobDefinitionServiceImpl 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.impl;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.activiti.bpmn.model.Activity;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.Process;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.bpmn.model.StartEvent;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.validation.ValidationError;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.JobDefinitionDao;
import org.finra.herd.dao.config.DaoSpringModuleConfig;
import org.finra.herd.model.AlreadyExistsException;
import org.finra.herd.model.annotation.NamespacePermission;
import org.finra.herd.model.api.xml.JobDefinition;
import org.finra.herd.model.api.xml.JobDefinitionCreateRequest;
import org.finra.herd.model.api.xml.JobDefinitionUpdateRequest;
import org.finra.herd.model.api.xml.NamespacePermissionEnum;
import org.finra.herd.model.api.xml.Parameter;
import org.finra.herd.model.api.xml.S3PropertiesLocation;
import org.finra.herd.model.dto.ConfigurationValue;
import org.finra.herd.model.jpa.JobDefinitionEntity;
import org.finra.herd.model.jpa.JobDefinitionParameterEntity;
import org.finra.herd.model.jpa.NamespaceEntity;
import org.finra.herd.service.JobDefinitionService;
import org.finra.herd.service.activiti.ActivitiHelper;
import org.finra.herd.service.helper.AlternateKeyHelper;
import org.finra.herd.service.helper.JobDefinitionDaoHelper;
import org.finra.herd.service.helper.JobDefinitionHelper;
import org.finra.herd.service.helper.NamespaceDaoHelper;
import org.finra.herd.service.helper.S3PropertiesLocationHelper;
/**
* The job definition service implementation.
*/
@Service
@Transactional(value = DaoSpringModuleConfig.HERD_TRANSACTION_MANAGER_BEAN_NAME)
public class JobDefinitionServiceImpl implements JobDefinitionService
{
// A deployable Activiti XML resource should have ".bpmn20.xml" extension as per Activiti user guide.
private static final String ACTIVITI_DEPLOY_XML_SUFFIX = "bpmn20.xml";
@Autowired
private ActivitiHelper activitiHelper;
@Autowired
private AlternateKeyHelper alternateKeyHelper;
@Autowired
private ConfigurationHelper configurationHelper;
@Autowired
private JobDefinitionDao jobDefinitionDao;
@Autowired
private JobDefinitionDaoHelper jobDefinitionDaoHelper;
@Autowired
private JobDefinitionHelper jobDefinitionHelper;
@Autowired
private NamespaceDaoHelper namespaceDaoHelper;
@Autowired
private RepositoryService activitiRepositoryService;
@Autowired
private S3PropertiesLocationHelper s3PropertiesLocationHelper;
/**
* Creates a new business object definition.
*
* @param request the business object definition create request.
* @param enforceAsync True to enforce first task is async, false to ignore
*
* @return the created business object definition.
*/
@NamespacePermission(fields = "#request.namespace", permissions = NamespacePermissionEnum.WRITE)
@Override
public JobDefinition createJobDefinition(JobDefinitionCreateRequest request, boolean enforceAsync) throws Exception
{
// Perform the validation.
validateJobDefinitionCreateRequest(request);
if (enforceAsync)
{
assertFirstTaskIsAsync(activitiHelper.constructBpmnModelFromXmlAndValidate(request.getActivitiJobXml()));
}
// Get the namespace and ensure it exists.
NamespaceEntity namespaceEntity = namespaceDaoHelper.getNamespaceEntity(request.getNamespace());
// Ensure a job definition with the specified name doesn't already exist.
JobDefinitionEntity jobDefinitionEntity = jobDefinitionDao.getJobDefinitionByAltKey(request.getNamespace(), request.getJobName());
if (jobDefinitionEntity != null)
{
throw new AlreadyExistsException(
"Unable to create job definition with name \"" + request.getJobName() + "\" because it already exists for namespace \"" +
request.getNamespace() + "\".");
}
// Create the new process definition.
ProcessDefinition processDefinition = createProcessDefinition(request.getNamespace(), request.getJobName(), request.getActivitiJobXml());
// Create a job definition entity from the request information.
jobDefinitionEntity =
createOrUpdateJobDefinitionEntity(null, namespaceEntity, request.getJobName(), request.getDescription(), processDefinition.getId(),
request.getParameters(), request.getS3PropertiesLocation());
// Persist the new entity.
jobDefinitionEntity = jobDefinitionDao.saveAndRefresh(jobDefinitionEntity);
// Create and return the job definition from the persisted entity.
return createJobDefinitionFromEntity(jobDefinitionEntity);
}
@NamespacePermission(fields = "#namespace", permissions = NamespacePermissionEnum.READ)
@Override
public JobDefinition getJobDefinition(String namespace, String jobName) throws Exception
{
// Validate the job definition alternate key.
String namespaceLocal = alternateKeyHelper.validateStringParameter("namespace", namespace);
String jobNameLocal = alternateKeyHelper.validateStringParameter("job name", jobName);
// Retrieve and ensure that a job definition exists.
JobDefinitionEntity jobDefinitionEntity = jobDefinitionDaoHelper.getJobDefinitionEntity(namespaceLocal, jobNameLocal);
// Create and return the job definition object from the persisted entity.
return createJobDefinitionFromEntity(jobDefinitionEntity);
}
@NamespacePermission(fields = "#namespace", permissions = NamespacePermissionEnum.WRITE)
@Override
public JobDefinition updateJobDefinition(String namespace, String jobName, JobDefinitionUpdateRequest request, boolean enforceAsync) throws Exception
{
// Validate the job definition alternate key.
String namespaceLocal = alternateKeyHelper.validateStringParameter("namespace", namespace);
String jobNameLocal = alternateKeyHelper.validateStringParameter("job name", jobName);
// Validate and trim the Activiti job XML.
Assert.hasText(request.getActivitiJobXml(), "An Activiti job XML must be specified.");
request.setActivitiJobXml(request.getActivitiJobXml().trim());
// Perform the job definition validation.
validateJobDefinition(namespaceLocal, jobNameLocal, request.getActivitiJobXml(), request.getParameters(), request.getS3PropertiesLocation());
if (enforceAsync)
{
assertFirstTaskIsAsync(activitiHelper.constructBpmnModelFromXmlAndValidate(request.getActivitiJobXml()));
}
// Get the namespace and ensure it exists.
NamespaceEntity namespaceEntity = namespaceDaoHelper.getNamespaceEntity(namespaceLocal);
// Retrieve and ensure that a job definition exists.
JobDefinitionEntity jobDefinitionEntity = jobDefinitionDaoHelper.getJobDefinitionEntity(namespaceLocal, jobNameLocal);
// Create the new process definition.
ProcessDefinition processDefinition = createProcessDefinition(namespaceLocal, jobNameLocal, request.getActivitiJobXml());
// Create a job definition entity from the request information.
jobDefinitionEntity =
createOrUpdateJobDefinitionEntity(jobDefinitionEntity, namespaceEntity, jobNameLocal, request.getDescription(), processDefinition.getId(),
request.getParameters(), request.getS3PropertiesLocation());
// Persist the entity.
jobDefinitionEntity = jobDefinitionDao.saveAndRefresh(jobDefinitionEntity);
// Create and return the job definition object from the persisted entity.
return createJobDefinitionFromEntity(jobDefinitionEntity);
}
/**
* Validates the job definition create request. This method also trims request parameters.
*
* @param request the request.
*
* @throws IllegalArgumentException if any validation errors were found.
*/
private void validateJobDefinitionCreateRequest(JobDefinitionCreateRequest request)
{
// Validate the job definition alternate key.
request.setNamespace(alternateKeyHelper.validateStringParameter("namespace", request.getNamespace()));
request.setJobName(alternateKeyHelper.validateStringParameter("job name", request.getJobName()));
// Validate and trim the Activiti job XML.
Assert.hasText(request.getActivitiJobXml(), "An Activiti job XML must be specified.");
request.setActivitiJobXml(request.getActivitiJobXml().trim());
// Perform the job definition validation.
validateJobDefinition(request.getNamespace(), request.getJobName(), request.getActivitiJobXml(), request.getParameters(),
request.getS3PropertiesLocation());
}
/**
* Validates the job definition create request. This method also trims request parameters.
*
* @param namespace the namespace.
* @param jobName the job name.
* @param activitiJobXml the Activiti Job XML
* @param parameters the list parameters.
* @param s3PropertiesLocation {@link S3PropertiesLocation}
*
* @throws IllegalArgumentException if any validation errors were found.
*/
private void validateJobDefinition(String namespace, String jobName, String activitiJobXml, List parameters,
S3PropertiesLocation s3PropertiesLocation)
{
// Ensure the Activiti XML doesn't contain a CDATA wrapper.
Assert.isTrue(!activitiJobXml.contains(" activitiModelErrors = activitiRepositoryService.validateProcess(bpmnModel);
StringBuilder validationErrors = new StringBuilder();
for (ValidationError validationError : activitiModelErrors)
{
validationErrors.append('\n').append(validationError.getDefaultDescription());
}
Assert.isTrue(activitiModelErrors.isEmpty(), "Activiti XML is not valid, Errors: " + validationErrors);
// Validate that parameter names are there and not duplicate.
Map parameterNameMap = new HashMap<>();
if (!CollectionUtils.isEmpty(parameters))
{
for (Parameter parameter : parameters)
{
Assert.hasText(parameter.getName(), "A parameter name must be specified.");
parameter.setName(parameter.getName().trim());
// Ensure the parameter key isn't a duplicate by using a map with a "lowercase" name as the key for case insensitivity.
String validationMapKey = parameter.getName().toLowerCase();
Assert.isTrue(!parameterNameMap.containsKey(validationMapKey), "Duplicate parameter name found: " + parameter.getName());
parameterNameMap.put(validationMapKey, validationMapKey);
}
}
if (s3PropertiesLocation != null)
{
s3PropertiesLocationHelper.validate(s3PropertiesLocation);
}
}
/**
* Deploys the Activiti XML into Activiti given the specified namespace and job name. If an existing process definition with the specified namespace and job
* name already exists, a new process definition with an incremented version will be created by Activiti.
*
* @param namespace the namespace.
* @param jobName the job name.
* @param activitiJobXml the Activiti job XML.
*
* @return the newly created process definition.
*/
private ProcessDefinition createProcessDefinition(String namespace, String jobName, String activitiJobXml)
{
// Deploy Activiti XML using Activiti API.
String activitiIdString = jobDefinitionHelper.buildActivitiIdString(namespace, jobName);
Deployment deployment =
activitiRepositoryService.createDeployment().name(activitiIdString).addString(activitiIdString + ACTIVITI_DEPLOY_XML_SUFFIX, activitiJobXml)
.deploy();
// Read the created process definition.
return activitiRepositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).list().get(0);
}
/**
* Asserts that the first asyncable task in the given model is indeed asynchronous. Only asserts when the configuration is set to true.
*
* @param bpmnModel The BPMN model
*/
private void assertFirstTaskIsAsync(BpmnModel bpmnModel)
{
if (Boolean.TRUE.equals(configurationHelper.getProperty(ConfigurationValue.ACTIVITI_JOB_DEFINITION_ASSERT_ASYNC, Boolean.class)))
{
Process process = bpmnModel.getMainProcess();
for (StartEvent startEvent : process.findFlowElementsOfType(StartEvent.class))
{
for (SequenceFlow sequenceFlow : startEvent.getOutgoingFlows())
{
String targetRef = sequenceFlow.getTargetRef();
FlowElement targetFlowElement = process.getFlowElement(targetRef);
if (targetFlowElement instanceof Activity)
{
Assert.isTrue(((Activity) targetFlowElement).isAsynchronous(), "Element with id \"" + targetRef +
"\" must be set to activiti:async=true. All tasks which start the workflow must be asynchronous to prevent certain undesired " +
"transactional behavior, such as records of workflow not being saved on errors. Please refer to Activiti and herd documentations " +
"for details.");
}
}
}
}
}
/**
* Creates a new job definition entity from the request information.
*
* @param jobDefinitionEntity an optional existing job definition entity to update. If null, then a new one will be created.
* @param namespaceEntity the namespace entity.
* @param jobName the job name.
* @param description the job definition description.
* @param activitiId the Activiti Id.
* @param parameters the job definition parameters.
*
* @return the newly created or existing updated job definition entity.
*/
private JobDefinitionEntity createOrUpdateJobDefinitionEntity(JobDefinitionEntity jobDefinitionEntity, NamespaceEntity namespaceEntity, String jobName,
String description, String activitiId, List parameters, S3PropertiesLocation s3PropertiesLocation)
{
JobDefinitionEntity jobDefinitionEntityLocal = jobDefinitionEntity;
// If a job definition entity doesn't yet exist, create a new one.
if (jobDefinitionEntityLocal == null)
{
jobDefinitionEntityLocal = new JobDefinitionEntity();
}
// Create a new entity.
jobDefinitionEntityLocal.setName(jobName);
jobDefinitionEntityLocal.setNamespace(namespaceEntity);
jobDefinitionEntityLocal.setDescription(description);
jobDefinitionEntityLocal.setActivitiId(activitiId);
// Set or clear S3 properties location
String bucketName = null;
String key = null;
if (s3PropertiesLocation != null)
{
bucketName = s3PropertiesLocation.getBucketName();
key = s3PropertiesLocation.getKey();
}
jobDefinitionEntityLocal.setS3BucketName(bucketName);
jobDefinitionEntityLocal.setS3ObjectKey(key);
// Create the parameters.
List parameterEntities = new ArrayList<>();
// As per generated JobDefinitionCreateRequest class, getParameters() never returns null.
if (!CollectionUtils.isEmpty(parameters))
{
for (Parameter parameter : parameters)
{
JobDefinitionParameterEntity parameterEntity = new JobDefinitionParameterEntity();
parameterEntities.add(parameterEntity);
parameterEntity.setName(parameter.getName());
parameterEntity.setValue(parameter.getValue());
}
}
// Set the new list of parameters on the entity.
jobDefinitionEntityLocal.setParameters(parameterEntities);
return jobDefinitionEntityLocal;
}
/**
* Creates the job definition from the persisted entity.
*
* @param jobDefinitionEntity the newly persisted job definition entity.
*
* @return the job definition.
*/
private JobDefinition createJobDefinitionFromEntity(JobDefinitionEntity jobDefinitionEntity) throws IOException
{
// Create the business object definition information.
JobDefinition jobDefinition = new JobDefinition();
jobDefinition.setId(jobDefinitionEntity.getId());
jobDefinition.setNamespace(jobDefinitionEntity.getNamespace().getCode());
jobDefinition.setJobName(jobDefinitionEntity.getName());
jobDefinition.setDescription(jobDefinitionEntity.getDescription());
String s3BucketName = jobDefinitionEntity.getS3BucketName();
String s3ObjectKey = jobDefinitionEntity.getS3ObjectKey();
if (s3BucketName != null && s3ObjectKey != null)
{
S3PropertiesLocation s3PropertiesLocation = new S3PropertiesLocation();
s3PropertiesLocation.setBucketName(s3BucketName);
s3PropertiesLocation.setKey(s3ObjectKey);
jobDefinition.setS3PropertiesLocation(s3PropertiesLocation);
}
// Retrieve Activiti XML from activiti.
ProcessDefinition processDefinition =
activitiRepositoryService.createProcessDefinitionQuery().processDefinitionId(jobDefinitionEntity.getActivitiId()).singleResult();
InputStream xmlStream = activitiRepositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getResourceName());
jobDefinition.setActivitiJobXml(IOUtils.toString(xmlStream));
// Add in the parameters.
List parameters = new ArrayList<>();
jobDefinition.setParameters(parameters);
for (JobDefinitionParameterEntity parameterEntity : jobDefinitionEntity.getParameters())
{
Parameter parameter = new Parameter(parameterEntity.getName(), parameterEntity.getValue());
jobDefinitionHelper.maskPassword(parameter);
parameters.add(parameter);
}
// Populate the "last updated by" user ID.
jobDefinition.setLastUpdatedByUserId(jobDefinitionEntity.getUpdatedBy());
return jobDefinition;
}
}