All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.finra.herd.service.impl.JobDefinitionServiceImpl Maven / Gradle / Ivy

Go to download

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.

There is a newer version: 0.160.0
Show newest version
/*
* 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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy