org.joget.apps.app.service.AppServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.joget.apps.app.service.AppServiceImpl.java

Source

package org.joget.apps.app.service;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.collections.map.ListOrderedMap;
import org.joget.apps.app.dao.AppDefinitionDao;
import org.joget.apps.app.dao.DatalistDefinitionDao;
import org.joget.apps.app.dao.EnvironmentVariableDao;
import org.joget.apps.app.dao.FormDefinitionDao;
import org.joget.apps.app.dao.MessageDao;
import org.joget.apps.app.dao.PackageDefinitionDao;
import org.joget.apps.app.dao.PluginDefaultPropertiesDao;
import org.joget.apps.app.dao.UserviewDefinitionDao;
import org.joget.apps.app.model.AppDefinition;
import org.joget.apps.app.model.DatalistDefinition;
import org.joget.apps.app.model.EnvironmentVariable;
import org.joget.apps.app.model.FormDefinition;
import org.joget.apps.app.model.ImportAppException;
import org.joget.apps.app.model.Message;
import org.joget.apps.app.model.PackageActivityForm;
import org.joget.apps.app.model.PackageActivityPlugin;
import org.joget.apps.app.model.PackageDefinition;
import org.joget.apps.app.model.PackageParticipant;
import org.joget.apps.app.model.PluginDefaultProperties;
import org.joget.apps.app.model.UserviewDefinition;
import org.joget.apps.form.dao.FormDataDao;
import org.joget.apps.form.lib.LinkButton;
import org.joget.apps.form.lib.SaveAsDraftButton;
import org.joget.apps.form.lib.SubmitButton;
import org.joget.apps.form.lib.TextField;
import org.joget.apps.form.lib.WorkflowFormBinder;
import org.joget.apps.form.model.Column;
import org.joget.apps.form.model.Element;
import org.joget.apps.form.model.Form;
import org.joget.apps.form.model.FormAction;
import org.joget.apps.form.model.FormData;
import org.joget.apps.form.model.FormRow;
import org.joget.apps.form.model.FormRowSet;
import org.joget.apps.form.model.FormStoreBinder;
import org.joget.apps.form.model.Section;
import org.joget.apps.form.service.FileUtil;
import org.joget.apps.form.service.FormService;
import org.joget.apps.form.service.FormUtil;
import org.joget.apps.userview.model.UserviewSetting;
import org.joget.apps.userview.service.UserviewService;
import org.joget.apps.workflow.lib.AssignmentCompleteButton;
import org.joget.commons.util.DynamicDataSourceManager;
import org.joget.commons.util.HostManager;
import org.joget.commons.util.LogUtil;
import org.joget.commons.util.ResourceBundleUtil;
import org.joget.commons.util.StringUtil;
import org.joget.commons.util.UuidGenerator;
import org.joget.plugin.base.PluginManager;
import org.joget.workflow.model.WorkflowActivity;
import org.joget.workflow.model.WorkflowAssignment;
import org.joget.workflow.model.WorkflowPackage;
import org.joget.workflow.model.WorkflowProcess;
import org.joget.workflow.model.WorkflowProcessLink;
import org.joget.workflow.model.WorkflowProcessResult;
import org.joget.workflow.model.WorkflowVariable;
import org.joget.workflow.model.service.WorkflowManager;
import org.joget.workflow.util.WorkflowUtil;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

/**
 * Implementation of AppService interface
 * 
 */
@SuppressWarnings("restriction")
@Service("appService")
public class AppServiceImpl implements AppService {

    @Autowired
    FormService formService;
    @Autowired
    WorkflowManager workflowManager;
    @Autowired
    AppDefinitionDao appDefinitionDao;
    @Autowired
    DatalistDefinitionDao datalistDefinitionDao;
    @Autowired
    UserviewDefinitionDao userviewDefinitionDao;
    @Autowired
    MessageDao messageDao;
    @Autowired
    EnvironmentVariableDao environmentVariableDao;
    @Autowired
    PluginDefaultPropertiesDao pluginDefaultPropertiesDao;
    @Autowired
    PackageDefinitionDao packageDefinitionDao;
    @Autowired
    PluginManager pluginManager;
    @Autowired
    FormDataDao formDataDao;
    @Autowired
    UserviewService userviewService;
    //----- Workflow use cases ------

    /**
     * Retrieves the workflow process definition for a specific app version.
     * @param appId
     * @param version
     * @param processDefId
     * @return
     */
    public WorkflowProcess getWorkflowProcessForApp(String appId, String version, String processDefId) {
        AppDefinition appDef = getAppDefinition(appId, version);
        PackageDefinition packageDef = appDef.getPackageDefinition();
        String processDefIdWithVersion = AppUtil.getProcessDefIdWithVersion(packageDef.getId(),
                packageDef.getVersion().toString(), processDefId);
        WorkflowProcess process = workflowManager.getProcess(processDefIdWithVersion);
        return process;
    }

    //    /**
    //     * Retrieves the app definition for a specific workflow process.
    //     * @param activityId
    //     * @return
    //     */
    //    public AppDefinition getAppDefinitionForWorkflowProcess(String processId) {
    //        AppDefinition appDef = null;
    //        String processDefId = workflowManager.getProcessDefIdByInstanceId(processId);
    //        String packageId = WorkflowUtil.getProcessDefPackageId(processDefId);
    //        Long packageVersion = new Long(WorkflowUtil.getProcessDefVersion(processDefId));
    //        PackageDefinition packageDef = packageDefinitionDao.loadPackageDefinitionByProcess(packageId, packageVersion, processDefId);
    //        appDef = packageDef.getAppDefinition();
    //        return appDef;
    //    }
    //
    /**
     * Retrieves the app definition for a specific workflow activity assignment.
     * @param activityId
     * @return
     */
    public AppDefinition getAppDefinitionForWorkflowActivity(String activityId) {
        AppDefinition appDef = null;

        WorkflowActivity activity = workflowManager.getActivityById(activityId);
        if (activity != null) {
            String processDefId = activity.getProcessDefId();
            WorkflowProcess process = workflowManager.getProcess(processDefId);
            if (process != null) {
                String packageId = process.getPackageId();
                Long packageVersion = Long.parseLong(process.getVersion());
                PackageDefinition packageDef = packageDefinitionDao.loadPackageDefinition(packageId,
                        packageVersion);
                if (packageDef != null) {
                    appDef = packageDef.getAppDefinition();
                }
            }
        }
        // set into thread
        AppUtil.setCurrentAppDefinition(appDef);
        return appDef;
    }

    /**
     * Retrieves the app definition for a specific workflow process.
     * @param processId
     * @return
     */
    public AppDefinition getAppDefinitionForWorkflowProcess(String processId) {
        AppDefinition appDef = null;

        WorkflowProcess process = workflowManager.getRunningProcessById(processId);
        if (process != null) {
            String packageId = process.getPackageId();
            Long packageVersion = Long.parseLong(process.getVersion());
            PackageDefinition packageDef = packageDefinitionDao.loadPackageDefinition(packageId, packageVersion);
            if (packageDef != null) {
                appDef = packageDef.getAppDefinition();
            }
        }
        // set into thread
        AppUtil.setCurrentAppDefinition(appDef);
        return appDef;
    }

    /**
     * Retrieves the app definition for a specific workflow process definition id.
     * @param processDefId
     * @return
     */
    public AppDefinition getAppDefinitionWithProcessDefId(String processDefId) {
        AppDefinition appDef = null;

        processDefId = workflowManager.getConvertedLatestProcessDefId(processDefId);
        String[] params = processDefId.split("#");
        String packageId = params[0];
        Long packageVersion = Long.parseLong(params[1]);

        PackageDefinition packageDef = packageDefinitionDao.loadPackageDefinition(packageId, packageVersion);
        if (packageDef != null) {
            appDef = packageDef.getAppDefinition();
        }

        // set into thread
        AppUtil.setCurrentAppDefinition(appDef);
        return appDef;
    }

    /**
     * Retrieve a form for a specific activity instance
     * @param appId
     * @param version
     * @param activityId
     * @param formData
     * @param formUrl
     * @return
     */
    public PackageActivityForm viewAssignmentForm(String appId, String version, String activityId,
            FormData formData, String formUrl) {
        return viewAssignmentForm(appId, version, activityId, formData, formUrl, null);
    }

    /**
     * Retrieve a form for a specific activity instance
     * @param appId
     * @param version
     * @param activityId
     * @param formData
     * @param formUrl
     * @param cancelUrl
     * @return
     */
    public PackageActivityForm viewAssignmentForm(String appId, String version, String activityId,
            FormData formData, String formUrl, String cancelUrl) {
        AppDefinition appDef = getAppDefinition(appId, version);
        WorkflowAssignment assignment = workflowManager.getAssignment(activityId);
        return viewAssignmentForm(appDef, assignment, formData, formUrl, cancelUrl);
    }

    /**
     * Retrieve a form for a specific activity instance
     * @param appDef
     * @param assignment
     * @param formData
     * @param formUrl
     * @return
     */
    public PackageActivityForm viewAssignmentForm(AppDefinition appDef, WorkflowAssignment assignment,
            FormData formData, String formUrl) {
        return viewAssignmentForm(appDef, assignment, formData, formUrl, null);
    }

    /**
     * Retrieve a form for a specific activity instance
     * @param appDef
     * @param assignment
     * @param formData
     * @param formUrl
     * @param cancelUrl
     * @return
     */
    public PackageActivityForm viewAssignmentForm(AppDefinition appDef, WorkflowAssignment assignment,
            FormData formData, String formUrl, String cancelUrl) {
        String activityId = assignment.getActivityId();
        String processId = assignment.getProcessId();
        String processDefId = assignment.getProcessDefId();
        String activityDefId = assignment.getActivityDefId();
        PackageActivityForm activityForm = retrieveMappedForm(appDef.getAppId(), appDef.getVersion().toString(),
                processDefId, activityDefId);

        // get origin process id
        String originProcessId = getOriginProcessId(processId);

        // get mapped form
        if (formData == null) {
            formData = new FormData();
        }
        formData.setActivityId(activityId);
        formData.setProcessId(processId);
        formData.setPrimaryKeyValue(originProcessId);

        Form form = retrieveForm(appDef, activityForm, formData, assignment);
        if (form == null) {
            form = createDefaultForm(processId, formData);
        }

        // set action URL
        form.setProperty("url", formUrl);

        // decorate form with actions
        if (activityForm != null && activityForm.getFormId() != null && !activityForm.getFormId().isEmpty()
                && !activityForm.getDisableSaveAsDraft()) {
            Element saveButton = (Element) pluginManager.getPlugin(SaveAsDraftButton.class.getName());
            saveButton.setProperty(FormUtil.PROPERTY_ID, "saveAsDraft");
            saveButton.setProperty("label", ResourceBundleUtil.getMessage("form.button.saveAsDraft"));
            form.addAction((FormAction) saveButton);
        }
        Element completeButton = (Element) pluginManager.getPlugin(AssignmentCompleteButton.class.getName());
        completeButton.setProperty(FormUtil.PROPERTY_ID, AssignmentCompleteButton.DEFAULT_ID);
        completeButton.setProperty("label", ResourceBundleUtil.getMessage("form.button.complete"));
        form.addAction((FormAction) completeButton);
        if (cancelUrl != null && !cancelUrl.isEmpty()) {
            Element cancelButton = (Element) pluginManager.getPlugin(LinkButton.class.getName());
            cancelButton.setProperty(FormUtil.PROPERTY_ID, "cancel");
            cancelButton.setProperty("label", ResourceBundleUtil.getMessage("general.method.label.cancel"));
            cancelButton.setProperty("url", cancelUrl);
            form.addAction((FormAction) cancelButton);
        }
        form = decorateFormActions(form);

        // set to definition
        if (activityForm == null) {
            activityForm = new PackageActivityForm();
        }
        activityForm.setForm(form);

        if (PackageActivityForm.ACTIVITY_FORM_TYPE_EXTERNAL.equals(activityForm.getType())) {
            // set external URL
            String externalUrl = AppUtil.processHashVariable(activityForm.getFormUrl(), assignment, null, null);
            if (externalUrl.indexOf("?") >= 0) {
                if (!externalUrl.endsWith("?") && !externalUrl.endsWith("&")) {
                    externalUrl += "&";
                }
            } else {
                externalUrl += "?";
            }
            activityForm.setFormUrl(externalUrl);
        }

        return activityForm;
    }

    /**
     * Process a submitted form to complete an assignment
     * @param form
     * @param assignment
     * @param formData
     * @param workflowVariableMap
     * @return
     */
    @SuppressWarnings("deprecation")
    public FormData completeAssignmentForm(Form form, WorkflowAssignment assignment, FormData formData,
            Map<String, String> workflowVariableMap) {
        if (formData == null) {
            formData = new FormData();
        }

        // get assignment
        String activityId = assignment.getActivityId();
        @SuppressWarnings("unused")
        String processId = assignment.getProcessId();
        @SuppressWarnings("unused")
        String processDefId = assignment.getProcessDefId();
        @SuppressWarnings("unused")
        String activityDefId = assignment.getActivityDefId();

        // accept assignment if necessary
        if (!assignment.isAccepted()) {
            workflowManager.assignmentAccept(activityId);
        }

        // get and submit mapped form
        if (form != null) {
            formData = submitForm(form, formData, false);
        }

        Map<String, String> errors = formData.getFormErrors();
        if (!formData.getStay() && (errors == null || errors.isEmpty())) {
            // complete assignment
            workflowManager.assignmentComplete(activityId, workflowVariableMap);
        }
        return formData;
    }

    /**
     * Process a submitted form to complete an assignment
     * @param appId
     * @param version
     * @param activityId
     * @param formData
     * @param workflowVariableMap
     * @return
     */
    @SuppressWarnings("deprecation")
    public FormData completeAssignmentForm(String appId, String version, String activityId, FormData formData,
            Map<String, String> workflowVariableMap) {
        if (formData == null) {
            formData = new FormData();
        }

        // get assignment
        WorkflowAssignment assignment = workflowManager.getAssignment(activityId);
        String processId = assignment.getProcessId();
        String processDefId = assignment.getProcessDefId();
        String activityDefId = assignment.getActivityDefId();

        // accept assignment if necessary
        if (!assignment.isAccepted()) {
            workflowManager.assignmentAccept(activityId);
        }

        // get and submit mapped form
        PackageActivityForm paf = retrieveMappedForm(appId, version, processDefId, activityDefId);
        if (paf != null) {
            String formDefId = paf.getFormId();
            if (formDefId != null && !formDefId.isEmpty()) {
                String originProcessId = getOriginProcessId(processId);
                formData.setPrimaryKeyValue(originProcessId);
                formData.setProcessId(processId);

                AppDefinition appDef = getAppDefinition(appId, version);
                Form form = retrieveForm(appDef, paf, formData, assignment);
                formData = submitForm(form, formData, false);
            }
        }

        Map<String, String> errors = formData.getFormErrors();
        if (!formData.getStay() && (errors == null || errors.isEmpty())) {
            // complete assignment
            workflowManager.assignmentComplete(activityId, workflowVariableMap);
        }
        return formData;
    }

    /**
     * Retrieve form mapped to start a process
     * @param appId
     * @param version
     * @param processDefId
     * @param formData
     * @param formUrl
     * @return
     */
    public PackageActivityForm viewStartProcessForm(String appId, String version, String processDefId,
            FormData formData, String formUrl) {
        AppDefinition appDef = getAppDefinition(appId, version);
        PackageActivityForm startFormDef = retrieveMappedForm(appId, version, processDefId,
                WorkflowUtil.ACTIVITY_DEF_ID_RUN_PROCESS);
        if (startFormDef != null) {
            if (startFormDef.getFormId() != null && !startFormDef.getFormId().isEmpty()) {
                // get mapped form
                Form startForm = retrieveForm(appDef, startFormDef, formData, null);
                if (startForm != null) {
                    // set action URL
                    startForm.setProperty("url", formUrl);

                    // decorate form with actions
                    Element submitButton = (Element) pluginManager
                            .getPlugin(AssignmentCompleteButton.class.getName());
                    submitButton.setProperty(FormUtil.PROPERTY_ID, AssignmentCompleteButton.DEFAULT_ID);
                    submitButton.setProperty("label", ResourceBundleUtil.getMessage("form.button.submit"));
                    startForm.addAction((FormAction) submitButton);
                    startForm = decorateFormActions(startForm);

                    // set to definition
                    startFormDef.setForm(startForm);
                }
            }
            if (PackageActivityForm.ACTIVITY_FORM_TYPE_EXTERNAL.equals(startFormDef.getType())) {
                // set external URL
                String externalUrl = AppUtil.processHashVariable(startFormDef.getFormUrl(), null, null, null);
                if (externalUrl.indexOf("?") >= 0) {
                    if (!externalUrl.endsWith("?") && !externalUrl.endsWith("&")) {
                        externalUrl += "&";
                    }
                } else {
                    externalUrl += "?";
                }
                startFormDef.setFormUrl(externalUrl);
            }
        }
        return startFormDef;
    }

    /**
     * Start a process through a form submission
     * @param appId
     * @param version
     * @param processDefId
     * @param formData
     * @param workflowVariableMap
     * @param originProcessId
     * @param formUrl
     * @return
     */
    public WorkflowProcessResult submitFormToStartProcess(String appId, String version, String processDefId,
            FormData formData, Map<String, String> workflowVariableMap, String originProcessId, String formUrl) {
        WorkflowProcessResult result = null;
        if (formData == null) {
            formData = new FormData();
        }

        AppDefinition appDef = getAppDefinition(appId, version);
        PackageDefinition packageDef = appDef.getPackageDefinition();
        String processDefIdWithVersion = AppUtil.getProcessDefIdWithVersion(packageDef.getId(),
                packageDef.getVersion().toString(), processDefId);

        // get form
        PackageActivityForm startFormDef = viewStartProcessForm(appId, appDef.getVersion().toString(), processDefId,
                formData, formUrl);
        if (startFormDef != null && startFormDef.getForm() != null) {
            Form startForm = startFormDef.getForm();

            FormData formResult = formService.executeFormActions(startForm, formData);
            if (formResult.getFormResult(AssignmentCompleteButton.DEFAULT_ID) != null) {
                // validate form
                formData = FormUtil.executeElementFormatDataForValidation(startForm, formData);
                formResult = formService.validateFormData(startForm, formData);

                Map<String, String> errors = formResult.getFormErrors();
                if (!formResult.getStay() && (errors == null || errors.isEmpty())) {
                    if (originProcessId == null
                            && formResult.getRequestParameter(FormUtil.FORM_META_ORIGINAL_ID) != null
                            && !formResult.getRequestParameter(FormUtil.FORM_META_ORIGINAL_ID).isEmpty()) {
                        originProcessId = formResult.getRequestParameter(FormUtil.FORM_META_ORIGINAL_ID);
                    } else if (startForm.getPrimaryKeyValue(formResult) != null) {
                        originProcessId = startForm.getPrimaryKeyValue(formResult);
                    }

                    // start process
                    result = workflowManager.processStart(processDefIdWithVersion, null, workflowVariableMap, null,
                            originProcessId, true);
                    String processId = result.getProcess().getInstanceId();
                    String originId = (originProcessId != null && originProcessId.trim().length() > 0)
                            ? originProcessId
                            : processId;
                    originId = getOriginProcessId(originId);

                    // set primary key
                    formResult.setPrimaryKeyValue(originId);
                    formResult.setProcessId(processId);

                    // submit form
                    formResult = submitForm(startForm, formResult, true);
                    errors = formResult.getFormErrors();
                    if (!formResult.getStay() && (errors == null || errors.isEmpty())) {
                        result = workflowManager.processStartWithInstanceId(processDefIdWithVersion, processId,
                                workflowVariableMap);

                        // set next activity if configured
                        boolean autoContinue = (startFormDef != null) && startFormDef.isAutoContinue();
                        if (!autoContinue) {
                            // clear next activities
                            result.setActivities(new ArrayList<WorkflowActivity>());
                        }
                    } else {
                        workflowManager.removeProcessInstance(processId);
                        result = null;
                    }
                }
            }
        }
        return result;
    }

    /**
     * Retrieves ID of the form data row that is created or updated upon form submission.
     * @param formResult
     * @return
     */
    protected String retrieveFormRowId(FormData formResult) {
        String formRowId = null;
        Collection<FormStoreBinder> binders = formResult.getStoreBinders();
        for (FormStoreBinder binder : binders) {
            if (binder instanceof FormStoreBinder) {
                FormRowSet rowSet = formResult.getStoreBinderData(binder);
                if (!rowSet.isEmpty()) {
                    FormRow row = rowSet.get(0);
                    formRowId = row.getProperty("FORM_ID"); // TODO: use constant for form ID field
                }
                break;
            }
        }
        return formRowId;
    }

    /**
     * Returns the form definition ID for the form mapped to the specified activity definition ID.
     * @param appId
     * @param version
     * @param activityDefId
     * @param processDefId
     * @return
     */
    public PackageActivityForm retrieveMappedForm(String appId, String version, String processDefId,
            String activityDefId) {
        String processDefIdWithoutVersion = WorkflowUtil.getProcessDefIdWithoutVersion(processDefId);
        AppDefinition appDef = getAppDefinition(appId, version);
        PackageDefinition packageDef = appDef.getPackageDefinition();
        PackageActivityForm paf = packageDef.getPackageActivityForm(processDefIdWithoutVersion, activityDefId);
        if (paf != null) {
            try {
                paf = (PackageActivityForm) paf.clone();
            } catch (CloneNotSupportedException ex) {
                LogUtil.error(AppServiceImpl.class.getName(), ex,
                        "Error cloning PackageActivityForm for " + activityDefId);
            }
        }
        return paf;
    }

    protected FormDefinition retrieveFormDefinition(AppDefinition appDef, PackageActivityForm activityForm) {
        FormDefinition formDef = null;
        if (activityForm != null) {
            String formId = activityForm.getFormId();
            if (formId != null && !formId.isEmpty()) {
                formDef = formDefinitionDao.loadById(formId, appDef);
            }
        }
        return formDef;
    }

    protected Form retrieveForm(AppDefinition appDef, PackageActivityForm activityForm, FormData formData,
            WorkflowAssignment wfAssignment) {
        Form form = null;
        if (appDef != null && activityForm != null) {
            String formId = activityForm.getFormId();
            if (formId != null && !formId.isEmpty()) {
                // retrieve form HTML
                form = loadFormByFormDefId(appDef.getId(), appDef.getVersion().toString(), formId, formData,
                        wfAssignment);
            }
        }
        return form;
    }

    /**
     * Create a default empty form containing buttons for submission and fields for workflow variables
     * @return 
     */
    protected Form createDefaultForm(String processId, FormData formData) {
        // create default empty form
        Form form = new Form();
        form.setProperty(FormUtil.PROPERTY_ID, "assignmentForm");
        form.setLoadBinder(new WorkflowFormBinder());
        form.setStoreBinder(new WorkflowFormBinder());

        // add textfields for workflow variables
        Collection<Element> children = new ArrayList<Element>();
        Collection<WorkflowVariable> variableList = workflowManager.getProcessVariableList(processId);
        for (WorkflowVariable variable : variableList) {
            String varId = variable.getId();
            String varName = variable.getName();
            TextField tf = new TextField();
            tf.setProperty(FormUtil.PROPERTY_ID, varId);
            tf.setProperty(FormUtil.PROPERTY_LABEL, varName);
            tf.setProperty(AppUtil.PROPERTY_WORKFLOW_VARIABLE, varId);
            children.add(tf);
        }
        form.setChildren(children);

        // load form
        String json = formService.generateElementJson(form);
        form = formService.loadFormFromJson(json, formData);

        // set workflow variable parameter names
        Collection<Element> formFields = form.getChildren(formData);
        for (Element element : formFields) {
            if (element instanceof TextField) {
                element.setCustomParameterName(
                        AppUtil.PREFIX_WORKFLOW_VARIABLE + element.getProperty(FormUtil.PROPERTY_ID));
            }
        }

        return form;
    }

    /**
     * Returns the origin process ID or recordId for a process instance.
     * The return value can be the process ID of the top-most process 
     * which is started that possibly triggers other sub-processes, or it is a record id
     * used to start the top-most process.
     * @param processId
     * @return
     */
    public String getOriginProcessId(String processId) {
        WorkflowProcessLink link = workflowManager.getWorkflowProcessLink(processId);
        String originId = (link != null) ? link.getOriginProcessId() : processId;
        return originId;
    }

    /**
     * Check to see whether an activity is configured to automatically continue on to the next activity.
     * @param packageId
     * @param packageVersion
     * @param processDefId
     * @param activityDefId
     * @return
     */
    public boolean isActivityAutoContinue(String packageId, String packageVersion, String processDefId,
            String activityDefId) {
        boolean autoContinue = false;
        Long version = null;
        try {
            version = Long.parseLong(packageVersion);
        } catch (Exception e) {
            // invalid number, ignore
        }
        if (version != null) {
            processDefId = WorkflowUtil.getProcessDefIdWithoutVersion(processDefId);
            PackageDefinition packageDef = packageDefinitionDao.loadPackageDefinition(packageId, version);
            if (packageDef != null) {
                PackageActivityForm paf = packageDef.getPackageActivityForm(processDefId, activityDefId);
                if (paf != null) {
                    autoContinue = paf.isAutoContinue();
                }
            }
        }
        return autoContinue;
    }

    /**
     * Retrieve a data form
     * @param appId
     * @param version
     * @param formDefId
     * @param saveButtonLabel
     * @param submitButtonLabel
     * @param cancelButtonLabel
     * @param formData
     * @param formUrl
     * @param cancelUrl
     * @return
     */
    public Form viewDataForm(String appId, String version, String formDefId, String saveButtonLabel,
            String submitButtonLabel, String cancelButtonLabel, FormData formData, String formUrl,
            String cancelUrl) {
        return viewDataForm(appId, version, formDefId, saveButtonLabel, submitButtonLabel, cancelButtonLabel, null,
                formData, formUrl, cancelUrl);
    }

    /**
     * Retrieve a data form
     * @param appId
     * @param version
     * @param formDefId
     * @param saveButtonLabel
     * @param submitButtonLabel
     * @param cancelButtonLabel
     * @param cancelButtonTarget
     * @param formData
     * @param formUrl
     * @param cancelUrl
     * @return 
     */
    public Form viewDataForm(String appId, String version, String formDefId, String saveButtonLabel,
            String submitButtonLabel, String cancelButtonLabel, String cancelButtonTarget, FormData formData,
            String formUrl, String cancelUrl) {
        AppDefinition appDef = getAppDefinition(appId, version);

        if (formData == null) {
            formData = new FormData();
        }

        // get form
        Form form = loadFormByFormDefId(appDef.getId(), appDef.getVersion().toString(), formDefId, formData, null);

        // set action URL
        form.setProperty("url", formUrl);

        // decorate form with actions
        if (saveButtonLabel != null) {
            if (saveButtonLabel.isEmpty()) {
                saveButtonLabel = ResourceBundleUtil.getMessage("form.button.saveAsDraft");
            }
            Element saveButton = (Element) pluginManager.getPlugin(SaveAsDraftButton.class.getName());
            saveButton.setProperty(FormUtil.PROPERTY_ID, "saveAsDraft");
            saveButton.setProperty("label", saveButtonLabel);
            form.addAction((FormAction) saveButton);
        }
        if (submitButtonLabel != null) {
            if (submitButtonLabel.isEmpty()) {
                submitButtonLabel = ResourceBundleUtil.getMessage("general.method.label.submit");
            }
            Element submitButton = (Element) pluginManager.getPlugin(SubmitButton.class.getName());
            submitButton.setProperty(FormUtil.PROPERTY_ID, "submit");
            submitButton.setProperty("label", submitButtonLabel);
            form.addAction((FormAction) submitButton);
        }
        if (cancelButtonLabel != null) {
            if (cancelButtonLabel.isEmpty()) {
                cancelButtonLabel = ResourceBundleUtil.getMessage("general.method.label.cancel");
            }
            Element cancelButton = (Element) pluginManager.getPlugin(LinkButton.class.getName());
            cancelButton.setProperty(FormUtil.PROPERTY_ID, "cancel");
            cancelButton.setProperty("label", cancelButtonLabel);
            cancelButton.setProperty("url", cancelUrl);
            if (cancelButtonTarget != null) {
                cancelButton.setProperty("target", cancelButtonTarget);
            }
            form.addAction((FormAction) cancelButton);
        }
        form = decorateFormActions(form);

        return form;
    }

    /**
     * Returns a Collection of form data for a process based on criteria
     * 
     * @Deprecated API used in v2. Not implemented since v3.
     * 
     * @param formDefId
     * @param processId
     * @param query
     * @param sort
     * @param desc
     * @param start
     * @param rows
     * @return
     */
    public Collection<Form> listProcessFormData(String formDefId, String processId, String query, String sort,
            Boolean desc, int start, int rows) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    /**
     * Returns the total number of form data rows for a process based on criteria
     * 
     * @Deprecated API used in v2. Not implemented since v3.
     * 
     * @param formDefId
     * @param query
     * @return
     */
    public int countProcessFormData(String formDefId, String query) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    //----- Console app management use cases ------
    /**
     * Finds the app definition based on the appId and version, cached where possible
     * @param appId
     * @param version If null, empty or equals to AppDefinition.VERSION_LATEST, the latest version is returned.
     * @return null if the specific app definition is not found
     */
    public AppDefinition getAppDefinition(String appId, String version) {
        // get app from thread
        boolean isAppDefReset = AppUtil.isAppDefinitionReset();
        AppDefinition appDef = AppUtil.getCurrentAppDefinition();
        Long versionLong = AppUtil.convertVersionToLong(version);
        if (isAppDefReset || appDef == null || !appDef.getId().equals(appId)
                || (versionLong != null && !appDef.getVersion().equals(versionLong))) {
            // no matching app in thread, load from DAO
            appDef = loadAppDefinition(appId, version);
        }
        return appDef;
    }

    /**
     * Loads the app definition based on the appId and version
     * @param appId
     * @param version If null, empty or equals to AppDefinition.VERSION_LATEST, the latest version is returned.
     * @return null if the specific app definition is not found
     */
    public AppDefinition loadAppDefinition(String appId, String version) {
        // get app from thread
        AppDefinition appDef = null;
        Long versionLong = AppUtil.convertVersionToLong(version);
        if (versionLong == null) {
            // load latest
            appDef = appDefinitionDao.loadById(appId);
        } else {
            // load specific version
            try {
                appDef = appDefinitionDao.loadVersion(appId, versionLong);
            } catch (NumberFormatException e) {
                // TODO: handle exception
            } catch (NullPointerException e) {
                // TODO: handle exception
            }
        }

        // set into thread
        AppUtil.setCurrentAppDefinition(appDef);
        return appDef;
    }

    /**
     * Create a new app definition
     * @param appDefinition
     * @return A Collection of errors (if any).
     */
    @Transactional
    public Collection<String> createAppDefinition(AppDefinition appDefinition) {
        return createAppDefinition(appDefinition, null);
    }

    /**
     * Create a new app definition and duplicate the other app
     * @param appDefinition
     * @param copyAppDefinition
     * @return A Collection of errors (if any).
     */
    @Transactional
    public Collection<String> createAppDefinition(AppDefinition appDefinition, AppDefinition copy) {
        Collection<String> errors = new ArrayList<String>();

        // check for duplicate
        String appId = appDefinition.getId();
        AppDefinition appDef = appDefinitionDao.loadById(appId);
        if (appDef != null) {
            errors.add("console.app.error.label.idExists");
        } else {
            if (copy != null) {
                byte[] appDefinitionXml = null;
                byte[] xpdl = null;
                ByteArrayOutputStream baos = null;

                TimeZone current = TimeZone.getDefault();
                TimeZone.setDefault(TimeZone.getTimeZone("GMT 0"));

                try {
                    baos = new ByteArrayOutputStream();

                    Serializer serializer = new Persister();
                    serializer.write(copy, baos);

                    appDefinitionXml = baos.toByteArray();
                    baos.close();

                    String value = new String(appDefinitionXml, "UTF-8");

                    //replace id and name
                    value = value.replaceAll("<id>" + copy.getAppId() + "</id>", "<id>" + appId + "</id>");
                    value = value.replaceAll("<name>" + copy.getName() + "</name>",
                            "<name>" + appDefinition.getName() + "</name>");
                    value = value.replaceAll("<appId>" + copy.getAppId() + "</appId>",
                            "<appId>" + appId + "</appId>");

                    appDefinitionXml = value.getBytes("UTF-8");

                    PackageDefinition packageDef = copy.getPackageDefinition();
                    if (packageDef != null) {
                        xpdl = workflowManager.getPackageContent(packageDef.getId(),
                                packageDef.getVersion().toString());
                        Map<String, String> replace = new HashMap<String, String>();
                        replace.put(copy.getAppId(), appId);
                        replace.put(copy.getName(), appDefinition.getName());
                        xpdl = StringUtil.searchAndReplaceByteContent(xpdl, replace);
                    }

                    //import
                    appDef = serializer.read(AppDefinition.class, new ByteArrayInputStream(appDefinitionXml),
                            false);
                    @SuppressWarnings("unused")
                    AppDefinition newAppDef = importAppDefinition(appDef, 1L, xpdl);
                } catch (Exception ex) {
                    LogUtil.error(getClass().getName(), ex, "");
                    appDefinitionDao.saveOrUpdate(appDefinition);
                } finally {
                    if (baos != null) {
                        try {
                            baos.close();
                        } catch (Exception e) {
                            LogUtil.error(getClass().getName(), e, "");
                        }
                    }

                    TimeZone.setDefault(current);
                }

            } else {
                // create app
                appDefinitionDao.saveOrUpdate(appDefinition);
            }
        }

        return errors;
    }

    /**
     * Create a new version of an app from an existing latest version
     * @param appId
     * @param version
     * @return
     */
    @Transactional
    public AppDefinition createNewAppDefinitionVersion(String appId) {
        TimeZone current = TimeZone.getDefault();
        TimeZone.setDefault(TimeZone.getTimeZone("GMT 0"));

        Long version = appDefinitionDao.getLatestVersion(appId);
        AppDefinition appDef = appDefinitionDao.loadVersion(appId, version);

        Serializer serializer = new Persister();
        AppDefinition newAppDef = null;

        try {
            byte[] appData = getAppDefinitionXml(appId, version);

            //for backward compatible
            Map<String, String> replacement = new HashMap<String, String>();
            replacement.put("<!--disableSaveAsDraft>", "<disableSaveAsDraft>");
            replacement.put("</disableSaveAsDraft-->", "</disableSaveAsDraft>");
            replacement.put("<!--description>", "<description>");
            replacement.put("</description-->", "</description>");
            replacement.put("<!--meta>", "<meta>");
            replacement.put("</meta-->", "</meta>");
            appData = StringUtil.searchAndReplaceByteContent(appData, replacement);

            newAppDef = serializer.read(AppDefinition.class, new ByteArrayInputStream(appData));
        } catch (Exception e) {
            LogUtil.error(AppServiceImpl.class.getName(), e, appId);
        } finally {
            TimeZone.setDefault(current);
        }

        PackageDefinition packageDef = appDef.getPackageDefinition();
        byte[] xpdl = null;

        if (packageDef != null) {
            xpdl = workflowManager.getPackageContent(packageDef.getId(), packageDef.getVersion().toString());
        }

        Long newAppVersion = newAppDef.getVersion() + 1;
        return importAppDefinition(newAppDef, newAppVersion, xpdl);
    }

    /**
     * Delete a specific app version
     * @param appId
     * @param version
     */
    @Transactional
    public void deleteAppDefinitionVersion(String appId, Long version) {
        AppDefinition appDef = appDefinitionDao.loadVersion(appId, version);

        appDefinitionDao.delete(appDef);
    }

    /**
     * Delete all versions of an app
     * @param appId
     */
    @Transactional
    public void deleteAllAppDefinitionVersions(String appId) {
        // delete app
        appDefinitionDao.deleteAllVersions(appId);

        // TODO: delete processes

    }

    //----- Console workflow management use cases ------
    /**
     * Deploy an XPDL package for an app.
     * @param appId
     * @param version
     * @param packageXpdl
     * @param createNewApp
     * @return
     * @throws Exception
     */
    @Transactional
    public PackageDefinition deployWorkflowPackage(String appId, String version, byte[] packageXpdl,
            boolean createNewApp) throws Exception {

        PackageDefinition packageDef = null;
        AppDefinition appDef = null;
        String packageId = workflowManager.getPackageIdFromDefinition(packageXpdl);

        // get app version
        if (appId != null && !appId.isEmpty()) {
            appDef = loadAppDefinition(appId, version);

            // verify packageId
            if (appDef != null && !packageId.equalsIgnoreCase(appDef.getAppId())) {
                throw new UnsupportedOperationException("Package ID does not match App ID");
            }
        } else {
            appDef = loadAppDefinition(packageId, null);
        }

        if (appDef != null || createNewApp) {
            Long originalVersion = null;

            //to fix package id letter case issue
            if (appDef != null && !packageId.equals(appDef.getAppId())) {
                packageXpdl = StringUtil.searchAndReplaceByteContent(packageXpdl, packageId, appDef.getAppId());
                packageId = appDef.getAppId();
            }

            // deploy package
            String versionStr = workflowManager.getCurrentPackageVersion(packageId);
            String packageIdToUpload = (versionStr != null && !versionStr.isEmpty()) ? packageId : null;
            workflowManager.processUpload(packageIdToUpload, packageXpdl);

            // load package
            versionStr = workflowManager.getCurrentPackageVersion(packageId);
            WorkflowPackage workflowPackage = workflowManager.getPackage(packageId, versionStr);

            // create app from package if not specified
            if (appDef == null) {
                appDef = new AppDefinition();
                appDef.setAppId(packageId);
                appDef.setName(workflowPackage.getPackageName());
                appDef.setVersion(new Long(1));
                createAppDefinition(appDef);
            }

            // get package definition
            packageDef = appDef.getPackageDefinition();
            Long packageVersion = Long.parseLong(versionStr);
            if (packageDef == null) {
                packageDef = packageDefinitionDao.createPackageDefinition(appDef, packageVersion);

                //if app version is the only version for the app and no package is found, set process start white list to admin user
                if (appDefinitionDao.countVersions(appId) == 1) {
                    Collection<WorkflowProcess> processList = workflowManager.getProcessList(appDef.getAppId(),
                            packageVersion.toString());
                    for (WorkflowProcess wp : processList) {
                        String processIdWithoutVersion = WorkflowUtil.getProcessDefIdWithoutVersion(wp.getId());
                        PackageParticipant participant = new PackageParticipant();
                        participant.setProcessDefId(processIdWithoutVersion);
                        participant.setParticipantId(WorkflowUtil.PROCESS_START_WHITE_LIST);
                        participant.setType(PackageParticipant.TYPE_ROLE);
                        participant.setValue(PackageParticipant.VALUE_ROLE_ADMIN);
                        packageDefinitionDao.addAppParticipant(appDef.getAppId(), appDef.getVersion(), participant);
                    }
                }
            } else {
                originalVersion = packageDef.getVersion();
                packageDefinitionDao.updatePackageDefinitionVersion(packageDef, packageVersion);
            }

            if (originalVersion != null) {
                updateRunningProcesses(packageId, originalVersion, packageVersion);
            }
        }
        return packageDef;
    }

    //----- Console form management use cases ------
    @Resource
    FormDefinitionDao formDefinitionDao;

    /**
     * Create a new form definition
     * @param appDefinition
     * @param formDefinition
     * @return A Collection of errors (if any).
     */
    @Transactional
    public Collection<String> createFormDefinition(AppDefinition appDefinition, FormDefinition formDefinition) {
        Collection<String> errors = new ArrayList<String>();

        // check for duplicate
        String formId = formDefinition.getId();
        FormDefinition formDef = formDefinitionDao.loadById(formId, appDefinition);
        if (formDef != null) {
            errors.add("console.form.error.label.idExists");
        } else {
            // set app to form
            formDefinition.setAppDefinition(appDefinition);

            // create app
            formDefinitionDao.add(formDefinition);
        }

        return errors;
    }

    //---- form data use cases
    /**
     * Loads a Form based on a specific form definition ID
     * @param appId
     * @param version
     * @param formDefId
     * @param primaryKeyValue
     * @return
     */
    protected Form loadFormByFormDefId(String appId, String version, String formDefId, FormData formData,
            WorkflowAssignment wfAssignment) {
        Form form = null;
        try {
            AppDefinition appDef = getAppDefinition(appId, version);
            FormDefinition formDef = formDefinitionDao.loadById(formDefId, appDef);

            if (formDef != null && formDef.getJson() != null) {
                String formJson = formDef.getJson();
                formJson = AppUtil.processHashVariable(formJson, wfAssignment, StringUtil.TYPE_JSON, null);
                form = (Form) formService.loadFormFromJson(formJson, formData);
            }
        } catch (Exception e) {
            LogUtil.error(getClass().getName(), e, e.getMessage());
        }
        return form;
    }

    /**
     * Decorates a Form by adding a horizontal row of FormAction buttons in a "section-actions" section.
     * @param form
     * @return
     */
    protected Form decorateFormActions(Form form) {
        if (form != null && form.getActions() != null) {
            // create new section for buttons
            Section section = new Section();
            section.setProperty(FormUtil.PROPERTY_ID, "section-actions");
            Collection<Element> sectionChildren = new ArrayList<Element>();
            section.setChildren(sectionChildren);
            Collection<Element> formChildren = form.getChildren();
            if (formChildren == null) {
                formChildren = new ArrayList<Element>();
            }
            formChildren.add(section);

            // add new horizontal column to section
            Column column = new Column();
            column.setProperty("horizontal", "true");
            Collection<Element> columnChildren = new ArrayList<Element>();
            column.setChildren(columnChildren);
            sectionChildren.add(column);

            // add actions to column
            for (FormAction formAction : form.getActions()) {
                if (formAction != null && formAction instanceof Element) {
                    columnChildren.add((Element) formAction);
                }
            }
        }
        return form;
    }

    /**
     * Use case for form submission by ID
     * @param appId
     * @param version
     * @param formDefId
     * @param formData
     * @param ignoreValidation
     * @return
     */
    public FormData submitForm(String appId, String version, String formDefId, FormData formData,
            boolean ignoreValidation) {
        Form form = loadFormByFormDefId(appId, version, formDefId, formData, null);
        if (form != null) {
            return formService.submitForm(form, formData, ignoreValidation);
        } else {
            return formData;
        }
    }

    /**
     * Use case for form submission by Form object
     * @param form
     * @param formData
     * @param ignoreValidation
     * @return
     */
    public FormData submitForm(Form form, FormData formData, boolean ignoreValidation) {
        if (form != null) {
            try {
                formData = formService.submitForm(form, formData, ignoreValidation);
                FormUtil.executePostFormSubmissionProccessor(form, formData);
            } catch (Exception ex) {
                String formId = FormUtil.getElementParameterName(form);
                formData.addFormError(formId, "Error storing data: " + ex.getMessage());
                LogUtil.error(FormService.class.getName(), ex, "Error executing store binder");
            }
            return formData;
        } else {
            return formData;
        }
    }

    /**
     * Load specific data row (record) by primary key value for a specific form
     * @param appId
     * @param version
     * @param formDefId
     * @param primaryKeyValue
     * @return null if the form is not available, empty FormRowSet if the form is available but record is not found.
     */
    public FormRowSet loadFormData(String appId, String version, String formDefId, String primaryKeyValue) {
        FormRowSet results = null;
        Form form = viewDataForm(appId, version, formDefId, null, null, null, null, null, null);
        if (form != null) {
            results = loadFormData(form, primaryKeyValue);
        }
        return results;
    }

    /**
     * Load specific data row (record) by primary key value for a specific form
     * @param form
     * @param primaryKeyValue
     * @return null if the form is not available, empty FormRowSet if the form is available but record is not found.
     */
    public FormRowSet loadFormData(Form form, String primaryKeyValue) {
        if (form != null) {
            String formDefId = form.getPropertyString(FormUtil.PROPERTY_ID);
            String tableName = form.getPropertyString(FormUtil.PROPERTY_TABLE_NAME);
            return internalLoadFormData(formDefId, tableName, primaryKeyValue, true);
        }
        return null;
    }

    /**
     * Method to load specific data row (record) by primary key value for a specific form.
     * This method is transactional (since v5), but retains the method name for backward compatibility reasons.
     * @param form
     * @param primaryKeyValue
     * @return null if the form is not available, empty FormRowSet if the form is available but record is not found.
     */
    public FormRowSet loadFormDataWithoutTransaction(Form form, String primaryKeyValue) {
        if (form != null) {
            String formDefId = form.getPropertyString(FormUtil.PROPERTY_ID);
            String tableName = form.getPropertyString(FormUtil.PROPERTY_TABLE_NAME);
            return internalLoadFormData(formDefId, tableName, primaryKeyValue, false);
        }
        return null;
    }

    /**
     * Method to load specific data row (record) by primary key value for a specific form.
     * This method is transactional (since v5), but retains the method name for backward compatibility reasons.
     * @param formDefid
     * @param tableName
     * @param primaryKeyValue
     * @return null if the form is not available, empty FormRowSet if the form is available but record is not found.
     */
    public FormRowSet loadFormDataWithoutTransaction(String formDefid, String tableName, String primaryKeyValue) {
        return internalLoadFormData(formDefid, tableName, primaryKeyValue, false);
    }

    /**
     * Load specific data row (record) by primary key value for a specific form
     * @param formDefId
     * @param tableName
     * @param primaryKeyValue
     * @param transactional Determines whether the DAO method to call i.e. transactional or non-transactional. No longer used in v5.
     * @return null if the form is not available, empty FormRowSet if the form is available but record is not found.
     */
    protected FormRowSet internalLoadFormData(String formDefId, String tableName, String primaryKeyValue,
            boolean transactional) {
        FormRowSet results = null;
        if (formDefId != null && tableName != null) {
            results = new FormRowSet();
            results.setMultiRow(false);
            if (primaryKeyValue != null && primaryKeyValue.trim().length() > 0) {
                FormRow row = (transactional) ? formDataDao.load(formDefId, tableName, primaryKeyValue)
                        : formDataDao.loadWithoutTransaction(formDefId, tableName, primaryKeyValue);
                if (row != null) {
                    results.add(row);
                }
                LogUtil.debug(getClass().getName(), "  -- Loaded form data row [" + primaryKeyValue + "] for form ["
                        + formDefId + "] from table [" + tableName + "]");
            }
        }
        return results;
    }

    /**
     * Store specific data row (record). 
     * @param appId
     * @param version
     * @param formDefId
     * @param rows
     * @param primaryKeyValue
     * @return 
     */
    public FormRowSet storeFormData(String appId, String version, String formDefId, FormRowSet rows,
            String primaryKeyValue) {
        FormRowSet results = null;
        Form form = viewDataForm(appId, version, formDefId, null, null, null, null, null, null);
        if (form != null) {
            results = storeFormData(form, rows, primaryKeyValue);
        }
        return results;
    }

    /**
     * Store specific data row (record) for a form. 
     * @param form
     * @param rows
     * @param primaryKeyValue For single-row data. If null, a UUID will be generated. For multi-row data, this value is not used.
     * @return
     */
    public FormRowSet storeFormData(Form form, FormRowSet rows, String primaryKeyValue) {
        if (form != null) {
            String formDefId = form.getPropertyString(FormUtil.PROPERTY_ID);
            String tableName = form.getPropertyString(FormUtil.PROPERTY_TABLE_NAME);

            return storeFormData(formDefId, tableName, rows, primaryKeyValue);
        }
        return null;
    }

    /**
     * Store specific data row (record) for a form. 
     * @param formDefId
     * @param tableName
     * @param rows
     * @param primaryKeyValue For single-row data. If null, a UUID will be generated. For multi-row data, this value is not used.
     * @return
     */
    public FormRowSet storeFormData(String formDefId, String tableName, FormRowSet rows, String primaryKeyValue) {
        FormRowSet results = null;
        if (formDefId != null && tableName != null && rows != null && !rows.isEmpty()) {

            // determine rows to store
            results = new FormRowSet();
            if (!rows.isMultiRow()) {
                results.add(rows.get(0));
            } else {
                primaryKeyValue = null;
                results.addAll(rows);
            }

            // iterate through rows
            for (int i = 0; i < results.size(); i++) {
                FormRow row = results.get(i);
                String rowPrimaryKeyValue = row.getId();

                // set id
                if (rowPrimaryKeyValue == null || rowPrimaryKeyValue.trim().length() == 0) {
                    rowPrimaryKeyValue = primaryKeyValue;
                }
                if (rowPrimaryKeyValue == null || rowPrimaryKeyValue.trim().length() == 0) {
                    // no primary key value specified, generate new primary key value
                    rowPrimaryKeyValue = UuidGenerator.getInstance().getUuid();
                }
                row.setId(rowPrimaryKeyValue);
                if (!rows.isMultiRow() && (primaryKeyValue == null || primaryKeyValue.trim().isEmpty())) {
                    primaryKeyValue = rowPrimaryKeyValue;
                }

                // set meta data
                Date currentDate = new Date();
                String currentUsername = WorkflowUtil.getCurrentUsername();

                row.setDateModified(currentDate);
                row.setModifiedBy(currentUsername);

                Date dateCreated = null;
                String createdBy = null;
                Boolean deleted = null;

                FormRowSet loadedRow = loadFormDataWithoutTransaction(formDefId, tableName, rowPrimaryKeyValue);
                if (loadedRow != null && loadedRow.iterator().hasNext()) {
                    dateCreated = loadedRow.iterator().next().getDateCreated();
                    createdBy = loadedRow.iterator().next().getCreatedBy();
                    deleted = loadedRow.iterator().next().getDeleted();
                }
                if (dateCreated == null) {
                    dateCreated = currentDate;
                    createdBy = currentUsername;
                }

                if (row.getDeleted() == null) {
                    row.setDeleted(deleted);
                }

                row.setDateCreated(dateCreated);
                row.setCreatedBy(createdBy);
            }

            // update DB schema
            formDataDao.updateSchema(formDefId, tableName, rows);

            FileUtil.checkAndUpdateFileName(results, tableName, primaryKeyValue);

            // save data
            formDataDao.saveOrUpdate(formDefId, tableName, results);
            LogUtil.info(getClass().getName(), "  -- Saved form data row [" + primaryKeyValue + "] for form ["
                    + formDefId + "] from table [" + tableName + "]");

            FileUtil.storeFileFromFormRowSet(results, tableName, primaryKeyValue);
        }
        return results;
    }

    /**
     * Get version of published app
     * @param appId
     * @return
     */
    public Long getPublishedVersion(String appId) {
        try {
            return appDefinitionDao.getPublishedVersion(appId);
        } catch (Exception e) {
        }
        return null;
    }

    /**
     * Publish a specific app version
     * @param appId
     * @param version set null to publish the latest version
     * @return the published AppDefinition, null if not found
     */
    public AppDefinition publishApp(String appId, String version) {
        // unset previous published version
        Long previousVersion = getPublishedVersion(appId);
        if (previousVersion != null && previousVersion != 0) {
            AppDefinition prevAppDef = appDefinitionDao.loadVersion(appId, previousVersion);
            prevAppDef.setPublished(Boolean.FALSE);
            appDefinitionDao.saveOrUpdate(prevAppDef);
        }
        // set published version
        AppDefinition appDef = null;
        Long versionLong = AppUtil.convertVersionToLong(version);
        if (versionLong == null) {
            // load latest
            appDef = appDefinitionDao.loadById(appId);
        } else {
            // load specific version
            appDef = appDefinitionDao.loadVersion(appId, versionLong);
        }
        if (appDef != null) {
            appDef.setPublished(Boolean.TRUE);
            appDefinitionDao.saveOrUpdate(appDef);
        }
        return appDef;
    }

    /**
     * Publish an app
     * @param appId
     * @return the unpublished AppDefinition, null if not found
     */
    public AppDefinition unpublishApp(String appId) {
        AppDefinition prevAppDef = null;
        // unset previous published version
        Long previousVersion = getPublishedVersion(appId);
        if (previousVersion != null && previousVersion != 0) {
            prevAppDef = appDefinitionDao.loadVersion(appId, previousVersion);
            prevAppDef.setPublished(Boolean.FALSE);
            appDefinitionDao.saveOrUpdate(prevAppDef);
        }
        return prevAppDef;
    }

    /**
     * Get App definition XML
     * @param appId
     * @param version
     * @return
     */
    public byte[] getAppDefinitionXml(String appId, Long version) {
        byte[] appDefinitionXml = null;

        ByteArrayOutputStream baos = null;

        TimeZone current = TimeZone.getDefault();
        TimeZone.setDefault(TimeZone.getTimeZone("GMT 0"));

        try {
            baos = new ByteArrayOutputStream();

            AppDefinition appDef = getAppDefinition(appId, Long.toString(version));

            Serializer serializer = new Persister();
            serializer.write(appDef, baos);

            appDefinitionXml = baos.toByteArray();
            baos.close();

            String value = new String(appDefinitionXml, "UTF-8");
            value = value.replaceAll("org\\.hibernate\\.collection\\.PersistentBag", "java.util.ArrayList");
            value = value.replaceAll("org\\.hibernate\\.collection\\.PersistentMap", "java.util.HashMap");

            //for backward compatible
            value = commentTag(value, "disableSaveAsDraft");
            value = commentTag(value, "meta");
            if (value.indexOf("<formDefinitionList>") > 0) {
                int start = value.indexOf("<formDefinitionList>");
                int end = value.indexOf("</formDefinitionList>");
                value = value.substring(0, start - 1) + commentTag(value.substring(start, end - 1), "description")
                        + value.substring(end);
            }
            int afterMessagePos = 14;
            if (value.indexOf("<messageList/>") > 0) {
                afterMessagePos += value.indexOf("<messageList/>");
            } else {
                afterMessagePos += value.indexOf("</messageList>");
            }
            value = value.substring(0, afterMessagePos)
                    + commentTag(value.substring(afterMessagePos + 1), "description");
            return value.getBytes("UTF-8");
        } catch (Exception ex) {
            LogUtil.error(getClass().getName(), ex, "");
        } finally {
            if (baos != null) {
                try {
                    baos.close();
                } catch (Exception e) {
                    LogUtil.error(getClass().getName(), e, "");
                }
            }

            TimeZone.setDefault(current);
        }
        return null;
    }

    private String commentTag(String content, String tag) {
        Pattern pattern = Pattern.compile("<" + tag + ">([^<])*</" + tag + ">");
        Matcher matcher = pattern.matcher(content);
        Set<String> foundList = new HashSet<String>();
        while (matcher.find()) {
            foundList.add(matcher.group());
        }

        for (String f : foundList) {
            String newf = f;
            newf = newf.replaceAll("-", "&#45;");
            newf = newf.replaceAll("<" + tag + ">", "<!--" + tag + ">");
            newf = newf.replaceAll("</" + tag + ">", "</" + tag + "-->");

            content = content.replaceAll(StringUtil.escapeRegex(f), StringUtil.escapeRegex(newf));
        }

        return content;
    }

    /**
     * Export an app version in ZIP format into an OutputStream
     * @param appId
     * @param version If null, the latest app version will be used.
     * @param output The OutputStream the ZIP content will be streamed into
     * @return Returns the OutputStream object parameter passed in. If null, a ByteArrayOutputStream will be created and returned. 
     * @throws IOException 
     */
    public OutputStream exportApp(String appId, String version, OutputStream output) throws IOException {
        ZipOutputStream zip = null;
        if (output == null) {
            output = new ByteArrayOutputStream();
        }
        try {
            AppDefinition appDef = loadAppDefinition(appId, version);
            if (appDef != null && output != null) {
                zip = new ZipOutputStream(output);

                // write zip entry for app XML
                byte[] data = getAppDefinitionXml(appId, appDef.getVersion());
                zip.putNextEntry(new ZipEntry("appDefinition.xml"));
                zip.write(data);
                zip.closeEntry();

                // write zip entry for app XML
                PackageDefinition packageDef = appDef.getPackageDefinition();
                if (packageDef != null) {
                    byte[] xpdl = workflowManager.getPackageContent(packageDef.getId(),
                            packageDef.getVersion().toString());
                    zip.putNextEntry(new ZipEntry("package.xpdl"));
                    zip.write(xpdl);
                    zip.closeEntry();
                }

                // finish the zip
                zip.finish();
            }
        } catch (Exception ex) {
            LogUtil.error(getClass().getName(), ex, "");
        } finally {
            if (zip != null) {
                zip.flush();
            }
        }
        return output;
    }

    /**
     * Import app from zip file
     * @param zip
     * @return
     */
    @Transactional
    public AppDefinition importApp(byte[] zip) throws ImportAppException {
        TimeZone current = TimeZone.getDefault();
        TimeZone.setDefault(TimeZone.getTimeZone("GMT 0"));

        try {
            byte[] appData = getAppDataXmlFromZip(zip);
            byte[] xpdl = getXpdlFromZip(zip);

            //for backward compatible
            Map<String, String> replacement = new HashMap<String, String>();
            replacement.put("<!--disableSaveAsDraft>", "<disableSaveAsDraft>");
            replacement.put("</disableSaveAsDraft-->", "</disableSaveAsDraft>");
            replacement.put("<!--description>", "<description>");
            replacement.put("</description-->", "</description>");
            replacement.put("<!--meta>", "<meta>");
            replacement.put("</meta-->", "</meta>");
            appData = StringUtil.searchAndReplaceByteContent(appData, replacement);

            Serializer serializer = new Persister();
            AppDefinition appDef = serializer.read(AppDefinition.class, new ByteArrayInputStream(appData), false);

            long appVersion = appDefinitionDao.getLatestVersion(appDef.getAppId());

            //Store appDef
            long newAppVersion = appVersion + 1;
            AppDefinition newAppDef = importAppDefinition(appDef, newAppVersion, xpdl);

            importPlugins(zip);

            return newAppDef;
        } catch (ImportAppException e) {
            throw e;
        } catch (Exception e) {
            LogUtil.error(getClass().getName(), e, "");
        } finally {
            TimeZone.setDefault(current);
        }
        return null;
    }

    /**
     * Find a form data record id based a field name and value
     * @param appId
     * @param appVersion
     * @param formDefId
     * @param foreignKeyName
     * @param foreignKeyValue
     * @return 
     */
    public String getPrimaryKeyWithForeignKey(String appId, String appVersion, String formDefId,
            String foreignKeyName, String foreignKeyValue) {
        Form form = loadFormByFormDefId(appId, appVersion, formDefId, null, null);

        return formDataDao.findPrimaryKey(form, foreignKeyName, foreignKeyValue);
    }

    /**
     * Update running processes for a package from a version to another.
     * The update is run in a background thread.
     * @param packageId
     * @param fromVersion
     * @param toVersion 
     */
    protected void updateRunningProcesses(final String packageId, final Long fromVersion, final Long toVersion) {
        final String profile = DynamicDataSourceManager.getCurrentProfile();
        final AppDefinition appDef = AppUtil.getCurrentAppDefinition();

        Thread backgroundThread = new Thread(new Runnable() {

            public void run() {
                HostManager.setCurrentProfile(profile);
                AppUtil.setCurrentAppDefinition(appDef);

                LogUtil.info(getClass().getName(), "Updating running processes for " + packageId + " from "
                        + fromVersion + " to " + toVersion);
                Collection<WorkflowProcess> runningProcessList = workflowManager.getRunningProcessList(packageId,
                        null, null, fromVersion.toString(), null, null, 0, null);

                Collection<WorkflowProcess> processes = workflowManager.getProcessList(packageId,
                        toVersion.toString());
                Collection<String> newProcessDefIds = new ArrayList<String>();
                for (WorkflowProcess process : processes) {
                    newProcessDefIds.add(process.getId());
                }

                for (WorkflowProcess process : runningProcessList) {
                    String processId = null;
                    try {
                        processId = process.getInstanceId();
                        String processDefId = process.getId();
                        processDefId = processDefId.replace("#" + fromVersion.toString() + "#",
                                "#" + toVersion.toString() + "#");

                        if (newProcessDefIds.contains(processDefId)) {
                            workflowManager.processCopyFromInstanceId(processId, processDefId, true);
                        } else {
                            workflowManager.processAbort(processId);
                            LogUtil.info(getClass().getName(), "Process Def ID " + processDefId
                                    + " does not exist. Aborted process " + processId + ".");
                        }
                    } catch (Exception e) {
                        LogUtil.error(getClass().getName(), e, "Error updating process " + processId);
                    }
                }
                LogUtil.info(getClass().getName(), "Completed updating running processes for " + packageId
                        + " from " + fromVersion + " to " + toVersion);
            }
        });
        backgroundThread.setDaemon(false);
        backgroundThread.start();
    }

    /**
     * Import an app definition object and XPDL content into the system.
     * @param appDef
     * @param appVersion
     * @param xpdl
     * @return 
     */
    @Transactional
    public AppDefinition importAppDefinition(AppDefinition appDef, Long appVersion, byte[] xpdl)
            throws ImportAppException {
        Boolean overrideEnvVariable = false;
        Boolean overridePluginDefault = false;

        HttpServletRequest request = WorkflowUtil.getHttpServletRequest();
        if (request != null && request.getParameterValues("overrideEnvVariable") != null) {
            overrideEnvVariable = true;
        }
        if (request != null && request.getParameterValues("overridePluginDefault") != null) {
            overridePluginDefault = true;
        }

        //fix app id letter case issue during import
        AppDefinition orgAppDef = loadAppDefinition(appDef.getAppId(), null);
        String appId = appDef.getAppId();
        if (orgAppDef != null) {
            appId = orgAppDef.getAppId();
        }

        LogUtil.debug(getClass().getName(), "Importing app " + appDef.getId());
        AppDefinition newAppDef = new AppDefinition();
        newAppDef.setAppId(appId);
        newAppDef.setVersion(appVersion);
        newAppDef.setId(appId);
        newAppDef.setName(appDef.getName());
        newAppDef.setPublished(Boolean.FALSE);
        Date currentDate = new Date();
        newAppDef.setDateCreated(currentDate);
        newAppDef.setDateModified(currentDate);
        newAppDef.setLicense(appDef.getLicense());
        newAppDef.setDescription(appDef.getDescription());
        newAppDef.setMeta(appDef.getMeta());
        appDefinitionDao.saveOrUpdate(newAppDef);

        if (appDef.getFormDefinitionList() != null) {
            for (FormDefinition o : appDef.getFormDefinitionList()) {
                o.setAppDefinition(newAppDef);
                formDefinitionDao.add(o);
            }

            String currentTable = "";
            Collection<String> importedForms = new ArrayList<String>();
            try {
                for (FormDefinition o : appDef.getFormDefinitionList()) {
                    currentTable = o.getTableName();
                    // initialize db table by making a dummy load
                    String dummyKey = "xyz123";
                    formDataDao.loadWithoutTransaction(o.getId(), o.getTableName(), dummyKey);
                    importedForms.add(o.getId());
                    LogUtil.debug(getClass().getName(),
                            "Initialized form " + o.getId() + " with table " + o.getTableName());
                }
            } catch (Exception e) {
                //error creating form data table, rollback
                for (String formId : importedForms) {
                    formDefinitionDao.delete(formId, newAppDef);
                }
                appDefinitionDao.delete(newAppDef);
                String errorMessage = "";
                if (currentTable.length() > 20) {
                    errorMessage = ": " + ResourceBundleUtil.getMessage("form.form.invalidId");
                }
                throw new ImportAppException(ResourceBundleUtil.getMessage("console.app.import.error.createTable",
                        new Object[] { currentTable, errorMessage }), e);
            }
        }

        if (appDef.getDatalistDefinitionList() != null) {
            for (DatalistDefinition o : appDef.getDatalistDefinitionList()) {
                o.setAppDefinition(newAppDef);
                datalistDefinitionDao.add(o);
                LogUtil.debug(getClass().getName(), "Added list " + o.getId());
            }
        }

        if (appDef.getUserviewDefinitionList() != null) {
            for (UserviewDefinition o : appDef.getUserviewDefinitionList()) {
                o.setAppDefinition(newAppDef);
                userviewDefinitionDao.add(o);
                LogUtil.debug(getClass().getName(), "Added userview " + o.getId());
            }
        }

        if (appDef.getEnvironmentVariableList() != null) {
            for (EnvironmentVariable o : appDef.getEnvironmentVariableList()) {
                if (!overrideEnvVariable && orgAppDef != null && orgAppDef.getEnvironmentVariableList() != null) {
                    EnvironmentVariable temp = environmentVariableDao.loadById(o.getId(), orgAppDef);
                    if (temp != null) {
                        o.setValue(temp.getValue());
                    }
                }

                if (o.getValue() == null) {
                    o.setValue("");
                }
                o.setAppDefinition(newAppDef);

                environmentVariableDao.add(o);
            }
        }

        if (appDef.getMessageList() != null) {
            for (Message o : appDef.getMessageList()) {
                o.setAppDefinition(newAppDef);
                messageDao.add(o);
            }
        }

        if (appDef.getPluginDefaultPropertiesList() != null) {
            for (PluginDefaultProperties o : appDef.getPluginDefaultPropertiesList()) {
                if (!overridePluginDefault && orgAppDef != null
                        && orgAppDef.getPluginDefaultPropertiesList() != null) {
                    PluginDefaultProperties temp = pluginDefaultPropertiesDao.loadById(o.getId(), orgAppDef);
                    if (temp != null) {
                        o.setPluginProperties(temp.getPluginProperties());
                    }
                }

                o.setAppDefinition(newAppDef);
                pluginDefaultPropertiesDao.add(o);
            }
        }

        try {
            if (xpdl != null) {
                PackageDefinition oldPackageDef = appDef.getPackageDefinition();

                //deploy package
                PackageDefinition packageDef = deployWorkflowPackage(newAppDef.getAppId(),
                        newAppDef.getVersion().toString(), xpdl, false);

                if (packageDef != null) {
                    if (oldPackageDef != null) {
                        if (oldPackageDef.getPackageActivityFormMap() != null) {
                            for (@SuppressWarnings("rawtypes")
                            Entry e : oldPackageDef.getPackageActivityFormMap().entrySet()) {
                                PackageActivityForm form = (PackageActivityForm) e.getValue();
                                form.setPackageDefinition(packageDef);
                                packageDefinitionDao.addAppActivityForm(newAppDef.getAppId(), appVersion, form);
                            }
                        }

                        if (oldPackageDef.getPackageActivityPluginMap() != null) {
                            for (@SuppressWarnings("rawtypes")
                            Entry e : oldPackageDef.getPackageActivityPluginMap().entrySet()) {
                                PackageActivityPlugin plugin = (PackageActivityPlugin) e.getValue();
                                plugin.setPackageDefinition(packageDef);
                                packageDefinitionDao.addAppActivityPlugin(newAppDef.getAppId(), appVersion, plugin);
                            }
                        }

                        if (oldPackageDef.getPackageParticipantMap() != null) {
                            for (@SuppressWarnings("rawtypes")
                            Entry e : oldPackageDef.getPackageParticipantMap().entrySet()) {
                                PackageParticipant participant = (PackageParticipant) e.getValue();
                                participant.setPackageDefinition(packageDef);
                                packageDefinitionDao.addAppParticipant(newAppDef.getAppId(), appVersion,
                                        participant);
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            LogUtil.error(getClass().getName(), e, "Error deploying package for " + appDef.getAppId());
        }

        // reload app from DB
        newAppDef = loadAppDefinition(newAppDef.getAppId(), newAppDef.getVersion().toString());
        LogUtil.debug(getClass().getName(),
                "Finished importing app " + newAppDef.getId() + " version " + newAppDef.getVersion());

        return newAppDef;
    }

    /**
     * Import plugins (JAR) from within a zip content.
     * @param zip
     * @throws Exception 
     */
    public void importPlugins(byte[] zip) throws Exception {
        ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zip));
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        ZipEntry entry = null;

        while ((entry = in.getNextEntry()) != null) {
            if (entry.getName().endsWith(".jar")) {
                int length;
                byte[] temp = new byte[1024];
                while ((length = in.read(temp, 0, 1024)) != -1) {
                    out.write(temp, 0, length);
                }

                pluginManager.upload(entry.getName(), new ByteArrayInputStream(out.toByteArray()));
            }
            out.flush();
            out.close();
        }
        in.close();
    }

    /**
     * Reads app XML from zip content.
     * @param zip
     * @return 
     * @throws java.lang.Exception 
     */
    public byte[] getAppDataXmlFromZip(byte[] zip) throws Exception {
        ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zip));
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        ZipEntry entry = null;

        while ((entry = in.getNextEntry()) != null) {
            if (entry.getName().contains("appDefinition.xml")) {
                int length;
                byte[] temp = new byte[1024];
                while ((length = in.read(temp, 0, 1024)) != -1) {
                    out.write(temp, 0, length);
                }

                return out.toByteArray();
            }
            out.flush();
            out.close();
        }
        in.close();

        return null;
    }

    /**
     * Reads XPDL from zip content.
     * @param zip
     * @return
     * @throws Exception 
     */
    public byte[] getXpdlFromZip(byte[] zip) throws Exception {
        ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zip));
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        ZipEntry entry = null;

        while ((entry = in.getNextEntry()) != null) {
            if (entry.getName().endsWith(".xpdl")) {
                int length;
                byte[] temp = new byte[1024];
                while ((length = in.read(temp, 0, 1024)) != -1) {
                    out.write(temp, 0, length);
                }

                return out.toByteArray();
            }
            out.flush();
            out.close();
        }
        in.close();

        return null;
    }

    /**
     * Get table name of a form
     * @param appId
     * @param appVersion
     * @param formDefId
     * @return 
     */
    public String getFormTableName(String appId, String appVersion, String formDefId) {
        AppDefinition appDef = getAppDefinition(appId, appVersion);
        return getFormTableName(appDef, formDefId);
    }

    /**
     * Get table name of a form
     * @param appDef
     * @param formDefId
     * @return 
     */
    public String getFormTableName(AppDefinition appDef, String formDefId) {
        FormDefinition formDef = formDefinitionDao.loadById(formDefId, appDef);
        if (formDef != null) {
            return formDef.getTableName();
        }
        return null;
    }

    /**
     * Retrieve list of published apps available to the current user
     * @param appId Optional filter by appId
     * @return 
     */
    public Collection<AppDefinition> getPublishedApps(String appId) {
        Collection<AppDefinition> resultAppDefinitionList = getPublishedApps(appId, false, false);
        return resultAppDefinitionList;
    }

    /**
     * Retrieve list of published apps available to the current user. Overloaded
     * to additionally filter by mobile view support.
     * @param appId Optional filter by appId
     * @param mobileView
     * @param mobileCache
     * @return
     */
    public Collection<AppDefinition> getPublishedApps(String appId, boolean mobileView, boolean mobileCache) {
        Collection<AppDefinition> resultAppDefinitionList = new ArrayList<AppDefinition>();
        Collection<AppDefinition> appDefinitionList;
        if (appId == null || appId.trim().isEmpty()) {
            // get list of published apps.
            appDefinitionList = appDefinitionDao.findPublishedApps("name", Boolean.FALSE, null, null);
        } else {
            // get specific app
            appDefinitionList = new ArrayList<AppDefinition>();
            Long version = getPublishedVersion(appId);
            if (version != null && version > 0) {
                AppDefinition appDef = getAppDefinition(appId, version.toString());
                if (appDef != null) {
                    appDefinitionList.add(appDef);
                }
            }
        }

        // filter based on availability and permission of userviews to run.
        for (Iterator<AppDefinition> i = appDefinitionList.iterator(); i.hasNext();) {
            AppDefinition appDef = i.next();

            try {
                Collection<UserviewDefinition> uvDefList = appDef.getUserviewDefinitionList();
                Collection<UserviewDefinition> newUvDefList = new ArrayList<UserviewDefinition>();

                for (UserviewDefinition uvDef : uvDefList) {
                    UserviewSetting userviewSetting = userviewService.getUserviewSetting(appDef, uvDef.getJson());
                    if (userviewSetting != null
                            && (userviewSetting.getPermission() == null || (userviewSetting.getPermission() != null
                                    && userviewSetting.getPermission().isAuthorize()))
                            && (!mobileView || !"true".equals(userviewSetting.getProperty("mobileViewDisabled")))
                            && (!mobileCache || "true".equals(userviewSetting.getProperty("mobileCacheEnabled")))) {
                        newUvDefList.add(uvDef);
                    }
                }

                if (!newUvDefList.isEmpty()) {
                    AppDefinition tempAppDef = new AppDefinition();
                    tempAppDef.setAppId(appDef.getId());
                    tempAppDef.setVersion(appDef.getVersion());
                    tempAppDef.setName(appDef.getName());
                    tempAppDef.setUserviewDefinitionList(newUvDefList);
                    resultAppDefinitionList.add(tempAppDef);
                }
            } catch (Exception e) {
                LogUtil.error(AppServiceImpl.class.getName(), e,
                        "Error generating userviews for  " + appDef.getId());
            }
        }
        return resultAppDefinitionList;
    }

    /**
     * Retrieve list of published processes available to the current user
     * @param appId Optional filter by appId
     * @return 
     */
    public Map<AppDefinition, Collection<WorkflowProcess>> getPublishedProcesses(String appId) {
        @SuppressWarnings("unchecked")
        Map<AppDefinition, Collection<WorkflowProcess>> appProcessMap = new ListOrderedMap();

        // get list of published apps.
        Collection<AppDefinition> appDefinitionList = null;
        if (appId == null || appId.trim().isEmpty()) {
            // get list of published apps.
            appDefinitionList = appDefinitionDao.findPublishedApps("name", Boolean.FALSE, null, null);
        } else {
            // get specific app
            appDefinitionList = new ArrayList<AppDefinition>();
            Long version = getPublishedVersion(appId);
            if (version != null && version > 0) {
                AppDefinition appDef = getAppDefinition(appId, version.toString());
                if (appDef != null) {
                    appDefinitionList.add(appDef);
                }
            }
        }

        // filter based on availability of processes to run.
        for (Iterator<AppDefinition> i = appDefinitionList.iterator(); i.hasNext();) {
            AppDefinition appDef = i.next();
            Collection<PackageDefinition> packageDefList = appDef.getPackageDefinitionList();
            if (packageDefList != null && !packageDefList.isEmpty()) {
                PackageDefinition packageDef = packageDefList.iterator().next();
                Collection<WorkflowProcess> processList = workflowManager.getProcessList(packageDef.getId(),
                        packageDef.getVersion().toString());

                Collection<WorkflowProcess> processListWithPermission = new ArrayList<WorkflowProcess>();

                for (WorkflowProcess process : processList) {
                    if (workflowManager.isUserInWhiteList(process.getId())) {
                        processListWithPermission.add(process);
                    }
                }

                if (!processListWithPermission.isEmpty()) {
                    appProcessMap.put(appDef, processListWithPermission);
                }
            } else {
                i.remove();
            }
        }

        return appProcessMap;
    }

    /**
     * Generate Message Bundle PO file to OutputStream
     * @param appId
     * @param version
     * @param locale
     * @param output
     * @throws IOException 
     */
    public void generatePO(String appId, String version, String locale, OutputStream output) throws IOException {
        Writer writer = new OutputStreamWriter(output, "UTF-8");

        try {
            writer.append("# This file was generated by Kecak Workflow\r\n");
            writer.append("# http://www.kecak.net\r\n");
            writer.append("msgid \"\"\r\n");
            writer.append("msgstr \"\"\r\n");
            writer.append("\"Content-Type: text/plain; charset=utf-8\\n\"\r\n");
            writer.append("\"Content-Transfer-Encoding: 8bit\\n\"\r\n");
            writer.append("\"Project-Id-Version: " + appId + "\\n\"\r\n");
            writer.append("\"POT-Creation-Date: \\n\"\r\n");
            writer.append("\"PO-Revision-Date: \\n\"\r\n");
            writer.append("\"Last-Translator: \\n\"\r\n");
            writer.append("\"Language-Team: \\n\"\r\n");
            writer.append("\"Language: " + locale + "\\n\"\r\n");
            writer.append("\"MIME-Version: 1.0\\n\"\r\n\r\n");

            Map<String, String> messages = getMessages(appId, version, locale);
            for (String key : messages.keySet()) {
                String value = messages.get(key);
                writer.append("msgid \"" + key + "\"\r\n");
                writer.append("msgstr \"" + value + "\"\r\n");
            }
        } catch (Exception e) {
            LogUtil.error(AppServiceImpl.class.getName(), e, "Error generating PO file for " + appId);
        } finally {
            writer.flush();
            writer.close();
        }
    }

    /**
     * Import Messages from a PO file
     * @param appId
     * @param version
     * @param locale
     * @param multipartFile
     * @throws IOException 
     */
    @Transactional
    public void importPO(String appId, String version, String locale, MultipartFile multipartFile)
            throws IOException {
        InputStream inputStream = multipartFile.getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
        AppDefinition appDef = getAppDefinition(appId, version);

        String line = null, key = null, translated = null;
        try {
            while ((line = bufferedReader.readLine()) != null) {
                if (line.startsWith("\"Language: ") && line.endsWith("\\n\"")) {
                    locale = line.substring(11, line.length() - 3);
                } else if (line.startsWith("msgid \"") && !line.equals("msgid \"\"")) {
                    key = line.substring(7, line.length() - 1);
                    translated = null;
                } else if (line.startsWith("msgstr \"")) {
                    translated = line.substring(8, line.length() - 1);
                }

                if (key != null && translated != null) {
                    Message message = messageDao.loadByMessageKey(key, locale, appDef);
                    if (message == null) {
                        message = new Message();
                        message.setLocale(locale);
                        message.setMessageKey(key);
                        message.setAppDefinition(appDef);
                        message.setMessage(translated);
                        messageDao.add(message);
                    } else {
                        message.setMessage(translated);
                        messageDao.update(message);
                    }
                    key = null;
                    translated = null;
                }
            }
        } catch (Exception e) {
        } finally {
            bufferedReader.close();
            inputStream.close();
        }
    }

    /**
     * Retrieve all apps without check for permission
     * @return 
     */
    public Collection<AppDefinition> getUnprotectedAppList() {
        Collection<AppDefinition> appDefinitionList = appDefinitionDao.findLatestVersions(null, null, null, "name",
                false, null, null);
        return appDefinitionList;
    }

    protected Map<String, String> getMessages(String appId, String version, String locale) {
        Map<String, String> messages = new HashMap<String, String>();

        AppDefinition appDef = getAppDefinition(appId, version);
        if (appDef != null) {
            Collection<DatalistDefinition> dList = appDef.getDatalistDefinitionList();
            if (dList != null && !dList.isEmpty()) {
                for (DatalistDefinition def : dList) {
                    messages.putAll(getMessages(def.getJson()));
                }
            }

            Collection<FormDefinition> fList = appDef.getFormDefinitionList();
            if (fList != null && !fList.isEmpty()) {
                for (FormDefinition def : fList) {
                    messages.putAll(getMessages(def.getJson()));
                }
            }

            Collection<UserviewDefinition> uList = appDef.getUserviewDefinitionList();
            if (uList != null && !uList.isEmpty()) {
                for (UserviewDefinition def : uList) {
                    messages.putAll(getMessages(def.getJson()));
                }
            }

            PackageDefinition packageDefinition = appDef.getPackageDefinition();
            if (packageDefinition != null) {
                Collection<WorkflowProcess> processList = workflowManager.getProcessList(appId,
                        packageDefinition.getVersion().toString());
                if (processList != null && !processList.isEmpty()) {
                    for (WorkflowProcess wp : processList) {
                        //get activity list
                        Collection<WorkflowActivity> activityList = workflowManager
                                .getProcessActivityDefinitionList(wp.getId());
                        if (activityList != null && !activityList.isEmpty()) {
                            for (WorkflowActivity activity : activityList) {
                                messages.putAll(getMessages(activity.getName()));
                            }
                        }
                    }
                }
            }

            Collection<Message> mList = messageDao.getMessageList(null, locale, appDef, null, null, null, null);
            if (mList != null && !mList.isEmpty()) {
                for (Message m : mList) {
                    messages.put(m.getMessageKey(), m.getMessage());
                }
            }
        }

        return messages;
    }

    protected Map<String, String> getMessages(String content) {
        Map<String, String> messages = new HashMap<String, String>();

        // check for hash # to avoid unnecessary processing
        if (!AppUtil.containsHashVariable(content)) {
            return messages;
        }

        //parse content
        if (content != null) {
            Pattern pattern = Pattern.compile("#i18n\\.([^#^\"]*)#");
            Matcher matcher = pattern.matcher(content);
            while (matcher.find()) {
                messages.put(matcher.group(1), "");
            }
        }

        return messages;
    }
}