org.activiti.kickstart.service.alfresco.AlfrescoKickstartServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.activiti.kickstart.service.alfresco.AlfrescoKickstartServiceImpl.java

Source

/* 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.activiti.kickstart.service.alfresco;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.activiti.kickstart.diagram.ProcessDiagramGenerator;
import org.activiti.kickstart.dto.KickstartFormProperty;
import org.activiti.kickstart.dto.KickstartTask;
import org.activiti.kickstart.dto.KickstartUserTask;
import org.activiti.kickstart.dto.KickstartWorkflow;
import org.activiti.kickstart.dto.KickstartWorkflowInfo;
import org.activiti.kickstart.service.Bpmn20MarshallingService;
import org.activiti.kickstart.service.KickstartService;
import org.activiti.kickstart.service.MetaDataKeys;
import org.apache.chemistry.opencmis.client.api.Document;
import org.apache.chemistry.opencmis.client.api.Folder;
import org.apache.chemistry.opencmis.client.api.ItemIterable;
import org.apache.chemistry.opencmis.client.api.QueryResult;
import org.apache.chemistry.opencmis.client.api.Repository;
import org.apache.chemistry.opencmis.client.api.Session;
import org.apache.chemistry.opencmis.client.api.SessionFactory;
import org.apache.chemistry.opencmis.client.runtime.SessionFactoryImpl;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.SessionParameter;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.enums.BindingType;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.io.IOUtils;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;

/**
 * @author Joram Barrez
 */
public class AlfrescoKickstartServiceImpl implements KickstartService {

    private static final Logger LOGGER = Logger.getLogger(AlfrescoKickstartServiceImpl.class.getName());

    // Constants
    // /////////////////////////////////////////////////////////////////////

    private static final String KICKSTART_PREFIX = "ks:";

    // URLS
    // ////////////////////////////////////////////

    private static final String ALFRESCO_BASE_URL = "http://localhost:8080/alfresco/service/";

    private static final String SHARE_BASE_URL = "http://localhost:8081/share/";

    // Alfresco specific folders and urls
    // //////////////////////////////////////////

    private static final String WORKFLOW_DEFINITION_FOLDER = "/Data Dictionary/Workflow Definitions";

    private static final String DATA_DICTIONARY_FOLDER = "/Data Dictionary/Models";

    private static final String FORM_CONFIG_UPLOAD_URL = SHARE_BASE_URL + "page/modules/module";

    // Task Model templates
    // /////////////////////////////////////////////////////////

    private static final String TEMPLATE_FOLDER = "/org/activiti/kickstart/service/alfresco/";

    private static final String TASK_MODEL_TEMPLATE_FILE = TEMPLATE_FOLDER + "task-model-template.xml";
    private static String TASK_MODEL_TEMPLATE;

    private static final String TASK_MODEL_TYPE_TEMPLATE_FILE = TEMPLATE_FOLDER + "task-model-type-template.xml";
    private static String TASK_MODEL_TYPE_TEMPLATE;

    private static final String TASK_MODEL_PROPERTY_TEMPLATE_FILE = TEMPLATE_FOLDER
            + "task-model-property-template.xml";
    private static String TASK_MODEL_PROPERTY_TEMPLATE;

    // Form Config templates
    // /////////////////////////////////////////////////////////

    private static final String FORM_CONFIG_TEMPLATE_FILE = TEMPLATE_FOLDER + "form-config-template.xml";
    private static String FORM_CONFIG_TEMPLATE;

    private static final String FORM_CONFIG_EVALUATOR_CONFIG_TEMPLATE_FILE = TEMPLATE_FOLDER
            + "form-config-evaluator-config-template.xml";
    private static String FORM_CONFIG_EVALUATOR_CONFIG_TEMPLATE;

    private static final String FORM_CONFIG_FIELD_TEMPLATE_FILE = TEMPLATE_FOLDER
            + "form-config-field-template.xml";
    private static String FORM_CONFIG_FIELD_TEMPLATE;

    private static final String FORM_CONFIG_FIELD_VISIBILITY_TEMPLATE_FILE = TEMPLATE_FOLDER
            + "form-config-field-visibility-template.xml";
    private static String FORM_CONFIG_FIELD_VISIBILITY_TEMPLATE;

    private static final String FORM_CONFIG_FIELD_INFO_TEMPLATE_FILE = TEMPLATE_FOLDER
            + "form-config-field-info-control-template.xml";
    private static String FORM_CONFIG_FIELD_INFO_TEMPLATE;

    // Service parameters
    // ///////////////////////////////////////////////////////////

    protected String cmisUser;
    protected String cmisPassword;
    protected String cmisAtompubUrl;

    // Service members
    // /////////////////////////////////////////////////////////////

    protected Session cachedSession;
    protected Bpmn20MarshallingService marshallingService;

    public AlfrescoKickstartServiceImpl(String cmisUser, String cmisPassword, String cmisAtompubUrl) {
        this.cmisUser = cmisUser;
        this.cmisPassword = cmisPassword;
        this.cmisAtompubUrl = cmisAtompubUrl;
    }

    protected Session getCmisSession() {
        if (cachedSession == null) {
            synchronized (this) {
                if (cachedSession == null) {
                    Map<String, String> parameters = new HashMap<String, String>();
                    parameters.put(SessionParameter.USER, cmisUser);
                    parameters.put(SessionParameter.PASSWORD, cmisPassword);
                    parameters.put(SessionParameter.ATOMPUB_URL, cmisAtompubUrl);
                    parameters.put(SessionParameter.BINDING_TYPE, BindingType.ATOMPUB.value());

                    // We're using the Alfresco extensions
                    parameters.put(SessionParameter.OBJECT_FACTORY_CLASS,
                            "org.alfresco.cmis.client.impl.AlfrescoObjectFactoryImpl");

                    // First need to fetch the repository info to know the repo id
                    SessionFactory sessionFactory = SessionFactoryImpl.newInstance();
                    List<Repository> repositories = sessionFactory.getRepositories(parameters);
                    String repositoryId = repositories.get(0).getId();
                    parameters.put(SessionParameter.REPOSITORY_ID, repositoryId);

                    cachedSession = sessionFactory.createSession(parameters);
                }
            }
        }
        return cachedSession;
    }

    public String deployWorkflow(KickstartWorkflow kickstartWorkflow, Map<String, String> metadata) {

        // Validate metadata
        String jsonSource = metadata.get(MetaDataKeys.WORKFLOW_JSON_SOURCE);
        if (jsonSource == null) {
            throw new RuntimeException("Missing metadata " + MetaDataKeys.WORKFLOW_JSON_SOURCE);
        }

        // Create base name for all files that will be stored
        String baseName = kickstartWorkflow.getId();

        // Following stringbuilders will construct a valid content model and form config
        StringBuilder taskModelsString = new StringBuilder();
        StringBuilder evaluatorConfigStringBuilder = new StringBuilder();

        // Mapping to store {formProperty} -> {form property with prefix and unique-ified name}
        HashMap<String, String> formPropertyMapping = new HashMap<String, String>();

        // Needs to go first, as the formkey will be filled in here
        for (KickstartTask task : kickstartWorkflow.getTasks()) {
            if (task instanceof KickstartUserTask) { // Only need to generte a form for user tasks
                generateTaskAndFormConfigForUserTask((KickstartUserTask) task, taskModelsString,
                        evaluatorConfigStringBuilder, baseName, formPropertyMapping);
            }
        }

        // Upload results to Alfresco
        uploadTaskModel(taskModelsString, baseName);
        uploadFormConfig(evaluatorConfigStringBuilder, kickstartWorkflow, baseName);

        // Upload process
        deployProcess(kickstartWorkflow, baseName, jsonSource); // Can't get the deployment id, so returning process definition id
        return baseName;
    }

    protected void deployProcess(KickstartWorkflow kickstartWorkflow, String baseFileName, String jsonSource) {
        Session cmisSession = getCmisSession();
        Folder workflowDefinitionFolder = (Folder) cmisSession.getObjectByPath(WORKFLOW_DEFINITION_FOLDER);

        uploadDiagramFile(kickstartWorkflow, baseFileName, workflowDefinitionFolder); // Process image (must go first, since it will add DI to the process xml)
        uploadJsonFile(baseFileName, jsonSource, workflowDefinitionFolder);
        uploadProcessFile(kickstartWorkflow, baseFileName, workflowDefinitionFolder);
    }

    private void uploadDiagramFile(KickstartWorkflow kickstartWorkflow, String baseFileName,
            Folder workflowDefinitionFolder) {
        LOGGER.info("Generating process image...");
        ProcessDiagramGenerator diagramGenerator = new ProcessDiagramGenerator(kickstartWorkflow,
                marshallingService);
        InputStream diagramInputStream = diagramGenerator.execute();

        // Diagram is deployed next to the process xml
        if (workflowDefinitionFolder == null) {
            throw new RuntimeException(
                    "Cannot find workflow definition folder '" + WORKFLOW_DEFINITION_FOLDER + "'");
        }

        String diagramFileName = baseFileName + ".png";
        ContentStream diagramContentStream = new ContentStreamImpl(diagramFileName, null, "image/png",
                diagramInputStream);
        Document diagramDocument = getDocumentFromFolder(workflowDefinitionFolder.getPath(), diagramFileName);
        if (diagramDocument == null) {

            HashMap<String, Object> diagramProperties = new HashMap<String, Object>();
            diagramProperties.put(PropertyIds.NAME, diagramFileName);
            diagramProperties.put(PropertyIds.OBJECT_TYPE_ID, "cmis:document");

            workflowDefinitionFolder.createDocument(diagramProperties, diagramContentStream, VersioningState.MAJOR);

        } else {

            diagramDocument.setContentStream(diagramContentStream, true);

        }
    }

    private void uploadJsonFile(String baseFileName, String jsonSource, Folder workflowDefinitionFolder) {
        LOGGER.info("Upload json source...");
        String jsonSrcFileName = baseFileName + ".json";
        ContentStream jsonSrcContentStream = new ContentStreamImpl(jsonSrcFileName, null, "application/json",
                new ByteArrayInputStream(jsonSource.getBytes()));
        Document jsonDocument = getDocumentFromFolder(workflowDefinitionFolder.getPath(), jsonSrcFileName);
        if (jsonDocument == null) {

            HashMap<String, Object> jsonSrcProperties = new HashMap<String, Object>();
            jsonSrcProperties.put(PropertyIds.NAME, jsonSrcFileName);
            jsonSrcProperties.put(PropertyIds.OBJECT_TYPE_ID, "cmis:document");

            workflowDefinitionFolder.createDocument(jsonSrcProperties, jsonSrcContentStream, VersioningState.MAJOR);

        } else {

            jsonDocument.setContentStream(jsonSrcContentStream, true);

        }
    }

    private void uploadProcessFile(KickstartWorkflow kickstartWorkflow, String baseFileName,
            Folder workflowDefinitionFolder) {
        String processFileName = baseFileName + ".bpmn20.xml";

        String workflowXML = marshallingService.marshallWorkflow(kickstartWorkflow);

        InputStream inputStream = new ByteArrayInputStream(workflowXML.getBytes());
        LOGGER.info("Uploading process definition xml...");
        prettyLogXml(workflowXML);
        ContentStream processContentStream = new ContentStreamImpl(processFileName, null, "application/xml",
                inputStream);

        Document processDocument = getDocumentFromFolder(workflowDefinitionFolder.getPath(), processFileName);
        if (processDocument == null) {

            HashMap<String, Object> properties = new HashMap<String, Object>();
            properties.put("cmis:name", processFileName);
            properties.put("cmis:objectTypeId", "D:bpm:workflowDefinition,P:cm:titled"); // Important! Process won't be deployed otherwise
            properties.put("bpm:definitionDeployed", true);
            properties.put("bpm:engineId", "activiti"); // Also vital for correct deployment!
            properties.put("cm:description", kickstartWorkflow.getName());

            processDocument = workflowDefinitionFolder.createDocument(properties, processContentStream,
                    VersioningState.MAJOR);

        } else {

            processDocument.setContentStream(processContentStream, true);

        }

        LOGGER.info("Process definition uploaded to '" + processDocument.getPaths() + "'");
    }

    protected String processNameToBaseName(String processName) {
        return processName.replace(".bpmn20.xml", "");
    }

    protected void generateTaskAndFormConfigForUserTask(KickstartUserTask userTask, StringBuilder taskModelsString,
            StringBuilder formConfigString, String baseName, HashMap<String, String> formPropertyMapping) {

        if (userTask.getForm() != null) {

            String uniqueTaskName = baseName + "_" + userTask.getName().toLowerCase().replace(" ", "_");
            String prefixedUniqueTaskName = KICKSTART_PREFIX + uniqueTaskName;
            userTask.getForm().setFormKey(prefixedUniqueTaskName);

            StringBuilder typeString = new StringBuilder();
            StringBuilder formAppearanceString = new StringBuilder();
            StringBuilder formVisibilityString = new StringBuilder();

            String descriptionPropertyName = KICKSTART_PREFIX + "description_" + uniqueTaskName;
            if (userTask.getDescription() != null) {
                formVisibilityString.append(
                        MessageFormat.format(getFormConfigFieldVisibilityTemplate(), descriptionPropertyName));
                formAppearanceString.append(
                        MessageFormat.format(getFormConfigInfoTemplate(), descriptionPropertyName, "Description"));
            }

            if (userTask.getForm().getFormProperties() != null
                    && userTask.getForm().getFormProperties().size() > 0) {

                // Get form-propertes
                for (KickstartFormProperty formProperty : userTask.getForm().getFormProperties()) {

                    String uniquePropertyName = KICKSTART_PREFIX + baseName + "_"
                            + createFriendlyName(formProperty.getProperty());
                    formPropertyMapping.put(formProperty.getProperty(), uniquePropertyName);

                    if (formProperty.getType().equals("documents")) {

                        // Package items are part of the parent content model task, 
                        // hence we do not need to add it to the task model
                        formVisibilityString.append(
                                MessageFormat.format(getFormConfigFieldVisibilityTemplate(), "packageItems"));

                        formAppearanceString.append(MessageFormat.format(getFormConfigFieldTemplate(),
                                "packageItems", formProperty.getProperty()));

                    } else {

                        // Property in type-definition
                        typeString.append(MessageFormat.format(getTaskModelPropertyTemplate(), uniquePropertyName,
                                getAlfrescoModelType(formProperty.getType()), formProperty.isRequired()));

                        // Visibility in form-config
                        formVisibilityString.append(
                                MessageFormat.format(getFormConfigFieldVisibilityTemplate(), uniquePropertyName));

                        // Appearance on screen in form-config
                        formAppearanceString.append(MessageFormat.format(getFormConfigFieldTemplate(),
                                uniquePropertyName, formProperty.getProperty()));

                    }

                }
            }

            // Replace all expressions in the description with the calculated value
            if (userTask.getDescription() != null) {
                for (String formProperty : formPropertyMapping.keySet()) {
                    String formPropertyExpression = "${" + formProperty + "}";
                    if (userTask.getDescription().contains(formPropertyExpression)) {

                        // Update description
                        userTask.setDescription(userTask.getDescription().replace(formPropertyExpression,
                                "${" + formPropertyMapping.get(formProperty) + "}"));

                    }
                }
            }

            // Add name and all form-properties to model XML
            taskModelsString.append(MessageFormat.format(getTaskModelTypeTemplate(), prefixedUniqueTaskName,
                    descriptionPropertyName, userTask.getDescription(), typeString.toString()));

            // Add task-form-config
            formConfigString.append(MessageFormat.format(getFormConfigEvaluatorConfigTemplate(),
                    prefixedUniqueTaskName, formVisibilityString.toString(), formAppearanceString.toString()));
        }
    }

    protected void uploadTaskModel(StringBuilder taskModelsString, String baseFileName) {
        Session session = getCmisSession();
        Folder modelFolder = (Folder) session.getObjectByPath(DATA_DICTIONARY_FOLDER);

        String taskModelFileName = baseFileName + "-task-model.xml";

        // Finally, wrap all taskdefinitions is right XML -> this is the FULL
        // model file, including generic start-task
        String taskModelId = UUID.randomUUID().toString();
        String taskModelXML = MessageFormat.format(getTaskModelTemplate(), taskModelId,
                taskModelsString.toString());
        LOGGER.info("Deploying task model XML:");
        prettyLogXml(taskModelXML);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(taskModelXML.getBytes());
        ContentStream contentStream = new ContentStreamImpl(taskModelFileName, null, "application/xml",
                inputStream);

        // Verify whether it is an update or a new workflow
        Document taskModelDocument = getDocumentFromFolder(DATA_DICTIONARY_FOLDER, taskModelFileName);
        if (taskModelDocument == null) {
            HashMap<String, Object> properties = new HashMap<String, Object>();
            properties.put("cmis:name", taskModelFileName);
            properties.put("cmis:objectTypeId", "D:cm:dictionaryModel");
            properties.put("cm:modelActive", true);

            LOGGER.info("Task model file : " + taskModelFileName);
            modelFolder.createDocument(properties, contentStream, VersioningState.MAJOR);
        } else {
            LOGGER.info("Updating content of " + taskModelFileName);
            taskModelDocument.setContentStream(contentStream, true);
        }
    }

    protected void uploadFormConfig(StringBuilder formConfigStringBuilder, KickstartWorkflow workflow,
            String baseFileName) {
        int version = 0;
        String formConfig = generateFormConfig(formConfigStringBuilder, workflow, version, baseFileName);

        int result = executeFormConfigUpload(formConfig);

        // Okay, Okay, this is pretty hackish. But it was the fastest way to get it working.
        // In an ideal world, with plenty of time and pink unicorns, we would fetch the form config
        // xml, adapt it with the new forms and save that back.
        // Or even beter: we could query Share to know the actual latest version number
        while (result == 409) {
            LOGGER.info("Found deployed form config with version " + version + " Trying now with " + (version + 1));
            version = version + 1;
            formConfig = generateFormConfig(formConfigStringBuilder, workflow, version, baseFileName);
            result = executeFormConfigUpload(formConfig);
        }

        // We're also uploading it to the workflow definition folder, for future use
        LOGGER.info("Uploading formconfig to " + WORKFLOW_DEFINITION_FOLDER);
        uploadStringToDocument(formConfig, WORKFLOW_DEFINITION_FOLDER, baseFileName + "-form-config.xml",
                "application/xml");
    }

    protected int executeFormConfigUpload(String formConfig) {
        HttpState state = new HttpState();
        state.setCredentials(new AuthScope(null, AuthScope.ANY_PORT),
                new UsernamePasswordCredentials(cmisUser, cmisPassword));

        LOGGER.info("Deploying form config XML: ");
        prettyLogXml(formConfig);

        PostMethod postMethod = new PostMethod(FORM_CONFIG_UPLOAD_URL);
        try {
            postMethod.setRequestEntity(new StringRequestEntity(formConfig, "application/xml", "UTF-8"));

            HttpClient httpClient = new HttpClient();
            int result = httpClient.executeMethod(null, postMethod, state);

            // Display status code
            LOGGER.info("Response status code: " + result);

            // Display response
            LOGGER.info("Response body: ");
            LOGGER.info(postMethod.getResponseBodyAsString());

            return result;

        } catch (Throwable t) {
            System.err.println("Error: " + t.getMessage());
            t.printStackTrace();
        } finally {
            postMethod.releaseConnection();
        }

        throw new RuntimeException("Programmatic error. You shouldn't be here.");
    }

    private String generateFormConfig(StringBuilder evaluatorConfigStringBuilder, KickstartWorkflow workflow,
            int version, String baseFileName) {
        String formId = "kickstart_form_" + baseFileName;

        if (version > 0) {
            formId += "_" + version;
        }

        String formConfig = MessageFormat.format(getFormConfigTemplate(), formId, workflow.getId(),
                evaluatorConfigStringBuilder.toString());
        return formConfig;
    }

    protected Object getAlfrescoModelType(String type) {
        if (type.equals("text")) {
            return "d:text";
        } else if (type.equals("date")) {
            return "d:date";
        } else if (type.equals("number")) {
            return "d:long";
        }
        return null;
    }

    protected String createFriendlyName(String property) {
        return property.toLowerCase().replace(" ", "_");
    }

    public String getWorkflowMetaData(String processDefinitionId, String metadataKey) {
        String metadataFile = processDefinitionId;
        if (metadataKey.equals(MetaDataKeys.WORKFLOW_JSON_SOURCE)) {
            metadataFile = metadataFile + ".json";
        }

        Document document = getDocumentFromFolder(WORKFLOW_DEFINITION_FOLDER, metadataFile);
        StringBuilder strb = new StringBuilder();

        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(document.getContentStream().getStream()));
        try {
            String line = bufferedReader.readLine();
            while (line != null) {
                strb.append(line);
                line = bufferedReader.readLine();
            }
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, "Could not read metadata '" + metadataKey + "' : " + e.getMessage());
            e.printStackTrace();
        } finally {
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return strb.toString();
    }

    public List<KickstartWorkflowInfo> findWorkflowInformation(boolean includeCounts) {

        if (includeCounts) {
            // Not yet implemented, cause it would be an 1+n call ...
            throw new UnsupportedOperationException();
        }

        // Fetch all BPMN 2.0 xml processes from the definitions folder
        Session cmisSession = getCmisSession();
        Folder workflowDefinitionFolder = (Folder) cmisSession.getObjectByPath(WORKFLOW_DEFINITION_FOLDER);
        String query = "select t.cm:description, d." + PropertyIds.NAME + ", d." + PropertyIds.CREATION_DATE
                + " from cmis:document as d join cm:titled as t on d.cmis:objectId = t.cmis:objectId where in_folder(d, '"
                + workflowDefinitionFolder.getId() + "') and d.cmis:name LIKE '%.bpmn20.xml' order by d.cmis:name";
        LOGGER.info("Executing CMIS query '" + query + "'");
        ItemIterable<QueryResult> results = cmisSession.query(query, false);

        // Transmorph them into the correct KickstartWorkflowInfo object
        ArrayList<KickstartWorkflowInfo> workflowInfos = new ArrayList<KickstartWorkflowInfo>();
        for (QueryResult result : results) {
            // We're using only a fraction of the KickstartWorkflowInfo objects
            KickstartWorkflowInfo kickstartWorkflowInfo = new KickstartWorkflowInfo();
            kickstartWorkflowInfo.setName((String) result.getPropertyValueById("cm:description"));
            kickstartWorkflowInfo
                    .setId(processNameToBaseName((String) result.getPropertyValueById(PropertyIds.NAME)));
            GregorianCalendar createDate = result.getPropertyValueById(PropertyIds.CREATION_DATE);
            kickstartWorkflowInfo.setCreateTime(createDate.getTime());
            workflowInfos.add(kickstartWorkflowInfo);
        }

        return workflowInfos;
    }

    public KickstartWorkflowInfo findWorkflowInformation(String processDefinitionId, boolean includeCounts) {
        KickstartWorkflowInfo kickstartWorkflowInfo = new KickstartWorkflowInfo();

        // Get general info
        Session cmisSession = getCmisSession();
        Folder workflowDefinitionFolder = (Folder) cmisSession.getObjectByPath(WORKFLOW_DEFINITION_FOLDER);

        try {
            Document bpmn20Document = (Document) cmisSession.getObjectByPath(
                    workflowDefinitionFolder.getPath() + "/" + generateBpmnResourceName(processDefinitionId));
            if (bpmn20Document != null) {
                kickstartWorkflowInfo
                        .setId(processNameToBaseName((String) bpmn20Document.getPropertyValue(PropertyIds.NAME)));
                kickstartWorkflowInfo.setName((String) bpmn20Document.getPropertyValue("cm:description"));
                kickstartWorkflowInfo.setCreateTime(
                        ((GregorianCalendar) bpmn20Document.getPropertyValue(PropertyIds.CREATION_DATE)).getTime());
            } else {
                return null;
            }
        } catch (CmisObjectNotFoundException e) {
            return null;
        }

        // Get counts
        if (includeCounts) {
            kickstartWorkflowInfo
                    .setNrOfRuntimeInstances(retrieveWorkflowInstanceIds(kickstartWorkflowInfo.getName()).size());
        }

        return kickstartWorkflowInfo;
    }

    public KickstartWorkflow findWorkflowById(String id) {
        throw new UnsupportedOperationException();
    }

    public void deleteWorkflow(String processDefinitionId) {
        // Delete all workflow instances, as this will block the undeployment of the process otherwise
        deleteWorkflowInstances(processDefinitionId);

        // TODO: make constants for the files, both for use in creation/removal

        // Remove all files in the workflow definition folder
        deleteDocumentFromFolder(WORKFLOW_DEFINITION_FOLDER, processDefinitionId + ".png");
        deleteDocumentFromFolder(WORKFLOW_DEFINITION_FOLDER, processDefinitionId + "_image.png");
        deleteDocumentFromFolder(WORKFLOW_DEFINITION_FOLDER, processDefinitionId + ".json");
        deleteDocumentFromFolder(WORKFLOW_DEFINITION_FOLDER, processDefinitionId + ".bpmn20.xml");

        // Remove task model
        deleteDocumentFromFolder(DATA_DICTIONARY_FOLDER, processDefinitionId + "-task-model.xml");

        // Remove form config
        deleteFormConfig(processDefinitionId);
        deleteDocumentFromFolder(WORKFLOW_DEFINITION_FOLDER, processDefinitionId + "-form-config.xml");
    }

    public InputStream getProcessImage(String processDefinitionId) {
        Session cmisSession = getCmisSession();
        Folder workflowDefinitionFolder = (Folder) cmisSession.getObjectByPath(WORKFLOW_DEFINITION_FOLDER);

        Document imageDocument = (Document) cmisSession.getObjectByPath(
                workflowDefinitionFolder.getPath() + "/" + processDefinitionIdToProcessImage(processDefinitionId));
        return imageDocument.getContentStream().getStream();
    }

    public void setProcessImage(String processDefinitionId, InputStream processImageStream) {
        Session cmisSession = getCmisSession();
        Folder workflowDefinitionFolder = (Folder) cmisSession.getObjectByPath(WORKFLOW_DEFINITION_FOLDER);

        String fileName = processDefinitionIdToProcessImage(processDefinitionId);
        ContentStream contentStream = new ContentStreamImpl(fileName, null, "image/png", processImageStream);

        Document imageDocument = getDocumentFromFolder(workflowDefinitionFolder.getPath(), fileName);
        if (imageDocument == null) {

            HashMap<String, Object> properties = new HashMap<String, Object>();
            properties.put("cmis:name", fileName);
            properties.put("cmis:objectTypeId", "cmis:document");

            workflowDefinitionFolder.createDocument(properties, contentStream, VersioningState.MAJOR);

        } else {

            imageDocument.setContentStream(contentStream, true);

        }

    }

    // Helpets //////////////////////

    /**
     * Generates a valid bpmn 2.0 file name for the given process name.
     */
    protected String generateBpmnResourceName(String string) {
        return string.replace(" ", "_") + ".bpmn20.xml";
    }

    protected String processDefinitionIdToProcessImage(String processDefinitionId) {
        return processDefinitionId + "_image.png";
    }

    public InputStream getBpmnXml(String processDefinitionId) {
        throw new UnsupportedOperationException();
    }

    // CMIS helper methods  //////////////////////////////////////////////////////////////////////////////////////////////

    protected void deleteDocumentFromFolder(String folderPath, String documentName) {
        Document document = getDocumentFromFolder(folderPath, documentName);
        if (document != null) {
            document.delete(true);
            LOGGER.info("Removed document " + folderPath + "/" + documentName);
        }
    }

    protected Document getDocumentFromFolder(String folderPath, String documentName) {
        try {
            Session cmisSession = getCmisSession();
            Folder workflowDefinitionFolder = (Folder) cmisSession.getObjectByPath(folderPath);
            String path = workflowDefinitionFolder.getPath() + "/" + documentName;
            return (Document) cmisSession.getObjectByPath(path);
        } catch (CmisObjectNotFoundException e) {
            return null;
        }
    }

    protected void uploadStringToDocument(String string, String folderPath, String documentName, String mimetype) {
        Session cmisSession = getCmisSession();
        Folder folder = (Folder) cmisSession.getObjectByPath(folderPath);

        ContentStream contentStream = new ContentStreamImpl(documentName, null, mimetype,
                new ByteArrayInputStream(string.getBytes()));
        Document document = getDocumentFromFolder(folder.getPath(), documentName);

        if (document == null) {
            HashMap<String, Object> properties = new HashMap<String, Object>();
            properties.put("cmis:name", documentName);
            properties.put("cmis:objectTypeId", "cmis:document");
            folder.createDocument(properties, contentStream, VersioningState.MAJOR);
        } else {
            document.setContentStream(contentStream, true);
        }
    }

    // Helper methods for REST calls /////////////////////////////////////////////////////////////////////////////////////

    protected List<String> retrieveWorkflowInstanceIds(String workflowName) {
        String url = ALFRESCO_BASE_URL + "api/workflow-instances?state=active";
        JsonNode json = executeGet(url);
        ArrayNode data = (ArrayNode) json.get("data");

        ArrayList<String> workflowInstanceIds = new ArrayList<String>();
        for (int i = 0; i < data.size(); i++) {
            String title = data.get(i).get("title").asText();
            if (title.equalsIgnoreCase(workflowName)) {
                workflowInstanceIds.add(data.get(i).get("id").asText());
            }
        }

        return workflowInstanceIds;
    }

    protected JsonNode executeGet(String url) {
        HttpState state = new HttpState();
        state.setCredentials(new AuthScope(null, AuthScope.ANY_PORT),
                new UsernamePasswordCredentials(cmisUser, cmisPassword));

        GetMethod getMethod = new GetMethod(url);
        LOGGER.info("Executing GET '" + url + "'");

        try {
            HttpClient httpClient = new HttpClient();
            int result = httpClient.executeMethod(null, getMethod, state);

            // Display Response
            String responseJson = getMethod.getResponseBodyAsString();
            LOGGER.info("Response status code: " + result);
            LOGGER.info("Response body: " + responseJson);

            ObjectMapper mapper = new ObjectMapper();
            JsonNode json = mapper.readTree(responseJson);
            return json;

        } catch (Throwable t) {
            System.err.println("Error: " + t.getMessage());
            t.printStackTrace();
        } finally {
            getMethod.releaseConnection();
        }
        return null;
    }

    protected void deleteWorkflowInstances(String workflowName) {
        List<String> workflowInstanceIds = retrieveWorkflowInstanceIds(workflowName);
        for (String workflowInstanceId : workflowInstanceIds) {
            deleteWorkflowInstance(workflowInstanceId);
            LOGGER.info("Deleted workflow instance '" + workflowInstanceId + "'");
        }
    }

    protected void deleteWorkflowInstance(String workflowInstanceId) {
        HttpState state = new HttpState();
        state.setCredentials(new AuthScope(null, AuthScope.ANY_PORT),
                new UsernamePasswordCredentials(cmisUser, cmisPassword));

        // Only fetching one, we're only interested in the paging information, after all
        String url = ALFRESCO_BASE_URL + "api/workflow-instances/" + workflowInstanceId + "?forced=true";
        DeleteMethod deleteMethod = new DeleteMethod(url);
        LOGGER.info("Executing DELETE '" + url + "'");

        try {
            HttpClient httpClient = new HttpClient();
            int result = httpClient.executeMethod(null, deleteMethod, state);

            // Display Response
            String responseJson = deleteMethod.getResponseBodyAsString();
            LOGGER.info("Response status code: " + result);
            LOGGER.info("Response body: " + responseJson);

        } catch (Throwable t) {
            System.err.println("Error: " + t.getMessage());
            t.printStackTrace();
        } finally {
            deleteMethod.releaseConnection();
        }
    }

    protected void deleteFormConfig(String workflowId) {
        String url = SHARE_BASE_URL + "page/modules/module/delete?moduleId="
                + URLEncoder.encode("kickstart_form_" + workflowId);
        int statusCode = executeDelete(url);

        // Cont. hackyness from the upload. Read all about it there.
        int version = 1;
        while (statusCode == 200) {
            statusCode = executeDelete(url + "_" + version);
            version++;
        }
    }

    private int executeDelete(String url) {
        LOGGER.info("Executed module delete: " + url);
        GetMethod method = new GetMethod(url);
        try {
            HttpState httpState = new HttpState();
            httpState.setCredentials(new AuthScope(null, AuthScope.ANY_PORT),
                    new UsernamePasswordCredentials("admin", "admin"));

            HttpClient httpClient = new HttpClient();
            int statusCode = httpClient.executeMethod(null, method, httpState);
            LOGGER.info("Status code result for delete: " + statusCode);
            return statusCode;
        } catch (Throwable t) {
            LOGGER.log(Level.SEVERE, "Error: " + t.getMessage());
            t.printStackTrace();
        } finally {
            method.releaseConnection();
        }

        throw new RuntimeException("Programmatic error. You shouldn't be here.");
    }

    // Getters & Setters

    public String getCmisUser() {
        return cmisUser;
    }

    public void setCmisUser(String cmisUser) {
        this.cmisUser = cmisUser;
    }

    public String getCmisPassword() {
        return cmisPassword;
    }

    public void setCmisPassword(String cmisPassword) {
        this.cmisPassword = cmisPassword;
    }

    public String getCmisAtompubUrl() {
        return cmisAtompubUrl;
    }

    public void setCmisAtompubUrl(String cmisAtompubUrl) {
        this.cmisAtompubUrl = cmisAtompubUrl;
    }

    public Bpmn20MarshallingService getMarshallingService() {
        return marshallingService;
    }

    public void setMarshallingService(Bpmn20MarshallingService marshallingService) {
        this.marshallingService = marshallingService;
    }

    // Helper methods for XML templates /////////////////////////////////////////////////////////////////

    protected String getTaskModelTemplate() {
        if (TASK_MODEL_TEMPLATE == null) {
            TASK_MODEL_TEMPLATE = readTemplateFile(TASK_MODEL_TEMPLATE_FILE);
        }
        return TASK_MODEL_TEMPLATE;
    }

    protected String getTaskModelTypeTemplate() {
        if (TASK_MODEL_TYPE_TEMPLATE == null) {
            TASK_MODEL_TYPE_TEMPLATE = readTemplateFile(TASK_MODEL_TYPE_TEMPLATE_FILE);
        }
        return TASK_MODEL_TYPE_TEMPLATE;
    }

    protected String getTaskModelPropertyTemplate() {
        if (TASK_MODEL_PROPERTY_TEMPLATE == null) {
            TASK_MODEL_PROPERTY_TEMPLATE = readTemplateFile(TASK_MODEL_PROPERTY_TEMPLATE_FILE);
        }
        return TASK_MODEL_PROPERTY_TEMPLATE;
    }

    protected String getFormConfigTemplate() {
        if (FORM_CONFIG_TEMPLATE == null) {
            FORM_CONFIG_TEMPLATE = readTemplateFile(FORM_CONFIG_TEMPLATE_FILE);
        }
        return FORM_CONFIG_TEMPLATE;
    }

    protected String getFormConfigEvaluatorConfigTemplate() {
        if (FORM_CONFIG_EVALUATOR_CONFIG_TEMPLATE == null) {
            FORM_CONFIG_EVALUATOR_CONFIG_TEMPLATE = readTemplateFile(FORM_CONFIG_EVALUATOR_CONFIG_TEMPLATE_FILE);
        }
        return FORM_CONFIG_EVALUATOR_CONFIG_TEMPLATE;
    }

    protected String getFormConfigFieldTemplate() {
        if (FORM_CONFIG_FIELD_TEMPLATE == null) {
            FORM_CONFIG_FIELD_TEMPLATE = readTemplateFile(FORM_CONFIG_FIELD_TEMPLATE_FILE);
        }
        return FORM_CONFIG_FIELD_TEMPLATE;
    }

    protected String getFormConfigFieldVisibilityTemplate() {
        if (FORM_CONFIG_FIELD_VISIBILITY_TEMPLATE == null) {
            FORM_CONFIG_FIELD_VISIBILITY_TEMPLATE = readTemplateFile(FORM_CONFIG_FIELD_VISIBILITY_TEMPLATE_FILE);
        }
        return FORM_CONFIG_FIELD_VISIBILITY_TEMPLATE;
    }

    protected String getFormConfigInfoTemplate() {
        if (FORM_CONFIG_FIELD_INFO_TEMPLATE == null) {
            FORM_CONFIG_FIELD_INFO_TEMPLATE = readTemplateFile(FORM_CONFIG_FIELD_INFO_TEMPLATE_FILE);
        }
        return FORM_CONFIG_FIELD_INFO_TEMPLATE;
    }

    protected String readTemplateFile(String templateFile) {
        LOGGER.info("Reading template file '" + templateFile + "'");
        InputStream inputStream = AlfrescoKickstartServiceImpl.class.getResourceAsStream(templateFile);
        if (inputStream == null) {
            LOGGER.warning("Could not read template file '" + templateFile + "'!");
        } else {
            try {
                return IOUtils.toString(inputStream);
            } catch (IOException e) {
                LOGGER.log(Level.SEVERE, "Error while reading '" + templateFile + "' : " + e.getMessage());
            }
        }
        return null;
    }

    protected void prettyLogXml(String xml) {
        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");

            Source xmlInput = new StreamSource(new StringReader(xml));

            StreamResult xmlOutput = new StreamResult(new StringWriter());
            transformer.transform(xmlInput, xmlOutput);
            LOGGER.info(xmlOutput.getWriter().toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}