org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue.java Source code

Java tutorial

Introduction

Here is the source code for org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue.java

Source

/**
 * Copyright 2005-2014 The Kuali Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.opensource.org/licenses/ecl2.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kuali.rice.kew.routeheader;

import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.kuali.rice.core.api.exception.RiceRuntimeException;
import org.kuali.rice.kew.actionitem.ActionItem;
import org.kuali.rice.kew.actionlist.CustomActionListAttribute;
import org.kuali.rice.kew.actionlist.DefaultCustomActionListAttribute;
import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
import org.kuali.rice.kew.actionrequest.ActionRequestValue;
import org.kuali.rice.kew.actiontaken.ActionTakenValue;
import org.kuali.rice.kew.api.KewApiConstants;
import org.kuali.rice.kew.api.WorkflowRuntimeException;
import org.kuali.rice.kew.api.action.ActionType;
import org.kuali.rice.kew.api.document.Document;
import org.kuali.rice.kew.api.document.DocumentContract;
import org.kuali.rice.kew.api.document.DocumentStatus;
import org.kuali.rice.kew.api.document.DocumentUpdate;
import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
import org.kuali.rice.kew.api.exception.ResourceUnavailableException;
import org.kuali.rice.kew.api.exception.WorkflowException;
import org.kuali.rice.kew.api.util.CodeTranslator;
import org.kuali.rice.kew.docsearch.DocumentSearchCriteriaEbo;
import org.kuali.rice.kew.doctype.ApplicationDocumentStatus;
import org.kuali.rice.kew.doctype.DocumentTypePolicy;
import org.kuali.rice.kew.doctype.bo.DocumentType;
import org.kuali.rice.kew.engine.CompatUtils;
import org.kuali.rice.kew.engine.node.Branch;
import org.kuali.rice.kew.engine.node.BranchState;
import org.kuali.rice.kew.engine.node.RouteNode;
import org.kuali.rice.kew.engine.node.RouteNodeInstance;
import org.kuali.rice.kew.mail.CustomEmailAttribute;
import org.kuali.rice.kew.mail.CustomEmailAttributeImpl;
import org.kuali.rice.kew.notes.CustomNoteAttribute;
import org.kuali.rice.kew.notes.CustomNoteAttributeImpl;
import org.kuali.rice.kew.notes.Note;
import org.kuali.rice.kew.quicklinks.dao.impl.QuickLinksDAOJpa;
import org.kuali.rice.kew.routeheader.dao.impl.DocumentRouteHeaderDAOJpa;
import org.kuali.rice.kew.service.KEWServiceLocator;
import org.kuali.rice.kim.api.identity.principal.Principal;
import org.kuali.rice.krad.bo.DataObjectBase;
import org.kuali.rice.krad.data.jpa.PortableSequenceGenerator;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.NamedAttributeNode;
import javax.persistence.NamedEntityGraph;
import javax.persistence.NamedEntityGraphs;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A document within KEW.  A document effectively represents a process that moves through
 * the workflow engine.  It is created from a particular {@link DocumentType} and follows
 * the route path defined by that DocumentType.
 *
 * <p>During a document's lifecycle it progresses through a series of statuses, starting
 * with INITIATED and moving to one of the terminal states (such as FINAL, CANCELED, etc).
 * The list of status on a document are defined in the {@link KewApiConstants} class and
 * include the constants starting with "ROUTE_HEADER_" and ending with "_CD".
 *
 * <p>Associated with the document is the document content.  The document content is XML
 * which represents the content of that document.  This XML content is typically used
 * to make routing decisions for the document.
 *
 * <p>A document has associated with it a set of {@link ActionRequestValue} object and
 * {@link ActionTakenValue} objects.  Action Requests represent requests for user
 * action (such as Approve, Acknowledge, etc).  Action Takens represent action that
 * users have performed on the document, such as approvals or cancelling of the document.
 *
 * <p>The instantiated route path of a document is defined by it's graph of
 * {@link RouteNodeInstance} objects.  The path starts at the initial node of the document
 * and progresses from there following the next nodes of each node instance.  The current
 * active nodes on the document are defined by the "active" flag on the node instance
 * where are not marked as "complete".
 *
 * @see DocumentType
 * @see ActionRequestValue
 * @see ActionItem
 * @see ActionTakenValue
 * @see RouteNodeInstance
 * @see KewApiConstants
 *
 * @author Kuali Rice Team (rice.collab@kuali.org)
 */
@Entity
@Table(name = "KREW_DOC_HDR_T")
@NamedQueries({
        @NamedQuery(name = QuickLinksDAOJpa.FIND_WATCHED_DOCUMENTS_BY_INITIATOR_WORKFLOW_ID_NAME, query = QuickLinksDAOJpa.FIND_WATCHED_DOCUMENTS_BY_INITIATOR_WORKFLOW_ID_QUERY),
        @NamedQuery(name = DocumentRouteHeaderDAOJpa.GET_APP_DOC_ID_NAME, query = DocumentRouteHeaderDAOJpa.GET_APP_DOC_ID_QUERY),
        @NamedQuery(name = DocumentRouteHeaderDAOJpa.GET_APP_DOC_STATUS_NAME, query = DocumentRouteHeaderDAOJpa.GET_APP_DOC_STATUS_QUERY),
        @NamedQuery(name = DocumentRouteHeaderDAOJpa.GET_DOCUMENT_HEADERS_NAME, query = DocumentRouteHeaderDAOJpa.GET_DOCUMENT_HEADERS_QUERY),
        @NamedQuery(name = DocumentRouteHeaderDAOJpa.GET_DOCUMENT_STATUS_NAME, query = DocumentRouteHeaderDAOJpa.GET_DOCUMENT_STATUS_QUERY),
        @NamedQuery(name = DocumentRouteHeaderDAOJpa.GET_DOCUMENT_ID_BY_DOC_TYPE_APP_ID_NAME, query = DocumentRouteHeaderDAOJpa.GET_DOCUMENT_ID_BY_DOC_TYPE_APP_ID_QUERY) })
@NamedEntityGraphs({
        @NamedEntityGraph(name = "DocumentRouteHeaderValue.ActionListAttributesOnly", attributeNodes = {
                @NamedAttributeNode("approvedDate"), @NamedAttributeNode("createDate"),
                @NamedAttributeNode("docRouteStatus"), @NamedAttributeNode("initiatorWorkflowId"),
                @NamedAttributeNode("appDocStatus"), @NamedAttributeNode("documentId"),
                @NamedAttributeNode("docRouteLevel"), @NamedAttributeNode("documentTypeId"),
                @NamedAttributeNode("docVersion") }) })
public class DocumentRouteHeaderValue extends DataObjectBase
        implements DocumentContract, DocumentSearchCriteriaEbo {

    private static final long serialVersionUID = -4700736340527913220L;
    private static final Logger LOG = Logger.getLogger(DocumentRouteHeaderValue.class);

    private static final String TERMINAL = "";
    private static final boolean FINAL_STATE = true;
    protected static final HashMap<String, String> legalActions;
    protected static final HashMap<String, String> stateTransitionMap;
    static {
        stateTransitionMap = new HashMap<String, String>();
        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_INITIATED_CD, KewApiConstants.ROUTE_HEADER_SAVED_CD
                + KewApiConstants.ROUTE_HEADER_ENROUTE_CD + KewApiConstants.ROUTE_HEADER_CANCEL_CD);

        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_SAVED_CD,
                KewApiConstants.ROUTE_HEADER_SAVED_CD + KewApiConstants.ROUTE_HEADER_ENROUTE_CD
                        + KewApiConstants.ROUTE_HEADER_CANCEL_CD + KewApiConstants.ROUTE_HEADER_PROCESSED_CD);

        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_ENROUTE_CD,
                KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD + KewApiConstants.ROUTE_HEADER_CANCEL_CD
                        + KewApiConstants.ROUTE_HEADER_PROCESSED_CD + KewApiConstants.ROUTE_HEADER_EXCEPTION_CD
                        + KewApiConstants.ROUTE_HEADER_SAVED_CD + DocumentStatus.RECALLED.getCode());
        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD, TERMINAL);
        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_CANCEL_CD, TERMINAL);
        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_FINAL_CD, TERMINAL);
        stateTransitionMap.put(DocumentStatus.RECALLED.getCode(), TERMINAL);
        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_EXCEPTION_CD,
                KewApiConstants.ROUTE_HEADER_EXCEPTION_CD + KewApiConstants.ROUTE_HEADER_ENROUTE_CD
                        + KewApiConstants.ROUTE_HEADER_CANCEL_CD + KewApiConstants.ROUTE_HEADER_PROCESSED_CD
                        + KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD + KewApiConstants.ROUTE_HEADER_SAVED_CD);
        stateTransitionMap.put(KewApiConstants.ROUTE_HEADER_PROCESSED_CD,
                KewApiConstants.ROUTE_HEADER_FINAL_CD + KewApiConstants.ROUTE_HEADER_PROCESSED_CD);

        legalActions = new HashMap<String, String>();
        legalActions.put(KewApiConstants.ROUTE_HEADER_INITIATED_CD,
                KewApiConstants.ACTION_TAKEN_FYI_CD + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD
                        + KewApiConstants.ACTION_TAKEN_SAVED_CD + KewApiConstants.ACTION_TAKEN_COMPLETED_CD
                        + KewApiConstants.ACTION_TAKEN_ROUTED_CD + KewApiConstants.ACTION_TAKEN_CANCELED_CD
                        + KewApiConstants.ACTION_TAKEN_ADHOC_CD + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD
                        + KewApiConstants.ACTION_TAKEN_BLANKET_APPROVE_CD + KewApiConstants.ACTION_TAKEN_MOVE_CD);
        legalActions.put(KewApiConstants.ROUTE_HEADER_SAVED_CD,
                KewApiConstants.ACTION_TAKEN_FYI_CD + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD
                        + KewApiConstants.ACTION_TAKEN_SAVED_CD + KewApiConstants.ACTION_TAKEN_COMPLETED_CD
                        + KewApiConstants.ACTION_TAKEN_ROUTED_CD + KewApiConstants.ACTION_TAKEN_APPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_CANCELED_CD + KewApiConstants.ACTION_TAKEN_ADHOC_CD
                        + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD
                        + KewApiConstants.ACTION_TAKEN_BLANKET_APPROVE_CD + KewApiConstants.ACTION_TAKEN_MOVE_CD);
        /* ACTION_TAKEN_ROUTED_CD not included in enroute state
         * ACTION_TAKEN_SAVED_CD removed as of version 2.4
         */
        legalActions.put(KewApiConstants.ROUTE_HEADER_ENROUTE_CD,
                /*KewApiConstants.ACTION_TAKEN_SAVED_CD + KewApiConstants.ACTION_TAKEN_ROUTED_CD + */KewApiConstants.ACTION_TAKEN_APPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD + KewApiConstants.ACTION_TAKEN_FYI_CD
                        + KewApiConstants.ACTION_TAKEN_ADHOC_CD + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD
                        + KewApiConstants.ACTION_TAKEN_BLANKET_APPROVE_CD + KewApiConstants.ACTION_TAKEN_CANCELED_CD
                        + KewApiConstants.ACTION_TAKEN_COMPLETED_CD + KewApiConstants.ACTION_TAKEN_DENIED_CD
                        + KewApiConstants.ACTION_TAKEN_SU_APPROVED_CD + KewApiConstants.ACTION_TAKEN_SU_CANCELED_CD
                        + KewApiConstants.ACTION_TAKEN_SU_DISAPPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_SU_ROUTE_LEVEL_APPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD
                        + KewApiConstants.ACTION_TAKEN_SU_RETURNED_TO_PREVIOUS_CD
                        + KewApiConstants.ACTION_TAKEN_MOVE_CD + ActionType.RECALL.getCode());
        /* ACTION_TAKEN_ROUTED_CD not included in exception state
         * ACTION_TAKEN_SAVED_CD removed as of version 2.4.2
         */
        legalActions.put(KewApiConstants.ROUTE_HEADER_EXCEPTION_CD,
                /*KewApiConstants.ACTION_TAKEN_SAVED_CD + */KewApiConstants.ACTION_TAKEN_FYI_CD
                        + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD + KewApiConstants.ACTION_TAKEN_ADHOC_CD
                        + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD + KewApiConstants.ACTION_TAKEN_APPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_BLANKET_APPROVE_CD + KewApiConstants.ACTION_TAKEN_CANCELED_CD
                        + KewApiConstants.ACTION_TAKEN_COMPLETED_CD + KewApiConstants.ACTION_TAKEN_DENIED_CD
                        + KewApiConstants.ACTION_TAKEN_SU_APPROVED_CD + KewApiConstants.ACTION_TAKEN_SU_CANCELED_CD
                        + KewApiConstants.ACTION_TAKEN_SU_DISAPPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_SU_ROUTE_LEVEL_APPROVED_CD
                        + KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD
                        + KewApiConstants.ACTION_TAKEN_SU_RETURNED_TO_PREVIOUS_CD
                        + KewApiConstants.ACTION_TAKEN_MOVE_CD);
        legalActions.put(KewApiConstants.ROUTE_HEADER_FINAL_CD, KewApiConstants.ACTION_TAKEN_FYI_CD
                + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD);
        legalActions.put(KewApiConstants.ROUTE_HEADER_CANCEL_CD, KewApiConstants.ACTION_TAKEN_FYI_CD
                + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD);
        legalActions.put(KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD, KewApiConstants.ACTION_TAKEN_FYI_CD
                + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD);
        legalActions.put(KewApiConstants.ROUTE_HEADER_PROCESSED_CD, KewApiConstants.ACTION_TAKEN_FYI_CD
                + KewApiConstants.ACTION_TAKEN_ACKNOWLEDGED_CD + KewApiConstants.ACTION_TAKEN_ADHOC_REVOKED_CD);
        legalActions.put(DocumentStatus.RECALLED.getCode(), TERMINAL);
    }

    public static final String CURRENT_ROUTE_NODE_NAME_DELIMITER = ", ";

    @Id
    @GeneratedValue(generator = "KREW_DOC_HDR_S")
    @PortableSequenceGenerator(name = "KREW_DOC_HDR_S")
    @Column(name = "DOC_HDR_ID", nullable = false)
    private String documentId;

    @Column(name = "DOC_TYP_ID")
    private String documentTypeId;

    @Column(name = "DOC_HDR_STAT_CD", nullable = false)
    private String docRouteStatus;

    @Column(name = "RTE_LVL", nullable = false)
    private Integer docRouteLevel;

    @Column(name = "STAT_MDFN_DT", nullable = false)
    private Timestamp dateModified;

    @Column(name = "CRTE_DT", nullable = false)
    private Timestamp createDate;

    @Column(name = "APRV_DT")
    private Timestamp approvedDate;

    @Column(name = "FNL_DT")
    private Timestamp finalizedDate;

    @Column(name = "TTL")
    private String docTitle;

    @Column(name = "APP_DOC_ID")
    private String appDocId;

    @Column(name = "DOC_VER_NBR", nullable = false)
    private Integer docVersion = new Integer(KewApiConstants.DocumentContentVersions.NODAL);

    @Column(name = "INITR_PRNCPL_ID", nullable = false)
    private String initiatorWorkflowId;

    @Column(name = "RTE_PRNCPL_ID")
    private String routedByUserWorkflowId;

    @Column(name = "RTE_STAT_MDFN_DT")
    private Timestamp routeStatusDate;

    @Column(name = "APP_DOC_STAT")
    private String appDocStatus;

    @Column(name = "APP_DOC_STAT_MDFN_DT")
    private Timestamp appDocStatusDate;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "KREW_INIT_RTE_NODE_INSTN_T", joinColumns = @JoinColumn(name = "DOC_HDR_ID"), inverseJoinColumns = @JoinColumn(name = "RTE_NODE_INSTN_ID"))
    private List<RouteNodeInstance> initialRouteNodeInstances = new ArrayList<RouteNodeInstance>();

    /**
     * The appDocStatusHistory keeps a list of Application Document Status transitions
     * for the document.  It tracks the previous status, the new status, and a timestamp of the
     * transition for each status transition.
     */
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "documentRouteHeaderValue")
    @OrderBy("statusTransitionId ASC")
    private List<DocumentStatusTransition> appDocStatusHistory = new ArrayList<DocumentStatusTransition>();

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "DOC_HDR_ID")
    @OrderBy("noteId ASC")
    private List<Note> notes = new ArrayList<Note>();

    @Transient
    private DocumentRouteHeaderValueContent documentContent;
    @Transient
    private boolean routingReport = false;
    @Transient
    private List<ActionRequestValue> simulatedActionRequests;

    public Principal getInitiatorPrincipal() {
        // if we are running a simulation, there will be no initiator
        if (StringUtils.isBlank(getInitiatorWorkflowId())) {
            return null;
        }
        return KEWServiceLocator.getIdentityHelperService().getPrincipal(getInitiatorWorkflowId());
    }

    public Principal getRoutedByPrincipal() {
        if (getRoutedByUserWorkflowId() == null) {
            return null;
        }
        return KEWServiceLocator.getIdentityHelperService().getPrincipal(getRoutedByUserWorkflowId());
    }

    public String getInitiatorDisplayName() {
        return KEWServiceLocator.getIdentityHelperService().getPerson(getInitiatorWorkflowId()).getName();
    }

    public String getRoutedByDisplayName() {
        return KEWServiceLocator.getIdentityHelperService().getPerson(getRoutedByUserWorkflowId()).getName();
    }

    public String getCurrentRouteLevelName() {
        String name = "Not Found";
        // TODO the isRouteLevelDocument junk can be ripped out
        if (routingReport) {
            name = "Routing Report";
        } else if (CompatUtils.isRouteLevelDocument(this)) {
            int routeLevelInt = getDocRouteLevel().intValue();
            if (LOG.isInfoEnabled()) {
                LOG.info("Getting current route level name for a Route level document: " + routeLevelInt
                        + CURRENT_ROUTE_NODE_NAME_DELIMITER + documentId);
            }
            List<RouteNode> routeLevelNodes = CompatUtils.getRouteLevelCompatibleNodeList(getDocumentType());
            if (LOG.isInfoEnabled()) {
                LOG.info("Route level compatible node list has " + routeLevelNodes.size() + " nodes");
            }
            if (routeLevelInt < routeLevelNodes.size()) {
                name = routeLevelNodes.get(routeLevelInt).getRouteNodeName();
            }
        } else {
            List<String> currentNodeNames = getCurrentNodeNames();
            name = StringUtils.join(currentNodeNames, CURRENT_ROUTE_NODE_NAME_DELIMITER);
        }
        return name;
    }

    public List<String> getCurrentNodeNames() {
        return KEWServiceLocator.getRouteNodeService().getCurrentRouteNodeNames(getDocumentId());
    }

    public String getRouteStatusLabel() {
        return CodeTranslator.getRouteStatusLabel(getDocRouteStatus());
    }

    public String getDocRouteStatusLabel() {
        return CodeTranslator.getRouteStatusLabel(getDocRouteStatus());
    }

    /**
     *
     * This method returns the Document Status Policy for the document type associated with this Route Header.
     * The Document Status Policy denotes whether the KEW Route Status, or the Application Document Status,
     * or both are to be displayed.
     *
     * @return
     */
    public String getDocStatusPolicy() {
        return getDocumentType().getDocumentStatusPolicy().getPolicyStringValue();
    }

    public List<ActionItem> getActionItems() {
        return (List<ActionItem>) KEWServiceLocator.getActionListService().findByDocumentId(documentId);
    }

    public List<ActionTakenValue> getActionsTaken() {
        return KEWServiceLocator.getActionTakenService().findByDocumentIdIgnoreCurrentInd(documentId);
    }

    public List<ActionRequestValue> getActionRequests() {
        if (this.simulatedActionRequests == null || this.simulatedActionRequests.isEmpty()) {
            return KEWServiceLocator.getActionRequestService().findByDocumentIdIgnoreCurrentInd(documentId);
        } else {
            return this.simulatedActionRequests;
        }
    }

    public List<ActionRequestValue> getSimulatedActionRequests() {
        if (this.simulatedActionRequests == null) {
            this.simulatedActionRequests = new ArrayList<ActionRequestValue>();
        }
        return this.simulatedActionRequests;
    }

    public void setSimulatedActionRequests(List<ActionRequestValue> simulatedActionRequests) {
        this.simulatedActionRequests = simulatedActionRequests;
    }

    public DocumentType getDocumentType() {
        return KEWServiceLocator.getDocumentTypeService().findById(getDocumentTypeId());
    }

    public java.lang.String getAppDocId() {
        return appDocId;
    }

    public void setAppDocId(java.lang.String appDocId) {
        this.appDocId = appDocId;
    }

    public java.sql.Timestamp getApprovedDate() {
        return approvedDate;
    }

    public void setApprovedDate(java.sql.Timestamp approvedDate) {
        this.approvedDate = approvedDate;
    }

    public java.sql.Timestamp getCreateDate() {
        return createDate;
    }

    public void setCreateDate(java.sql.Timestamp createDate) {
        this.createDate = createDate;
    }

    public java.lang.String getDocContent() {
        return getDocumentContent().getDocumentContent();
    }

    public void setDocContent(java.lang.String docContent) {
        DocumentRouteHeaderValueContent content = getDocumentContent();
        content.setDocumentContent(docContent);
    }

    public java.lang.Integer getDocRouteLevel() {
        return docRouteLevel;
    }

    public void setDocRouteLevel(java.lang.Integer docRouteLevel) {
        this.docRouteLevel = docRouteLevel;
    }

    public java.lang.String getDocRouteStatus() {
        return docRouteStatus;
    }

    public void setDocRouteStatus(java.lang.String docRouteStatus) {
        this.docRouteStatus = docRouteStatus;
    }

    public java.lang.String getDocTitle() {
        return docTitle;
    }

    public void setDocTitle(java.lang.String docTitle) {
        this.docTitle = docTitle;
    }

    @Override
    public String getDocumentTypeId() {
        return documentTypeId;
    }

    public void setDocumentTypeId(String documentTypeId) {
        this.documentTypeId = documentTypeId;
    }

    public java.lang.Integer getDocVersion() {
        return docVersion;
    }

    public void setDocVersion(java.lang.Integer docVersion) {
        this.docVersion = docVersion;
    }

    public java.sql.Timestamp getFinalizedDate() {
        return finalizedDate;
    }

    public void setFinalizedDate(java.sql.Timestamp finalizedDate) {
        this.finalizedDate = finalizedDate;
    }

    public java.lang.String getInitiatorWorkflowId() {
        return initiatorWorkflowId;
    }

    public void setInitiatorWorkflowId(java.lang.String initiatorWorkflowId) {
        this.initiatorWorkflowId = initiatorWorkflowId;
    }

    public java.lang.String getRoutedByUserWorkflowId() {
        if ((isEnroute()) && (StringUtils.isBlank(routedByUserWorkflowId))) {
            return initiatorWorkflowId;
        }
        return routedByUserWorkflowId;
    }

    public void setRoutedByUserWorkflowId(java.lang.String routedByUserWorkflowId) {
        this.routedByUserWorkflowId = routedByUserWorkflowId;
    }

    @Override
    public String getDocumentId() {
        return documentId;
    }

    public void setDocumentId(java.lang.String documentId) {
        this.documentId = documentId;
    }

    public java.sql.Timestamp getRouteStatusDate() {
        return routeStatusDate;
    }

    public void setRouteStatusDate(java.sql.Timestamp routeStatusDate) {
        this.routeStatusDate = routeStatusDate;
    }

    public java.sql.Timestamp getDateModified() {
        return dateModified;
    }

    public void setDateModified(java.sql.Timestamp dateModified) {
        this.dateModified = dateModified;
    }

    /**
     *
     * This method returns the Application Document Status.
     * This status is an alternative to the Route Status that may be used for a document.
     * It is configurable per document type.
     *
     * @see ApplicationDocumentStatus
     * @see DocumentTypePolicy
     *
     * @return
     */
    public java.lang.String getAppDocStatus() {
        if (StringUtils.isBlank(appDocStatus)) {
            return KewApiConstants.UNKNOWN_STATUS;
        }
        return appDocStatus;
    }

    public void setAppDocStatus(java.lang.String appDocStatus) {
        this.appDocStatus = appDocStatus;
    }

    /**
     *
     * This method returns a combination of the route status label and the app doc status.
     *
     * @return
     */
    public String getCombinedStatus() {
        String routeStatus = getRouteStatusLabel();
        String appStatus = getAppDocStatus();
        if (StringUtils.isNotEmpty(routeStatus)) {
            if (StringUtils.isNotEmpty(appStatus)) {
                routeStatus += ", " + appStatus;
            }
        } else {
            return appStatus;
        }
        return routeStatus;
    }

    /**
     *
     * This method sets the appDocStatus.
     * It firsts validates the new value against the defined acceptable values, if defined.
     * It also updates the AppDocStatus date, and saves the status transition information
     *
     * @param appDocStatus
     * @throws WorkflowRuntimeException
     */
    public void updateAppDocStatus(java.lang.String appDocStatus) throws WorkflowRuntimeException {
        //validate against allowable values if defined
        if (appDocStatus != null && appDocStatus.length() > 0
                && !appDocStatus.equalsIgnoreCase(this.appDocStatus)) {
            DocumentType documentType = KEWServiceLocator.getDocumentTypeService()
                    .findById(this.getDocumentTypeId());
            if (documentType.getValidApplicationStatuses() != null
                    && documentType.getValidApplicationStatuses().size() > 0) {
                Iterator<ApplicationDocumentStatus> iter = documentType.getValidApplicationStatuses().iterator();
                boolean statusValidated = false;
                while (iter.hasNext()) {
                    ApplicationDocumentStatus myAppDocStat = iter.next();
                    if (appDocStatus.compareToIgnoreCase(myAppDocStat.getStatusName()) == 0) {
                        statusValidated = true;
                        break;
                    }
                }
                if (!statusValidated) {
                    WorkflowRuntimeException xpee = new WorkflowRuntimeException(
                            "AppDocStatus value " + appDocStatus + " not allowable.");
                    LOG.error("Error validating nextAppDocStatus name: " + appDocStatus
                            + " against acceptable values.", xpee);
                    throw xpee;
                }
            }

            // set the status value
            String oldStatus = this.appDocStatus;
            this.appDocStatus = appDocStatus;

            // update the timestamp
            setAppDocStatusDate(new Timestamp(System.currentTimeMillis()));

            // save the status transition
            this.appDocStatusHistory.add(new DocumentStatusTransition(documentId, oldStatus, appDocStatus));
        }

    }

    public java.sql.Timestamp getAppDocStatusDate() {
        return appDocStatusDate;
    }

    public void setAppDocStatusDate(java.sql.Timestamp appDocStatusDate) {
        this.appDocStatusDate = appDocStatusDate;
    }

    public Object copy(boolean preserveKeys) {
        throw new UnsupportedOperationException("The copy method is deprecated and unimplemented!");
    }

    /**
     * @return True if the document is in the state of Initiated
     */
    public boolean isStateInitiated() {
        return KewApiConstants.ROUTE_HEADER_INITIATED_CD.equals(docRouteStatus);
    }

    /**
     * @return True if the document is in the state of Saved
     */
    public boolean isStateSaved() {
        return KewApiConstants.ROUTE_HEADER_SAVED_CD.equals(docRouteStatus);
    }

    /**
     * @return true if the document has ever been inte enroute state
     */
    public boolean isRouted() {
        return !(isStateInitiated() || isStateSaved());
    }

    public boolean isInException() {
        return KewApiConstants.ROUTE_HEADER_EXCEPTION_CD.equals(docRouteStatus);
    }

    public boolean isDisaproved() {
        return KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD.equals(docRouteStatus);
    }

    public boolean isCanceled() {
        return KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(docRouteStatus);
    }

    public boolean isFinal() {
        return KewApiConstants.ROUTE_HEADER_FINAL_CD.equals(docRouteStatus);
    }

    public boolean isEnroute() {
        return KewApiConstants.ROUTE_HEADER_ENROUTE_CD.equals(docRouteStatus);
    }

    /**
     * @return true if the document is in the processed state
     */
    public boolean isProcessed() {
        return KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(docRouteStatus);
    }

    public boolean isRoutable() {
        return KewApiConstants.ROUTE_HEADER_ENROUTE_CD.equals(docRouteStatus) ||
        //KewApiConstants.ROUTE_HEADER_EXCEPTION_CD.equals(docRouteStatus) ||
                KewApiConstants.ROUTE_HEADER_SAVED_CD.equals(docRouteStatus)
                || KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(docRouteStatus);
    }

    /**
     * Return true if the given action code is valid for this document's current state.
     * This method only verifies statically defined action/state transitions, it does not
     * perform full action validation logic.
     * @see org.kuali.rice.kew.actions.ActionRegistry#getValidActions(org.kuali.rice.kim.api.identity.principal.PrincipalContract, DocumentRouteHeaderValue)
     * @param actionCd The action code to be tested.
     * @return True if the action code is valid for the document's status.
     */
    public boolean isValidActionToTake(String actionCd) {
        String actions = legalActions.get(docRouteStatus);
        return actions.contains(actionCd);
    }

    public boolean isValidStatusChange(String newStatus) {
        return stateTransitionMap.get(getDocRouteStatus()).contains(newStatus);
    }

    public void setRouteStatus(String newStatus, boolean finalState) throws InvalidActionTakenException {
        if (newStatus != getDocRouteStatus()) {
            // only modify the status mod date if the status actually changed
            setRouteStatusDate(new Timestamp(System.currentTimeMillis()));
        }
        if (stateTransitionMap.get(getDocRouteStatus()).contains(newStatus)) {
            LOG.debug("changing status");
            setDocRouteStatus(newStatus);
        } else {
            LOG.debug("unable to change status");
            throw new InvalidActionTakenException(
                    "Document status " + CodeTranslator.getRouteStatusLabel(getDocRouteStatus())
                            + " cannot transition to status " + CodeTranslator.getRouteStatusLabel(newStatus));
        }
        setDateModified(new Timestamp(System.currentTimeMillis()));
        if (finalState) {
            LOG.debug("setting final timeStamp");
            setFinalizedDate(new Timestamp(System.currentTimeMillis()));
        }
    }

    /**
     * Mark the document as being processed.
     *
     * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentProcessed() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked processed");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_PROCESSED_CD, !FINAL_STATE);
    }

    /**
     * Mark document cancled.
     *
     * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentCanceled() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked canceled");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_CANCEL_CD, FINAL_STATE);
    }

    /**
     * Mark document recalled.
     *
     * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentRecalled() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked recalled");
        }
        setRouteStatus(DocumentStatus.RECALLED.getCode(), FINAL_STATE);
    }

    /**
     * Mark document disapproved
     *
     * @throws ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentDisapproved() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked disapproved");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD, FINAL_STATE);
    }

    /**
     * Mark document saved
     *
     * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentSaved() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked saved");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_SAVED_CD, !FINAL_STATE);
    }

    /**
     * Mark the document as being in the exception state.
     *
     * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentInException() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked in exception");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_EXCEPTION_CD, !FINAL_STATE);
    }

    /**
     * Mark the document as being actively routed.
     *
     * @throws ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentEnroute() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked enroute");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_ENROUTE_CD, !FINAL_STATE);
    }

    /**
     * Mark document finalized.
     *
     * @throws ResourceUnavailableException
     * @throws InvalidActionTakenException
     */
    public void markDocumentFinalized() throws InvalidActionTakenException {
        if (LOG.isDebugEnabled()) {
            LOG.debug(this + " marked finalized");
        }
        setRouteStatus(KewApiConstants.ROUTE_HEADER_FINAL_CD, FINAL_STATE);
    }

    /**
     * This method takes data from a VO and sets it on this route header
     * @param routeHeaderVO
     * @throws org.kuali.rice.kew.api.exception.WorkflowException
     */
    public void setRouteHeaderData(Document routeHeaderVO) throws WorkflowException {
        if (!ObjectUtils.equals(getDocTitle(), routeHeaderVO.getTitle())) {
            KEWServiceLocator.getActionListService().updateActionItemsForTitleChange(getDocumentId(),
                    routeHeaderVO.getTitle());
        }
        setDocTitle(routeHeaderVO.getTitle());
        setAppDocId(routeHeaderVO.getApplicationDocumentId());
        setDateModified(new Timestamp(System.currentTimeMillis()));
        updateAppDocStatus(routeHeaderVO.getApplicationDocumentStatus());

        /* set the variables from the routeHeaderVO */
        for (Map.Entry<String, String> kvp : routeHeaderVO.getVariables().entrySet()) {
            setVariable(kvp.getKey(), kvp.getValue());
        }
    }

    public void applyDocumentUpdate(DocumentUpdate documentUpdate) {
        if (documentUpdate != null) {
            String thisDocTitle = getDocTitle() == null ? "" : getDocTitle();
            String updateDocTitle = documentUpdate.getTitle() == null ? "" : documentUpdate.getTitle();
            if (!StringUtils.equals(thisDocTitle, updateDocTitle)) {
                KEWServiceLocator.getActionListService().updateActionItemsForTitleChange(getDocumentId(),
                        documentUpdate.getTitle());
            }
            setDocTitle(updateDocTitle);
            setAppDocId(documentUpdate.getApplicationDocumentId());
            setDateModified(new Timestamp(System.currentTimeMillis()));
            updateAppDocStatus(documentUpdate.getApplicationDocumentStatus());

            Map<String, String> variables = documentUpdate.getVariables();
            for (String variableName : variables.keySet()) {
                setVariable(variableName, variables.get(variableName));
            }
        }
    }

    /**
     * Convenience method that returns the branch of the first (and presumably only?) initial node
     * @return the branch of the first (and presumably only?) initial node
     */
    public Branch getRootBranch() {
        if (!this.initialRouteNodeInstances.isEmpty()) {
            return getInitialRouteNodeInstance(0).getBranch();
        }
        return null;
    }

    /**
     * Looks up a variable (embodied in a "BranchState" key/value pair) in the
     * branch state table.
     */
    private BranchState findVariable(String name) {
        Branch rootBranch = getRootBranch();
        if (rootBranch != null) {
            List<BranchState> branchState = rootBranch.getBranchState();
            Iterator<BranchState> it = branchState.iterator();
            while (it.hasNext()) {
                BranchState state = it.next();
                if (ObjectUtils.equals(state.getKey(), BranchState.VARIABLE_PREFIX + name)) {
                    return state;
                }
            }
        }
        return null;
    }

    /**
     * Gets a variable
     * @param name variable name
     * @return variable value, or null if variable is not defined
     */
    public String getVariable(String name) {
        BranchState state = findVariable(name);
        if (state == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Variable not found: '" + name + "'");
            }
            return null;
        }
        return state.getValue();
    }

    public void removeVariableThatContains(String name) {
        List<BranchState> statesToRemove = new ArrayList<BranchState>();
        for (BranchState state : this.getRootBranchState()) {
            if (state.getKey().contains(name)) {
                statesToRemove.add(state);
            }
        }
        this.getRootBranchState().removeAll(statesToRemove);
    }

    /**
     * Sets a variable
     * @param name variable name
     * @param value variable value, or null if variable should be removed
     */
    public void setVariable(String name, String value) {
        BranchState state = findVariable(name);
        Branch rootBranch = getRootBranch();
        if (rootBranch != null) {
            List<BranchState> branchState = rootBranch.getBranchState();
            if (state == null) {
                if (value == null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("set non existent variable '" + name + "' to null value");
                    }
                    return;
                }
                LOG.debug("Adding branch state: '" + name + "'='" + value + "'");
                state = new BranchState();
                state.setBranch(rootBranch);
                state.setKey(BranchState.VARIABLE_PREFIX + name);
                state.setValue(value);
                rootBranch.addBranchState(state);
            } else {
                if (value == null) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Removing value: " + state.getKey() + "=" + state.getValue());
                    }
                    branchState.remove(state);
                } else {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Setting value of variable '" + name + "' to '" + value + "'");
                    }
                    state.setValue(value);
                }
            }
        }
    }

    public List<BranchState> getRootBranchState() {
        if (this.getRootBranch() != null) {
            return this.getRootBranch().getBranchState();
        }
        return null;
    }

    public CustomActionListAttribute getCustomActionListAttribute() throws WorkflowException {
        CustomActionListAttribute customActionListAttribute = null;
        if (this.getDocumentType() != null) {
            customActionListAttribute = this.getDocumentType().getCustomActionListAttribute();
            if (customActionListAttribute != null) {
                return customActionListAttribute;
            }
        }
        customActionListAttribute = new DefaultCustomActionListAttribute();
        return customActionListAttribute;
    }

    public CustomEmailAttribute getCustomEmailAttribute() throws WorkflowException {
        CustomEmailAttribute customEmailAttribute = null;
        try {
            if (this.getDocumentType() != null) {
                customEmailAttribute = this.getDocumentType().getCustomEmailAttribute();
                if (customEmailAttribute != null) {
                    customEmailAttribute.setRouteHeaderVO(DocumentRouteHeaderValue.to(this));
                    return customEmailAttribute;
                }
            }
        } catch (Exception e) {
            LOG.warn("Error in retrieving custom email attribute", e);
        }
        customEmailAttribute = new CustomEmailAttributeImpl();
        customEmailAttribute.setRouteHeaderVO(DocumentRouteHeaderValue.to(this));
        return customEmailAttribute;
    }

    public CustomNoteAttribute getCustomNoteAttribute() throws WorkflowException {
        CustomNoteAttribute customNoteAttribute = null;
        try {
            if (this.getDocumentType() != null) {
                customNoteAttribute = this.getDocumentType().getCustomNoteAttribute();
                if (customNoteAttribute != null) {
                    customNoteAttribute.setRouteHeaderVO(DocumentRouteHeaderValue.to(this));
                    return customNoteAttribute;
                }
            }
        } catch (Exception e) {
            LOG.warn("Error in retrieving custom note attribute", e);
        }
        customNoteAttribute = new CustomNoteAttributeImpl();
        customNoteAttribute.setRouteHeaderVO(DocumentRouteHeaderValue.to(this));
        return customNoteAttribute;
    }

    public ActionRequestValue getDocActionRequest(int index) {
        List<ActionRequestValue> actionRequests = getActionRequests();
        while (actionRequests.size() <= index) {
            ActionRequestValue actionRequest = new ActionRequestFactory(this).createBlankActionRequest();
            actionRequest.setNodeInstance(new RouteNodeInstance());
            actionRequests.add(actionRequest);
        }
        return actionRequests.get(index);
    }

    public ActionTakenValue getDocActionTaken(int index) {
        List<ActionTakenValue> actionsTaken = getActionsTaken();
        while (actionsTaken.size() <= index) {
            actionsTaken.add(new ActionTakenValue());
        }
        return actionsTaken.get(index);
    }

    public ActionItem getDocActionItem(int index) {
        List<ActionItem> actionItems = getActionItems();
        while (actionItems.size() <= index) {
            actionItems.add(new ActionItem());
        }
        return actionItems.get(index);
    }

    private RouteNodeInstance getInitialRouteNodeInstance(int index) {
        if (initialRouteNodeInstances.size() >= index) {
            return initialRouteNodeInstances.get(index);
        }
        return null;
    }

    public boolean isRoutingReport() {
        return routingReport;
    }

    public void setRoutingReport(boolean routingReport) {
        this.routingReport = routingReport;
    }

    public List<RouteNodeInstance> getInitialRouteNodeInstances() {
        return initialRouteNodeInstances;
    }

    public void setInitialRouteNodeInstances(List<RouteNodeInstance> initialRouteNodeInstances) {
        this.initialRouteNodeInstances = initialRouteNodeInstances;
    }

    public List<Note> getNotes() {
        return notes;
    }

    public void setNotes(List<Note> notes) {
        this.notes = notes;
    }

    public DocumentRouteHeaderValueContent getDocumentContent() {
        if (documentContent == null) {
            documentContent = KEWServiceLocator.getRouteHeaderService().getContent(getDocumentId());
        }
        return documentContent;
    }

    public void setDocumentContent(DocumentRouteHeaderValueContent documentContent) {
        this.documentContent = documentContent;
    }

    public List<DocumentStatusTransition> getAppDocStatusHistory() {
        return this.appDocStatusHistory;
    }

    public void setAppDocStatusHistory(List<DocumentStatusTransition> appDocStatusHistory) {
        this.appDocStatusHistory = appDocStatusHistory;
    }

    @Override
    public DocumentStatus getStatus() {
        return DocumentStatus.fromCode(getDocRouteStatus());
    }

    @Override
    public DateTime getDateCreated() {
        if (getCreateDate() == null) {
            return null;
        }
        return new DateTime(getCreateDate().getTime());
    }

    @Override
    public DateTime getDateLastModified() {
        if (getDateModified() == null) {
            return null;
        }
        return new DateTime(getDateModified().getTime());
    }

    @Override
    public DateTime getDateApproved() {
        if (getApprovedDate() == null) {
            return null;
        }
        return new DateTime(getApprovedDate().getTime());
    }

    @Override
    public DateTime getDateFinalized() {
        if (getFinalizedDate() == null) {
            return null;
        }
        return new DateTime(getFinalizedDate().getTime());
    }

    @Override
    public String getTitle() {
        return docTitle;
    }

    @Override
    public String getApplicationDocumentId() {
        return appDocId;
    }

    @Override
    public String getInitiatorPrincipalId() {
        return initiatorWorkflowId;
    }

    @Override
    public String getRoutedByPrincipalId() {
        return routedByUserWorkflowId;
    }

    @Override
    public String getDocumentTypeName() {
        return getDocumentType().getName();
    }

    @Override
    public String getDocumentHandlerUrl() {
        return getDocumentType().getResolvedDocumentHandlerUrl();
    }

    @Override
    public String getApplicationDocumentStatus() {
        return appDocStatus;
    }

    @Override
    public DateTime getApplicationDocumentStatusDate() {
        if (appDocStatusDate == null) {
            return null;
        }
        return new DateTime(appDocStatusDate.getTime());
    }

    @Override
    public Map<String, String> getVariables() {
        Map<String, String> documentVariables = new HashMap<String, String>();
        /* populate the routeHeaderVO with the document variables */
        // FIXME: we assume there is only one for now
        Branch routeNodeInstanceBranch = getRootBranch();
        // Ok, we are using the "branch state" as the arbitrary convenient repository for flow/process/edoc variables
        // so we need to stuff them into the VO
        if (routeNodeInstanceBranch != null) {
            List<BranchState> listOfBranchStates = routeNodeInstanceBranch.getBranchState();
            for (BranchState bs : listOfBranchStates) {
                if (bs.getKey() != null && bs.getKey().startsWith(BranchState.VARIABLE_PREFIX)) {
                    if (LOG.isDebugEnabled()) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Setting branch state variable on vo: " + bs.getKey() + "=" + bs.getValue());
                        }
                    }
                    documentVariables.put(bs.getKey().substring(BranchState.VARIABLE_PREFIX.length()),
                            bs.getValue());
                }
            }
        }
        return documentVariables;
    }

    public static Document to(DocumentRouteHeaderValue documentBo) {
        if (documentBo == null) {
            return null;
        }
        Document.Builder builder = Document.Builder.create(documentBo);
        return builder.build();
    }

    public static DocumentRouteHeaderValue from(Document document) {
        DocumentRouteHeaderValue documentBo = new DocumentRouteHeaderValue();
        documentBo.setAppDocId(document.getApplicationDocumentId());
        if (document.getDateApproved() != null) {
            documentBo.setApprovedDate(new Timestamp(document.getDateApproved().getMillis()));
        }
        if (document.getDateCreated() != null) {
            documentBo.setCreateDate(new Timestamp(document.getDateCreated().getMillis()));
        }
        if (StringUtils.isEmpty(documentBo.getDocContent())) {
            documentBo.setDocContent(KewApiConstants.DEFAULT_DOCUMENT_CONTENT);
        }
        documentBo.setDocRouteStatus(document.getStatus().getCode());
        documentBo.setDocTitle(document.getTitle());
        if (document.getDocumentTypeName() != null) {
            DocumentType documentType = KEWServiceLocator.getDocumentTypeService()
                    .findByName(document.getDocumentTypeName());
            if (documentType == null) {
                throw new RiceRuntimeException(
                        "Could not locate the given document type name: " + document.getDocumentTypeName());
            }
            documentBo.setDocumentTypeId(documentType.getDocumentTypeId());
        }
        if (document.getDateFinalized() != null) {
            documentBo.setFinalizedDate(new Timestamp(document.getDateFinalized().getMillis()));
        }
        documentBo.setInitiatorWorkflowId(document.getInitiatorPrincipalId());
        documentBo.setRoutedByUserWorkflowId(document.getRoutedByPrincipalId());
        documentBo.setDocumentId(document.getDocumentId());
        if (document.getDateLastModified() != null) {
            documentBo.setDateModified(new Timestamp(document.getDateLastModified().getMillis()));
        }
        documentBo.setAppDocStatus(document.getApplicationDocumentStatus());
        if (document.getApplicationDocumentStatusDate() != null) {
            documentBo.setAppDocStatusDate(new Timestamp(document.getApplicationDocumentStatusDate().getMillis()));
        }

        // Convert the variables
        Map<String, String> variables = document.getVariables();
        if (variables != null && !variables.isEmpty()) {
            for (Map.Entry<String, String> kvp : variables.entrySet()) {
                documentBo.setVariable(kvp.getKey(), kvp.getValue());
            }
        }

        return documentBo;
    }

    @Override
    public void refresh() {
    }

    public DocumentRouteHeaderValue deepCopy(Map<Object, Object> visited) {
        if (visited.containsKey(this)) {
            return (DocumentRouteHeaderValue) visited.get(this);
        }
        DocumentRouteHeaderValue copy = new DocumentRouteHeaderValue();
        visited.put(this, copy);
        copy.documentId = documentId;
        copy.documentTypeId = documentTypeId;
        copy.docRouteStatus = docRouteStatus;
        copy.docRouteLevel = docRouteLevel;
        copy.dateModified = copyTimestamp(dateModified);
        copy.createDate = copyTimestamp(createDate);
        copy.approvedDate = copyTimestamp(approvedDate);
        copy.finalizedDate = copyTimestamp(finalizedDate);
        copy.docTitle = docTitle;
        copy.appDocId = appDocId;
        copy.docVersion = docVersion;
        copy.initiatorWorkflowId = initiatorWorkflowId;
        copy.routedByUserWorkflowId = routedByUserWorkflowId;
        copy.routeStatusDate = copyTimestamp(routeStatusDate);
        copy.appDocStatus = appDocStatus;
        copy.appDocStatusDate = copyTimestamp(appDocStatusDate);
        if (documentContent != null) {
            copy.documentContent = documentContent.deepCopy(visited);
        }
        copy.routingReport = routingReport;
        if (initialRouteNodeInstances != null) {
            List<RouteNodeInstance> copies = new ArrayList<RouteNodeInstance>();
            for (RouteNodeInstance routeNodeInstance : initialRouteNodeInstances) {
                copies.add(routeNodeInstance.deepCopy(visited));
            }
            copy.setInitialRouteNodeInstances(copies);
        }
        if (appDocStatusHistory != null) {
            List<DocumentStatusTransition> copies = new ArrayList<DocumentStatusTransition>();
            for (DocumentStatusTransition documentStatusTransition : appDocStatusHistory) {
                copies.add(documentStatusTransition.deepCopy(visited));
            }
            copy.setAppDocStatusHistory(copies);
        }
        if (notes != null) {
            List<Note> copies = new ArrayList<Note>();
            for (Note note : notes) {
                copies.add(note.deepCopy(visited));
            }
            copy.setNotes(copies);
        }

        return copy;
    }

    private Timestamp copyTimestamp(Timestamp timestamp) {
        if (timestamp == null) {
            return null;
        }
        return new Timestamp(timestamp.getTime());
    }

}