001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.xml;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.core.api.CoreApiServiceLocator;
020import org.kuali.rice.core.api.util.xml.XmlException;
021import org.kuali.rice.core.api.util.xml.XmlHelper;
022import org.kuali.rice.core.api.util.xml.XmlJotter;
023import org.kuali.rice.kew.api.KewApiConstants;
024import org.kuali.rice.kew.api.WorkflowRuntimeException;
025import org.kuali.rice.kew.api.exception.InvalidParentDocTypeException;
026import org.kuali.rice.kew.api.exception.WorkflowException;
027import org.kuali.rice.kew.doctype.ApplicationDocumentStatus;
028import org.kuali.rice.kew.doctype.ApplicationDocumentStatusCategory;
029import org.kuali.rice.kew.doctype.DocumentTypeAttributeBo;
030import org.kuali.rice.kew.doctype.DocumentTypePolicy;
031import org.kuali.rice.kew.doctype.bo.DocumentType;
032import org.kuali.rice.kew.document.DocumentTypeMaintainable;
033import org.kuali.rice.kew.engine.node.ActivationTypeEnum;
034import org.kuali.rice.kew.engine.node.BranchPrototype;
035import org.kuali.rice.kew.engine.node.NodeType;
036import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
037import org.kuali.rice.kew.engine.node.RoleNode;
038import org.kuali.rice.kew.engine.node.RouteNode;
039import org.kuali.rice.kew.engine.node.RouteNodeConfigParam;
040import org.kuali.rice.kew.export.KewExportDataSet;
041import org.kuali.rice.kew.role.RoleRouteModule;
042import org.kuali.rice.kew.rule.FlexRM;
043import org.kuali.rice.kew.rule.bo.RuleAttribute;
044import org.kuali.rice.kew.rule.bo.RuleTemplateBo;
045import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
046import org.kuali.rice.kew.service.KEWServiceLocator;
047import org.kuali.rice.kew.util.Utilities;
048import org.kuali.rice.kim.api.group.Group;
049import org.kuali.rice.kim.api.group.GroupService;
050import org.kuali.rice.kim.api.services.KimApiServiceLocator;
051import org.kuali.rice.kns.maintenance.Maintainable;
052import org.kuali.rice.kns.util.MaintenanceUtils;
053import org.kuali.rice.krad.exception.GroupNotFoundException;
054import org.kuali.rice.krad.util.ObjectUtils;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.w3c.dom.NamedNodeMap;
058import org.w3c.dom.Node;
059import org.w3c.dom.NodeList;
060import org.xml.sax.SAXException;
061
062import javax.xml.parsers.DocumentBuilderFactory;
063import javax.xml.parsers.ParserConfigurationException;
064import javax.xml.xpath.XPath;
065import javax.xml.xpath.XPathConstants;
066import javax.xml.xpath.XPathExpressionException;
067import java.io.BufferedInputStream;
068import java.io.ByteArrayInputStream;
069import java.io.IOException;
070import java.io.InputStream;
071import java.util.ArrayList;
072import java.util.Arrays;
073import java.util.Collection;
074import java.util.HashMap;
075import java.util.HashSet;
076import java.util.Iterator;
077import java.util.LinkedList;
078import java.util.List;
079import java.util.Map;
080import java.util.Set;
081
082import static org.kuali.rice.core.api.impex.xml.XmlConstants.*;
083
084
085/**
086 * A parser for parsing an XML file into {@link DocumentType}s.
087 *
088 * @author Kuali Rice Team (rice.collab@kuali.org)
089 */
090public class DocumentTypeXmlParser {
091
092    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(DocumentTypeXmlParser.class);
093
094    private static final String NEXT_NODE_EXP = "./@nextNode";
095    private static final String PARENT_NEXT_NODE_EXP = "../@nextNode";
096    private static final String NEXT_DOC_STATUS_EXP = "./@nextAppDocStatus";
097    /**
098     * Default route node activation type to use if omitted
099     */
100    private static final String DEFAULT_ACTIVATION_TYPE = "S";
101
102    public List docTypeRouteNodes;
103    private Map nodesMap;
104    private XPath xpath;
105    private Group defaultExceptionWorkgroup;
106    
107    protected XPath getXPath() {
108        if (this.xpath == null) {
109            this.xpath = XPathHelper.newXPath();
110        }
111        return xpath;
112    }
113    
114    public List<DocumentType> parseDocumentTypes(InputStream input) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException, WorkflowException, GroupNotFoundException {
115        Document routeDocument=XmlHelper.trimXml(input);
116        Map<String, DocumentType> documentTypesByName = new HashMap();
117        for (DocumentType type:  parseAllDocumentTypes(routeDocument)) {
118            documentTypesByName.put(type.getName(), type);
119        }
120        return new ArrayList<DocumentType>(documentTypesByName.values());
121    }
122
123    /**
124     * Parses all document types, both standard and routing.
125     * 
126     * @param routeDocument The DOM document to parse.
127     * @return A list containing the desired document types.
128     */
129    private List<DocumentType> parseAllDocumentTypes(Document routeDocument) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException, WorkflowException, GroupNotFoundException {
130        // A mapping from the names of uninitialized parent doc types to the child nodes that depend on the parent doc.
131        Map<String,List<DocTypeNode>> pendingChildDocs = new HashMap<String,List<DocTypeNode>>();
132        // A mapping from the names of uninitialized parent doc types to the names of the dependent children.
133        Map<String,List<String>> pendingChildNames = new HashMap<String,List<String>>();
134        // A stack containing Iterators over the various lists of unprocessed nodes; this allows for faster parent-child resolution
135        // without having to use recursion.
136        List<Iterator<DocTypeNode>> docInitStack = new ArrayList<Iterator<DocTypeNode>>();
137        // The first List of document types.
138        List<DocTypeNode> initialList = new ArrayList<DocTypeNode>();
139        // The current size of the stack.
140        int stackLen = 0;
141        // The current Iterator instance.
142        Iterator<DocTypeNode> currentIter = null;
143        // The current document type node.
144        DocTypeNode currDocNode = null;
145
146        List<DocumentType> docTypeBeans = new ArrayList<DocumentType>();
147                
148        // Acquire the "standard" and "routing" document types.
149        NodeList initialNodes = null;
150        xpath = XPathHelper.newXPath();
151        initialNodes = (NodeList) getXPath().evaluate("/" + DATA_ELEMENT + "/" + DOCUMENT_TYPES + "/" + DOCUMENT_TYPE, routeDocument, XPathConstants.NODESET);
152        // Take each NodeList's nodes and insert them into a List implementation.
153        for (int j = 0; j < initialNodes.getLength(); j++) {
154            Node documentTypeNode = initialNodes.item(j);
155            boolean docIsStandard = true;
156            try {
157                String xpathModeExpression = "./@" + DOCUMENT_TYPE_OVERWRITE_MODE;
158                if (XmlHelper.pathExists(xpath, xpathModeExpression, documentTypeNode)) {
159                    String overwriteMode = (String) getXPath().evaluate(xpathModeExpression, documentTypeNode, XPathConstants.STRING);
160                    docIsStandard = !StringUtils.equalsIgnoreCase("true", overwriteMode);
161                }
162            } catch (XPathExpressionException xpee) {
163                LOG.error("Error trying to check for '" + DOCUMENT_TYPE_OVERWRITE_MODE + "' attribute on document type element", xpee);
164                throw xpee;
165            }
166                initialList.add(new DocTypeNode(documentTypeNode, docIsStandard));
167        }
168
169        // Setup the Iterator instance to start with.
170        currentIter = initialList.iterator();
171        
172        // Keep looping until all Iterators are complete or an uncaught exception is thrown.
173        while (stackLen >= 0) {
174                // Determine the action to take based on whether there are remaining nodes in the present iterator.
175                if (currentIter.hasNext()) {
176                        // If the current iterator still has more nodes, process the next one.
177                        String newParentName = null;
178                        currDocNode = currentIter.next();
179                        // Initialize the document, and catch any child initialization problems.
180                        try {
181                                // Take appropriate action based on whether the document is a standard one or a routing one.
182                                DocumentType docType = parseDocumentType(!currDocNode.isStandard, currDocNode.docNode); 
183                                // Insert into appropriate position in the final list, based on the doc type's location in the XML file's list.
184                                docTypeBeans.add(docType);
185                        // Store the document's name for reference.
186                        newParentName = docType.getName();
187                        }
188                        catch (InvalidParentDocTypeException exc) {
189                                // If the parent document has not been processed yet, then store the child document.
190                        List<DocTypeNode> tempList = null;
191                        List<String> tempStrList = null;
192                        String parentName = exc.getParentName();
193                        String childName = exc.getChildName();
194                        if (parentName == null || childName == null) { // Make sure the parent & child documents' names are defined.
195                                throw exc;
196                        }
197                        tempList = pendingChildDocs.get(parentName);
198                        tempStrList = pendingChildNames.get(parentName);
199                        if (tempList == null) { // Initialize a new child document list if necessary.
200                                tempList = new ArrayList<DocTypeNode>();
201                                tempStrList = new ArrayList<String>();
202                                pendingChildDocs.put(parentName, tempList);
203                                pendingChildNames.put(parentName, tempStrList);
204                        }
205                                tempList.add(currDocNode);
206                                tempStrList.add(childName);
207                        }
208                        
209                // Check for any delayed child documents that are dependent on the current document.
210                        List<DocTypeNode> childrenToProcess = pendingChildDocs.remove(newParentName);
211                        pendingChildNames.remove(newParentName);
212                        if (childrenToProcess != null) {
213                                LOG.info("'" + newParentName + "' has children that were delayed; now processing them...");
214                                // If there are any pending children, push the old Iterator onto the stack and process the new Iterator on the next
215                                // iteration of the loop.
216                                stackLen++;
217                                docInitStack.add(currentIter);
218                                currentIter = childrenToProcess.iterator();
219                        }
220                }
221                else {
222                        // If the current Iterator has reached its end, discard it and pop the next one (if any) from the stack.
223                        stackLen--;
224                        currentIter = ((stackLen >= 0) ? docInitStack.remove(stackLen) : null);
225                }
226        }
227        
228        // Throw an error if there are still any uninitialized child documents.
229        if (pendingChildDocs.size() > 0) {
230                StringBuilder errMsg = new StringBuilder("Invalid parent document types: ");
231                // Construct the error message.
232                for (Iterator<String> unknownParents = pendingChildNames.keySet().iterator(); unknownParents.hasNext();) {
233                        String currParent = unknownParents.next();
234                        errMsg.append("Invalid parent doc type '").append(currParent).append("' is needed by child doc types ");
235                        for (Iterator<String> failedChildren = pendingChildNames.get(currParent).iterator(); failedChildren.hasNext();) {
236                                String currChild = failedChildren.next();
237                                errMsg.append('\'').append(currChild).append((failedChildren.hasNext()) ? "', " : "'; ");
238                        }
239                }
240                // Throw the exception.
241                throw new InvalidParentDocTypeException(null, null, errMsg.toString());
242        }
243        
244        return docTypeBeans;
245    }
246
247    private DocumentType parseDocumentType(boolean isOverwrite, Node documentTypeNode) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException, WorkflowException, GroupNotFoundException {
248        DocumentType documentType = getFullDocumentType(isOverwrite, documentTypeNode);
249//        parseStructure(isOverwrite, documentTypeNode, documentType, new RoutePathContext());
250        // reset variables
251        docTypeRouteNodes = null;
252        nodesMap = null;
253        xpath = null;
254        defaultExceptionWorkgroup = null;
255
256        LOG.debug("Saving document type " + documentType.getName());
257        routeDocumentType(documentType);
258        return documentType;
259    }
260
261    private DocumentType getFullDocumentType(boolean isOverwrite, Node documentTypeNode) throws XPathExpressionException, GroupNotFoundException, XmlException, WorkflowException, SAXException, IOException, ParserConfigurationException {
262        DocumentType documentType = getDocumentType(isOverwrite, documentTypeNode);
263        /*
264         * The following code does not need to apply the isOverwrite mode logic because it already checks to see if each node
265         * is available on the ingested XML. If the node is ingested then it doesn't matter if we're in overwrite mode or not
266         * the ingested code should save.
267         */
268        NodeList policiesList = (NodeList) getXPath().evaluate("./" + POLICIES, documentTypeNode, XPathConstants.NODESET);
269        if (policiesList.getLength() > 1) {
270            // more than one <policies> tag is invalid
271            throw new XmlException("More than one " + POLICIES + " node is present in a document type node");
272        }
273        else if (policiesList.getLength() > 0) {
274            // if there is exactly one <policies> tag then parse it and use the values
275            NodeList policyNodes = (NodeList) getXPath().evaluate("./" + POLICY, policiesList.item(0), XPathConstants.NODESET);
276            documentType.setDocumentTypePolicies(getDocumentTypePolicies(policyNodes, documentType));
277        }
278
279        NodeList attributeList = (NodeList) getXPath().evaluate("./attributes", documentTypeNode, XPathConstants.NODESET);
280        if (attributeList.getLength() > 1) {
281            throw new XmlException("More than one attributes node is present in a document type node");
282        }
283        else if (attributeList.getLength() > 0) {
284            NodeList attributeNodes = (NodeList) getXPath().evaluate("./attribute", attributeList.item(0), XPathConstants.NODESET);
285            documentType.setDocumentTypeAttributes(getDocumentTypeAttributes(attributeNodes, documentType));
286        }
287
288        NodeList securityList = (NodeList) getXPath().evaluate("./" + SECURITY, documentTypeNode, XPathConstants.NODESET);
289        if (securityList.getLength() > 1) {
290            throw new XmlException("More than one " + SECURITY + " node is present in a document type node");
291        }
292        else if (securityList.getLength() > 0) {
293           try {
294             Node securityNode = securityList.item(0);
295             String securityText = XmlJotter.jotNode(securityNode);
296             documentType.setDocumentTypeSecurityXml(securityText);
297           }
298           catch (Exception e) {
299             throw new XmlException(e);
300           }
301        }
302        parseStructure(isOverwrite, documentTypeNode, documentType, new RoutePathContext());
303        return documentType;
304    }
305
306    private void parseStructure(boolean isOverwrite, Node documentTypeNode, DocumentType documentType, RoutePathContext context) throws XPathExpressionException, XmlException, GroupNotFoundException {
307        // TODO have a validation function that takes an xpath statement and blows chunks if that
308        // statement returns false
309        boolean hasRoutePathsElement = false;
310        try {
311            hasRoutePathsElement = XmlHelper.pathExists(xpath, "./" + ROUTE_PATHS, documentTypeNode);
312        } catch (XPathExpressionException xpee) {
313            LOG.error("Error obtaining document type " + ROUTE_PATHS, xpee);
314            throw xpee;
315        }
316        boolean hasRouteNodesElement = false;
317        try {
318            hasRouteNodesElement = XmlHelper.pathExists(xpath, "./" + ROUTE_NODES, documentTypeNode);
319        } catch (XPathExpressionException xpee) {
320            LOG.error("Error obtaining document type " + ROUTE_NODES, xpee);
321            throw xpee;
322        }
323
324        // check to see if we're in overwrite mode
325        if (isOverwrite) {
326            // since we're in overwrite mode, if we don't have a routeNodes element or a routePaths element we simply return
327            if (!hasRouteNodesElement && !hasRoutePathsElement) {
328                return;
329            }
330            // if we have route nodes and route paths we're going to overwrite all existing processes so we can clear the current list
331            else if (hasRouteNodesElement && hasRoutePathsElement) {
332                documentType.setProcesses(new ArrayList());
333            }
334            // check to see if we have one but not the other element of routePaths and routeNodes
335            else if (!hasRouteNodesElement || !hasRoutePathsElement) {
336                // throw an exception since an ingestion can only have neither or both of the routePaths and routeNodes elements
337                throw new XmlException("A overwriting document type ingestion can not have only one of the " + ROUTE_PATHS + " and " + ROUTE_NODES + " elements.  Either both or neither should be defined.");
338            }
339        }
340
341        NodeList processNodes;
342
343        try {
344            if (XmlHelper.pathExists(xpath, "./" + ROUTE_PATHS + "/" + ROUTE_PATH, documentTypeNode)) {
345                processNodes = (NodeList) getXPath().evaluate("./" + ROUTE_PATHS + "/" + ROUTE_PATH, documentTypeNode, XPathConstants.NODESET);
346            } else {
347                if (hasRoutePathsElement) {
348                    createEmptyProcess(documentType);
349                }
350                return;
351            }
352        } catch (XPathExpressionException xpee) {
353            LOG.error("Error obtaining document type routePaths", xpee);
354            throw xpee;
355        }
356
357        createProcesses(processNodes, documentType);
358
359        NodeList nodeList = null;
360        try {
361            nodeList = (NodeList) getXPath().evaluate("./" + ROUTE_PATHS + "/" + ROUTE_PATH + "/start", documentTypeNode, XPathConstants.NODESET);
362        } catch (XPathExpressionException xpee) {
363            LOG.error("Error obtaining document type routePath start", xpee);
364            throw xpee;
365        }
366        if (nodeList.getLength() > 1) {
367            throw new XmlException("More than one start node is present in route path");
368        } else if (nodeList.getLength() == 0) {
369            throw new XmlException("No start node is present in route path");
370        }
371        try {
372            nodeList = (NodeList) getXPath().evaluate(".//" + ROUTE_NODES, documentTypeNode, XPathConstants.NODESET);
373        } catch (XPathExpressionException xpee) {
374            LOG.error("Error obtaining document type routeNodes", xpee);
375            throw xpee;
376        }
377        if (nodeList.getLength() > 1) {
378            throw new XmlException("More than one routeNodes node is present in documentType node");
379        } else if (nodeList.getLength() == 0) {
380            throw new XmlException("No routeNodes node is present in documentType node");
381        }
382        Node routeNodesNode = nodeList.item(0);
383        checkForOrphanedRouteNodes(documentTypeNode, routeNodesNode);
384
385        // passed validation.
386        nodesMap = new HashMap();
387        for (int index = 0; index < processNodes.getLength(); index++) {
388            Node processNode = processNodes.item(index);
389            String startName;
390            try {
391                startName = (String) getXPath().evaluate("./start/@name", processNode, XPathConstants.STRING);
392            } catch (XPathExpressionException xpee) {
393                LOG.error("Error obtaining routePath start name attribute", xpee);
394                throw xpee;
395            }
396            String processName = KewApiConstants.PRIMARY_PROCESS_NAME;
397            if (org.apache.commons.lang.StringUtils.isEmpty(startName)) {
398                try {
399                    startName = (String) getXPath().evaluate("./@" + INITIAL_NODE, processNode, XPathConstants.STRING);
400                } catch (XPathExpressionException xpee) {
401                    LOG.error("Error obtaining routePath initialNode attribute", xpee);
402                    throw xpee;
403                }
404                try {
405                    processName = (String) getXPath().evaluate("./@" + PROCESS_NAME, processNode, XPathConstants.STRING);
406                } catch (XPathExpressionException xpee) {
407                    LOG.error("Error obtaining routePath processName attribute", xpee);
408                    throw xpee;
409                }
410                if (org.apache.commons.lang.StringUtils.isEmpty(startName)) {
411                    throw new XmlException("Invalid routePath: no initialNode attribute defined!");
412                }
413            }
414            RouteNode routeNode = createRouteNode(null, startName, processNode, routeNodesNode, documentType, context);
415            if (routeNode != null) {
416                ProcessDefinitionBo process = documentType.getNamedProcess(processName);
417                process.setInitialRouteNode(routeNode);
418            }
419        }
420
421    }
422
423    private DocumentType getDocumentType(boolean isOverwrite, Node documentTypeNode) throws XPathExpressionException, GroupNotFoundException, XmlException, WorkflowException, SAXException, IOException, ParserConfigurationException {
424        DocumentType documentType = null;
425        String documentTypeName = getDocumentTypeNameFromNode(documentTypeNode);
426        DocumentType previousDocumentType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
427        if (isOverwrite) {
428            // we don't need the isOverwrite value to be passed here because we're only temporarily creating this document type in memory
429            documentType = generateNewDocumentTypeFromExisting(documentTypeName);
430            // export the document type that exists in the database
431        }
432        // if we don't have a valid value for documentType create a brand new instance
433        if (ObjectUtils.isNull(documentType)) {
434            documentType = new DocumentType();
435        }
436        documentType.setName(documentTypeName);
437
438        // set the description on the document type
439        // the description is always taken from the previous document type unless specified in the ingested file
440        String description = null;
441        try {
442            description = (String) getXPath().evaluate("./" + DESCRIPTION, documentTypeNode, XPathConstants.STRING);
443        } catch (XPathExpressionException xpee) {
444            LOG.error("Error obtaining document type description", xpee);
445            throw xpee;
446        }
447        // if the ingestion has produced a valid value then set it on the document
448        if (StringUtils.isNotBlank(description)) {
449            documentType.setDescription(description);
450        }
451        // at this point we know the ingested value is blank
452        else if (!isOverwrite) {
453            // if this is not an overwrite we need to check the previous document type version for a value to pull forward
454            if ( (ObjectUtils.isNotNull(previousDocumentType)) && (StringUtils.isNotBlank(previousDocumentType.getDescription())) ) {
455                // keep the same value from the previous version of the document type from the database
456                description = previousDocumentType.getDescription();
457            }
458            documentType.setDescription(description);
459        }
460
461        // set the label on the document type
462        String label = null;
463        try {
464            label = (String) getXPath().evaluate("./" + LABEL, documentTypeNode, XPathConstants.STRING);
465        } catch (XPathExpressionException xpee) {
466            LOG.error("Error obtaining document type label", xpee);
467            throw xpee;
468        }
469        // if the ingestion has produced a valid value then set it on the document
470        if (StringUtils.isNotBlank(label)) {
471            documentType.setLabel(label);
472        }
473        // at this point we know the ingested value is blank
474        else if (!isOverwrite) {
475            // if this is not an overwrite we need to check the previous document type version for a value to pull forward
476            if (ObjectUtils.isNotNull(previousDocumentType) && StringUtils.isNotBlank(previousDocumentType.getLabel())) {
477                // keep the same value from the previous version of the document type from the database
478                label = previousDocumentType.getLabel();
479            } else {
480                // otherwise set it to undefined
481                label = KewApiConstants.DEFAULT_DOCUMENT_TYPE_LABEL;
482            }
483            documentType.setLabel(label);
484        }
485
486        // set the post processor class on the document type
487        try {
488            /*
489             * - if the element tag is ingested... take whatever value is given, even if it's empty 
490             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
491             * then the documentType should already carry the value from the previous document type
492             */
493            if (XmlHelper.pathExists(xpath, "./" + POST_PROCESSOR_NAME, documentTypeNode)) {
494                String postProcessor = (String) getXPath().evaluate("./" + POST_PROCESSOR_NAME, documentTypeNode, XPathConstants.STRING);
495                if (StringUtils.isEmpty(postProcessor)) {
496                    documentType.setPostProcessorName(KewApiConstants.POST_PROCESSOR_NON_DEFINED_VALUE);
497                } else {
498                    documentType.setPostProcessorName((String) getXPath().evaluate("./" + POST_PROCESSOR_NAME, documentTypeNode, XPathConstants.STRING));
499                }    
500            }
501                
502        } catch (XPathExpressionException xpee) {
503            LOG.error("Error obtaining document type postProcessorName", xpee);
504            throw xpee;
505        }
506
507        // set the DocumentTypeAuthorizer
508        try {
509            if (XmlHelper.pathExists(xpath, "./" + AUTHORIZER, documentTypeNode)) {
510                String authorizer = (String) getXPath().evaluate("./" + AUTHORIZER, documentTypeNode, XPathConstants.STRING);
511                if (StringUtils.isNotBlank(authorizer)) {
512                    documentType.setAuthorizer(authorizer);
513                }
514            }
515        } catch (XPathExpressionException xpee) {
516            LOG.error("Error obtaining document type authorizer", xpee);
517            throw xpee;
518        }
519
520        // set the document handler URL on the document type
521        try {
522            /*
523             * - if the element tag is ingested... take whatever value is given, even if it's empty 
524             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
525             * then the documentType should already carry the value from the previous document type
526             */
527            if (XmlHelper.pathExists(xpath, "./" + DOC_HANDLER, documentTypeNode)) {
528                documentType.setUnresolvedDocHandlerUrl((String) getXPath().evaluate("./" + DOC_HANDLER, documentTypeNode, XPathConstants.STRING));
529            }
530        } catch (XPathExpressionException xpee) {
531            LOG.error("Error obtaining document type docHandler", xpee);
532            throw xpee;
533        }
534
535        // set the help definition URL on the document type
536        String helpDefUrl = null;
537        try {
538            helpDefUrl = (String) getXPath().evaluate("./" + HELP_DEFINITION_URL, documentTypeNode, XPathConstants.STRING);
539        } catch (XPathExpressionException xpee) {
540            LOG.error("Error obtaining document type help definition url", xpee);
541            throw xpee;
542        }
543        // if the ingestion has produced a valid value then set it on the document
544        if (StringUtils.isNotBlank(helpDefUrl)) {
545            documentType.setUnresolvedHelpDefinitionUrl(helpDefUrl);
546        }
547        // at this point we know the ingested value is blank
548        else if (!isOverwrite) {
549            // if this is not an overwrite, we need to check the previous document type version for a value to pull forward
550            if ( (ObjectUtils.isNotNull(previousDocumentType)) && (StringUtils.isNotBlank(previousDocumentType.getUnresolvedHelpDefinitionUrl())) ) {
551                // keep the same value from the previous version of the document type from the database
552                helpDefUrl = previousDocumentType.getUnresolvedHelpDefinitionUrl();
553            }
554            documentType.setUnresolvedHelpDefinitionUrl(helpDefUrl);
555        }
556        
557        // set the doc search help URL on the document type
558        String docSearchHelpUrl = null;
559        try {
560            docSearchHelpUrl = (String) getXPath().evaluate("./" + DOC_SEARCH_HELP_URL, documentTypeNode, XPathConstants.STRING);
561        } catch (XPathExpressionException xpee) {
562            LOG.error("Error obtaining document type document search help url", xpee);
563            throw xpee;
564        }
565        // if the ingestion has produced a valid value then set it on the document
566        if (StringUtils.isNotBlank(docSearchHelpUrl)) {
567            documentType.setUnresolvedDocSearchHelpUrl(docSearchHelpUrl);
568        }
569        // at this point we know the ingested value is blank
570        else if (!isOverwrite) {
571            // if this is not an overwrite, we need to check the previous document type version for a value to pull forward
572            if ( (ObjectUtils.isNotNull(previousDocumentType)) && (StringUtils.isNotBlank(previousDocumentType.getUnresolvedDocSearchHelpUrl())) ) {
573                // keep the same value from the previous version of the document type from the database
574                docSearchHelpUrl = previousDocumentType.getUnresolvedDocSearchHelpUrl();
575            }
576            documentType.setUnresolvedDocSearchHelpUrl(docSearchHelpUrl);
577        }
578
579        // set the application id on the document type
580        try {
581            /*
582             * - if the element tag is ingested... take whatever value is given, even if it's empty 
583             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
584             * then the documentType should already carry the value from the previous document type
585             */
586                if (XmlHelper.pathExists(xpath, "./" + APPLICATION_ID, documentTypeNode)) {
587                        documentType.setActualApplicationId((String) getXPath().evaluate("./" + APPLICATION_ID, documentTypeNode, XPathConstants.STRING));
588                } else if (XmlHelper.pathExists(xpath, "./" + SERVICE_NAMESPACE, documentTypeNode)) {
589                documentType.setActualApplicationId((String) getXPath().evaluate("./" + SERVICE_NAMESPACE, documentTypeNode, XPathConstants.STRING));
590                LOG.warn(SERVICE_NAMESPACE + " element was set on document type XML but is deprecated and will be removed in a future version, please use " + APPLICATION_ID + " instead.");
591            }
592        } catch (XPathExpressionException xpee) {
593            LOG.error("Error obtaining document type applicationId", xpee);
594            throw xpee;
595        }
596
597        // set the notification from address on the document type
598        try {
599            /*
600             * - if the element tag is ingested... take whatever value is given, even if it's empty 
601             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
602             * then the documentType should already carry the value from the previous document type
603             */
604            if (XmlHelper.pathExists(xpath, "./" + NOTIFICATION_FROM_ADDRESS, documentTypeNode)) {
605                documentType.setActualNotificationFromAddress((String) getXPath().evaluate("./" + NOTIFICATION_FROM_ADDRESS, documentTypeNode, XPathConstants.STRING));
606            }
607        } catch (XPathExpressionException xpee) {
608            LOG.error("Error obtaining document type " + NOTIFICATION_FROM_ADDRESS, xpee);
609            throw xpee;
610        }
611
612        try {
613            /*
614             * - if the element tag is ingested... take whatever value is given, even if it's empty 
615             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
616             * then the documentType should already carry the value from the previous document type
617             */
618            if (XmlHelper.pathExists(xpath, "./" + CUSTOM_EMAIL_STYLESHEET, documentTypeNode)) {
619                documentType.setCustomEmailStylesheet((String) getXPath().evaluate("./" + CUSTOM_EMAIL_STYLESHEET, documentTypeNode, XPathConstants.STRING));
620            }
621        } catch (XPathExpressionException xpee) {
622            LOG.error("Error obtaining document type " + CUSTOM_EMAIL_STYLESHEET, xpee);
623            throw xpee;
624        }
625
626        // any ingested document type by default becomes the current document type
627        documentType.setCurrentInd(Boolean.TRUE);
628
629        // set up the default exception workgroup for the document type
630        String exceptionWg = null;
631        String exceptionWgName = null;
632        String exceptionWgNamespace = null;
633        try {
634                if (XmlHelper.pathExists(xpath, "./" + DEFAULT_EXCEPTION_GROUP_NAME, documentTypeNode)) {
635                        exceptionWgName = (String) getXPath().evaluate("./" + DEFAULT_EXCEPTION_GROUP_NAME, documentTypeNode, XPathConstants.STRING);
636                        exceptionWgNamespace = (String) getXPath().evaluate("./" + DEFAULT_EXCEPTION_GROUP_NAME + "/@" + NAMESPACE, documentTypeNode, XPathConstants.STRING);
637                        exceptionWg = exceptionWgName;
638                } else {
639                        exceptionWg = (String) getXPath().evaluate("./" + DEFAULT_EXCEPTION_WORKGROUP_NAME, documentTypeNode, XPathConstants.STRING);
640                }
641        } catch (XPathExpressionException xpee) {
642            LOG.error("Error obtaining document type " + DEFAULT_EXCEPTION_GROUP_NAME, xpee);
643            throw xpee;
644        }
645        // we don't need to take the isOverwrite into account here because this ingestion method is a shortcut to use the same workgroup in all route nodes
646        if (StringUtils.isNotBlank(exceptionWg)) {
647                if (StringUtils.isNotBlank(exceptionWgName)) { // Found a "defaultExceptionGroupName" element.
648                        // allow core config parameter replacement in documenttype workgroups
649                        exceptionWgName = Utilities.substituteConfigParameters(exceptionWgName).trim();
650                        exceptionWgNamespace = Utilities.substituteConfigParameters(exceptionWgNamespace).trim();
651                } else { // Found a deprecated "defaultExceptionWorkgroupName" element.
652                        LOG.warn((new StringBuilder(160)).append("Document Type XML is using deprecated element '").append(DEFAULT_EXCEPTION_WORKGROUP_NAME).append(
653                                        "', please use '").append(DEFAULT_EXCEPTION_GROUP_NAME).append("' instead.").toString());
654                // allow core config parameter replacement in documenttype workgroups
655                exceptionWg = Utilities.substituteConfigParameters(exceptionWg);
656                exceptionWgName = Utilities.parseGroupName(exceptionWg);
657                exceptionWgNamespace = Utilities.parseGroupNamespaceCode(exceptionWg);
658                }
659            Group exceptionGroup = getGroupService().getGroupByNamespaceCodeAndName(exceptionWgNamespace,
660                    exceptionWgName);
661            if(exceptionGroup == null) {
662                throw new WorkflowRuntimeException("Exception workgroup name " + exceptionWgName + " does not exist");
663            }
664            documentType.setDefaultExceptionWorkgroup(exceptionGroup);
665            defaultExceptionWorkgroup = exceptionGroup;
666        }
667
668        // set up the active indicator on the document type
669        try {
670             // if the element tag is ingested... take whatever value is given
671            if (XmlHelper.pathExists(xpath, "./" + ACTIVE, documentTypeNode)) {
672                documentType.setActive(Boolean.valueOf((String) getXPath().evaluate("./" + ACTIVE, documentTypeNode, XPathConstants.STRING)));
673            } 
674            // if isOverwrite is false set the default value
675            else if (!isOverwrite) {
676                documentType.setActive(Boolean.TRUE);
677            }
678        } catch (XPathExpressionException xpee) {
679            LOG.error("Error obtaining document type active flag", xpee);
680            throw xpee;
681        }
682
683        // check for a parent document type for the ingested document type
684        boolean parentElementExists = false;
685        try {
686            parentElementExists = XmlHelper.pathExists(xpath, "./" + PARENT, documentTypeNode);
687        } catch (XPathExpressionException xpee) {
688            LOG.error("Error obtaining document type parent", xpee);
689            throw xpee;
690        }
691        /*
692         * - if the element tag is ingested... take whatever value is given 
693         * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
694         * then the documentType should already carry the value from the previous document type
695         */
696        if (parentElementExists) {
697            // the tag was ingested so we'll use whatever the user attempted to set
698            String parentDocumentTypeName = null;
699            try {
700                parentDocumentTypeName = (String) getXPath().evaluate("./" + PARENT, documentTypeNode, XPathConstants.STRING);
701            } catch (XPathExpressionException xpee) {
702                LOG.error("Error obtaining document type parent", xpee);
703                throw xpee;
704            }
705            DocumentType parentDocumentType = KEWServiceLocator.getDocumentTypeService().findByName(parentDocumentTypeName);
706            if (parentDocumentType == null) {
707                //throw new XmlException("Invalid parent document type: '" + parentDocumentTypeName + "'");
708                LOG.info("Parent document type '" + parentDocumentTypeName +
709                        "' could not be found; attempting to delay processing of '" + documentTypeName + "'...");
710                throw new InvalidParentDocTypeException(parentDocumentTypeName, documentTypeName,
711                        "Invalid parent document type: '" + parentDocumentTypeName + "'");
712            }
713            documentType.setDocTypeParentId(parentDocumentType.getDocumentTypeId());
714        }
715
716        // set the super user workgroup name on the document type
717        try {
718            /*
719             * - if the element tag is ingested... take whatever value is given
720             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
721             * then the documentType should already carry the value from the previous document type
722             */
723                if (XmlHelper.pathExists(xpath, "./" + SUPER_USER_GROUP_NAME, documentTypeNode)) {
724                        documentType.setSuperUserWorkgroupNoInheritence(retrieveValidKimGroup("./" + SUPER_USER_GROUP_NAME, documentTypeNode, false));
725                }
726                else if (XmlHelper.pathExists(xpath, "./" + SUPER_USER_WORKGROUP_NAME, documentTypeNode)) {
727                        LOG.warn((new StringBuilder(160)).append("Document Type XML is using deprecated element '").append(SUPER_USER_WORKGROUP_NAME).append(
728                                        "', please use '").append(SUPER_USER_GROUP_NAME).append("' instead.").toString());
729                documentType.setSuperUserWorkgroupNoInheritence(retrieveValidKimGroup("./" + SUPER_USER_WORKGROUP_NAME, documentTypeNode, true));
730            }
731        } catch (XPathExpressionException xpee) {
732            LOG.error("Error obtaining document type " + SUPER_USER_GROUP_NAME, xpee);
733            throw xpee;
734        }
735
736        // set the blanket approve workgroup name on the document type
737        String blanketWorkGroup = null;
738        String blanketGroupName = null;
739        String blanketNamespace = null;
740        String blanketApprovePolicy = null;
741        try {
742            // check if the blanket approve workgroup name element tag was set on the ingested document type and get value if it was
743                if (XmlHelper.pathExists(xpath, "./" + BLANKET_APPROVE_GROUP_NAME, documentTypeNode)) {
744                        blanketGroupName = (String) getXPath().evaluate("./" + BLANKET_APPROVE_GROUP_NAME, documentTypeNode, XPathConstants.STRING);
745                        blanketNamespace = (String) getXPath().evaluate("./" + BLANKET_APPROVE_GROUP_NAME + "/@" + NAMESPACE, documentTypeNode, XPathConstants.STRING);
746                        blanketWorkGroup = blanketGroupName;
747                }
748                else if (XmlHelper.pathExists(xpath, "./" + BLANKET_APPROVE_WORKGROUP_NAME, documentTypeNode)) {
749                blanketWorkGroup = (String) getXPath().evaluate("./" + BLANKET_APPROVE_WORKGROUP_NAME, documentTypeNode, XPathConstants.STRING);
750            }
751        } catch (XPathExpressionException xpee) {
752            LOG.error("Error obtaining document type " + BLANKET_APPROVE_GROUP_NAME, xpee);
753            throw xpee;
754        }
755        try {
756            // check if the blanket approve policy element tag was set on the ingested document type and get value if it was
757            if (XmlHelper.pathExists(xpath, "./" + BLANKET_APPROVE_POLICY, documentTypeNode)) {
758                blanketApprovePolicy =(String) getXPath().evaluate("./" + BLANKET_APPROVE_POLICY, documentTypeNode, XPathConstants.STRING);
759            }
760        } catch (XPathExpressionException xpee) {
761            LOG.error("Error obtaining document type " + BLANKET_APPROVE_POLICY, xpee);
762            throw xpee;
763        }
764        // first check to see if the user ingested both a workgroup name and a policy
765        if (StringUtils.isNotBlank(blanketWorkGroup) && StringUtils.isNotBlank(blanketApprovePolicy)) {
766            throw new XmlException("Only one of the blanket approve xml tags can be set");
767        }
768        else if (StringUtils.isNotBlank(blanketWorkGroup)) {
769            if (isOverwrite) {
770                // if overwrite mode is on we need to make sure we clear out the blanket approve policy in case that was the previous document type's method
771                documentType.setBlanketApprovePolicy(null);
772            }
773            if (StringUtils.isNotBlank(blanketGroupName)) { // Found a "blanketApproveGroupName" element.
774                documentType.setBlanketApproveWorkgroup(retrieveValidKimGroupUsingGroupNameAndNamespace(blanketGroupName, blanketNamespace));
775            } else { // Found a deprecated "blanketApproveWorkgroupName" element.
776                LOG.warn((new StringBuilder(160)).append("Document Type XML is using deprecated element '").append(BLANKET_APPROVE_WORKGROUP_NAME).append(
777                                        "', please use '").append(BLANKET_APPROVE_GROUP_NAME).append("' instead.").toString());
778                documentType.setBlanketApproveWorkgroup(retrieveValidKimGroupUsingUnparsedGroupName(blanketWorkGroup));
779            }
780        }
781        else if (StringUtils.isNotBlank(blanketApprovePolicy)) {
782            if (isOverwrite) {
783                // if overwrite mode is on we need to make sure we clear out the blanket approve workgroup in case that was the previous document type's method
784                documentType.setBlanketApproveWorkgroup(null);
785            }
786            documentType.setBlanketApprovePolicy(blanketApprovePolicy);
787        }
788
789        // set the reporting workgroup name on the document type
790        try {
791                if (XmlHelper.pathExists(xpath, "./" + REPORTING_GROUP_NAME, documentTypeNode)) {
792                        documentType.setReportingWorkgroup(retrieveValidKimGroup("./" + REPORTING_GROUP_NAME, documentTypeNode, false));
793                }
794                else if (XmlHelper.pathExists(xpath, "./" + REPORTING_WORKGROUP_NAME, documentTypeNode)) {
795                        LOG.warn((new StringBuilder(160)).append("Document Type XML is using deprecated element '").append(REPORTING_WORKGROUP_NAME).append(
796                                        "', please use '").append(REPORTING_GROUP_NAME).append("' instead.").toString());
797                documentType.setReportingWorkgroup(retrieveValidKimGroup("./" + REPORTING_WORKGROUP_NAME, documentTypeNode, true));
798            }
799        } catch (XPathExpressionException xpee) {
800            LOG.error("Error obtaining document type " + REPORTING_GROUP_NAME, xpee);
801            throw xpee;
802        }
803
804        // set the routing version on the document type
805        try {
806            /*
807             * - if the element tag is ingested... take whatever value is given 
808             * - we disregard the isOverwrite because if the element tag does not exist in the ingestion
809             * then the documentType should already carry the value from the previous document type
810             */
811            if (XmlHelper.pathExists(xpath, "./" + ROUTING_VERSION, documentTypeNode)) {
812                String version;
813                try {
814                    version = (String) getXPath().evaluate("./" + ROUTING_VERSION, documentTypeNode, XPathConstants.STRING);
815                } catch (XPathExpressionException xpee) {
816                    LOG.error("Error obtaining document type routingVersion", xpee);
817                    throw xpee;
818                }
819                // verify that the routing version is one of the two valid values
820                if (!(version.equals(KewApiConstants.ROUTING_VERSION_ROUTE_LEVEL) || version.equals(KewApiConstants.ROUTING_VERSION_NODAL))) {
821                    throw new WorkflowRuntimeException("Invalid routing version on document type: " + version);
822                }
823                documentType.setRoutingVersion(version);
824            }
825        } catch (XPathExpressionException xpee) {
826            LOG.error("Error obtaining document type routingVersion", xpee);
827            throw xpee;
828        }
829
830        // KULRICE-7786: Document Specific Doc Search Application Document Status should be available (and groupable)
831        // on the basic version of search.
832        //
833        // Side-effect: populates the application statuses and categories on the documentType
834        ApplicationDocumentStatusParser.parseValidApplicationStatuses(documentType, documentTypeNode, getXPath());
835
836        return documentType;
837    }
838
839    private Group retrieveValidKimGroup(String xpathExpression, Node documentTypeNode, boolean deprecatedGroupElement) throws XPathExpressionException, GroupNotFoundException {
840        String groupName;
841        String groupNamespace = null;
842        try {
843            groupName = (String) getXPath().evaluate(xpathExpression, documentTypeNode, XPathConstants.STRING);
844            // If not using the deprecated "namespaceCode:GroupName" format for group names, obtain the namespace from the element's "namespace" attribute.
845            if (!deprecatedGroupElement) {
846                groupNamespace = (String) getXPath().evaluate(xpathExpression + "/@" + NAMESPACE, documentTypeNode, XPathConstants.STRING);
847            }
848        } catch (XPathExpressionException xpee) {
849            LOG.error("Error obtaining document type workgroup using xpath expression: " + xpathExpression, xpee);
850            throw xpee;
851        }
852        // Use the appropriate method to retrieve the group, based on whether or not the deprecated "namespaceCode:groupName" naming pattern is in use. 
853        return (deprecatedGroupElement) ?
854                        retrieveValidKimGroupUsingUnparsedGroupName(groupName) : retrieveValidKimGroupUsingGroupNameAndNamespace(groupName, groupNamespace);
855    }
856
857    private Group retrieveValidKimGroupUsingGroupNameAndNamespace(String groupName, String groupNamespace) throws GroupNotFoundException {
858        // allow core config parameter replacement in documenttype workgroups
859        groupName = Utilities.substituteConfigParameters(groupName).trim();
860        groupNamespace = Utilities.substituteConfigParameters(groupNamespace).trim();
861        return retrieveValidKimGroupUsingProcessedGroupNameAndNamespace(groupName, groupNamespace);
862    }
863    
864    private Group retrieveValidKimGroupUsingUnparsedGroupName(String unparsedGroupName) throws GroupNotFoundException {
865        // allow core config parameter replacement in documenttype workgroups
866        unparsedGroupName = Utilities.substituteConfigParameters(unparsedGroupName);
867        String groupName = Utilities.parseGroupName(unparsedGroupName);
868        String groupNamespace = Utilities.parseGroupNamespaceCode(unparsedGroupName);
869        return retrieveValidKimGroupUsingProcessedGroupNameAndNamespace(groupName, groupNamespace);
870    }
871
872    private Group retrieveValidKimGroupUsingProcessedGroupNameAndNamespace(String groupName, String groupNamespace) throws GroupNotFoundException {
873        if (StringUtils.isBlank(groupNamespace) || StringUtils.isBlank(groupName)) {
874            throw new GroupNotFoundException("Valid Workgroup could not be found... Namespace: " + groupNamespace + "  Name: " + groupName);
875        }
876        Group workgroup = getGroupService().getGroupByNamespaceCodeAndName(groupNamespace, groupName);
877        if (workgroup == null) {
878            throw new GroupNotFoundException("Valid Workgroup could not be found... Namespace: " + groupNamespace + "  Name: " + groupName);
879        }
880        return workgroup;
881    }
882    
883    public DocumentType generateNewDocumentTypeFromExisting(String documentTypeName) throws SAXException, IOException, ParserConfigurationException, XPathExpressionException, GroupNotFoundException, WorkflowException {
884        // export the document type that exists in the database
885        DocumentType docTypeFromDatabase = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
886        if (ObjectUtils.isNotNull(docTypeFromDatabase)) {
887            KewExportDataSet kewExportDataSet = new KewExportDataSet();
888            kewExportDataSet.getDocumentTypes().add(docTypeFromDatabase);
889            byte[] xmlBytes = CoreApiServiceLocator.getXmlExporterService().export(kewExportDataSet.createExportDataSet());
890            // use the exported document type from the database to generate the new document type
891            Document tempDocument = XmlHelper.trimXml(new BufferedInputStream(new ByteArrayInputStream(xmlBytes)));
892            Node documentTypeNode = (Node) getXPath().evaluate("/" + DATA_ELEMENT + "/" + DOCUMENT_TYPES + "/" + DOCUMENT_TYPE, tempDocument, XPathConstants.NODE);
893            return getFullDocumentType(false, documentTypeNode);
894        }
895        return null;
896    }
897
898    private void routeDocumentType(DocumentType documentType) {
899        DocumentType docType = KEWServiceLocator.getDocumentTypeService().findByName(documentType.getName());
900        // if the docType exists then check locking
901        if (ObjectUtils.isNotNull(docType)) {
902            Maintainable docTypeMaintainable = new DocumentTypeMaintainable();
903            docTypeMaintainable.setBusinessObject(docType);
904            docTypeMaintainable.setBoClass(docType.getClass());
905            // below will throw a ValidationException if a valid locking document exists
906            MaintenanceUtils.checkForLockingDocument(docTypeMaintainable, true);
907        }
908        KEWServiceLocator.getDocumentTypeService().versionAndSave(documentType);
909    }
910
911    private String getDocumentTypeNameFromNode(Node documentTypeNode) throws XPathExpressionException {
912        try {
913            return (String) getXPath().evaluate("./name", documentTypeNode, XPathConstants.STRING);
914        } catch (XPathExpressionException xpee) {
915            LOG.error("Error obtaining document type name", xpee);
916            throw xpee;
917        }
918    }
919
920    /**
921     * Checks for route nodes that are declared but never used and throws an XmlException if one is discovered.
922     */
923    private void checkForOrphanedRouteNodes(Node documentTypeNode, Node routeNodesNode) throws XPathExpressionException, XmlException {
924        NodeList nodesInPath = (NodeList) getXPath().evaluate("./routePaths/routePath//*/@name", documentTypeNode, XPathConstants.NODESET);
925        List<String> nodeNamesInPath = new ArrayList<String>(nodesInPath.getLength());
926        for (int index = 0; index < nodesInPath.getLength(); index++) {
927            Node nameNode = nodesInPath.item(index);
928            nodeNamesInPath.add(nameNode.getNodeValue());
929        }
930
931        NodeList declaredNodes = (NodeList) getXPath().evaluate("./*/@name", routeNodesNode, XPathConstants.NODESET);
932        List<String> declaredNodeNames = new ArrayList<String>(declaredNodes.getLength());
933        for (int index = 0; index < declaredNodes.getLength(); index++) {
934            Node nameNode = declaredNodes.item(index);
935            declaredNodeNames.add(nameNode.getNodeValue());
936        }
937
938        // now compare the declared nodes to the ones actually used
939        List<String> orphanedNodes = new ArrayList<String>();
940        for (String declaredNode : declaredNodeNames) {
941            boolean foundNode = false;
942            for (String nodeInPath : nodeNamesInPath) {
943                if (nodeInPath.equals(declaredNode)) {
944                    foundNode = true;
945                    break;
946                }
947            }
948            if (!foundNode) {
949                orphanedNodes.add(declaredNode);
950            }
951        }
952        if (!orphanedNodes.isEmpty()) {
953            String message = "The following nodes were declared but never used: ";
954            for (Iterator iterator = orphanedNodes.iterator(); iterator.hasNext();) {
955                String orphanedNode = (String) iterator.next();
956                message += orphanedNode + (iterator.hasNext() ? ", " : "");
957            }
958            throw new XmlException(message);
959        }
960    }
961
962    private void createProcesses(NodeList processNodes, DocumentType documentType) {
963        for (int index = 0; index < processNodes.getLength(); index++) {
964            Node processNode = processNodes.item(index);
965            NamedNodeMap attributes = processNode.getAttributes();
966            Node processNameNode = attributes.getNamedItem(PROCESS_NAME);
967            String processName = (processNameNode == null ? null : processNameNode.getNodeValue());
968            ProcessDefinitionBo process = new ProcessDefinitionBo();
969            if (org.apache.commons.lang.StringUtils.isEmpty(processName)) {
970                process.setInitial(true);
971                process.setName(KewApiConstants.PRIMARY_PROCESS_NAME);
972            } else {
973                process.setInitial(false);
974                process.setName(processName);
975            }
976            process.setDocumentType(documentType);
977            documentType.addProcess(process);
978        }
979    }
980    
981    private void createEmptyProcess(DocumentType documentType) {
982            ProcessDefinitionBo process = new ProcessDefinitionBo();
983            
984            process.setInitial(true);
985            process.setName(KewApiConstants.PRIMARY_PROCESS_NAME);
986
987            process.setDocumentType(documentType);
988            documentType.addProcess(process);
989    }
990
991    private RoutePathContext findNodeOnXPath( String nodeName, RoutePathContext context,  Node routePathNode ) throws XPathExpressionException, XmlException {
992        Node currentNode;
993
994        while (context.nodeQName.length() > 1) {
995            context.nodeXPath = context.nodeXPath.substring(0,context.nodeXPath.lastIndexOf("//"));
996            context.nodeQName = context.nodeQName.substring(0,context.nodeQName.lastIndexOf(":", context.nodeQName.lastIndexOf(":")-1)+1);
997            if (StringUtils.isBlank( context.nodeQName)) {
998                context.nodeQName = ":";
999            }
1000
1001            try {
1002                 currentNode = (Node) getXPath().evaluate(context.nodeXPath + "//" + "*[@name = '" + nodeName +"']" , routePathNode, XPathConstants.NODE);
1003            } catch (XPathExpressionException xpee) {
1004                 LOG.error("Error obtaining routePath for routeNode", xpee);
1005                 throw xpee;
1006            }
1007
1008            if (currentNode != null) {
1009                 return  context;
1010            }
1011        }
1012
1013        return context;
1014    }
1015
1016    private RouteNode createRouteNode(RouteNode previousRouteNode, String nodeName, Node routePathNode, Node routeNodesNode, DocumentType documentType, RoutePathContext context) throws XPathExpressionException, XmlException, GroupNotFoundException {
1017        if (nodeName == null) return null;
1018
1019        Node currentNode;
1020        context.nodeXPath += "//" + "*[@name = '" + nodeName +"']";
1021        context.nodeQName += nodeName + ":";
1022
1023        try {
1024            currentNode = (Node) getXPath().evaluate(context.nodeXPath + "//" + "*[@name = '" + nodeName +"']" , routePathNode, XPathConstants.NODE);
1025            if (currentNode == null) {
1026                findNodeOnXPath( nodeName, context,  routePathNode );
1027                currentNode = (Node) getXPath().evaluate(context.nodeXPath + "//" + "*[@name = '" + nodeName +"']" , routePathNode, XPathConstants.NODE);
1028            }
1029        } catch (XPathExpressionException xpee) {
1030            LOG.error("Error obtaining routePath for routeNode", xpee);
1031            throw xpee;
1032        }
1033
1034        if (currentNode == null) {
1035            String message = "Next node '" + nodeName + "' for node '" + previousRouteNode.getRouteNodeName() + "' not found!";
1036            LOG.error(message);
1037            throw new XmlException(message);
1038        }
1039
1040        context.nodeXPath += "//" + "*[@name = '" + nodeName +"']";
1041        context.nodeQName += nodeName + ":";
1042        LOG.debug("nodeQNme:"+context.nodeQName);
1043        boolean nodeIsABranch;
1044
1045        try {
1046            nodeIsABranch = ((Boolean) getXPath().evaluate("self::node()[local-name() = 'branch']", currentNode, XPathConstants.BOOLEAN)).booleanValue();
1047        } catch (XPathExpressionException xpee) {
1048            LOG.error("Error testing whether node is a branch", xpee);
1049            throw xpee;
1050        }
1051
1052        if (nodeIsABranch) {
1053            throw new XmlException("Next node cannot be a branch node");
1054        }
1055
1056        String localName;
1057
1058        try {
1059            localName = (String) getXPath().evaluate("local-name(.)", currentNode, XPathConstants.STRING);
1060        } catch (XPathExpressionException xpee) {
1061            LOG.error("Error obtaining node local-name", xpee);
1062            throw xpee;
1063        }
1064
1065        RouteNode currentRouteNode = null;
1066
1067        if (nodesMap.containsKey(context.nodeQName)) {
1068            currentRouteNode = (RouteNode) nodesMap.get(context.nodeQName);
1069        } else {
1070            String nodeExpression = ".//*[@name='" + nodeName + "']";
1071            currentRouteNode = makeRouteNodePrototype(localName, nodeName, nodeExpression, routeNodesNode, documentType, context);
1072        }
1073
1074        if ("split".equalsIgnoreCase(localName)) {
1075            getSplitNextNodes(currentNode, routePathNode, currentRouteNode, routeNodesNode, documentType,  cloneContext(context));
1076        }
1077
1078        if (previousRouteNode != null) {
1079            previousRouteNode.getNextNodes().add(currentRouteNode);
1080            nodesMap.put(context.previousNodeQName, previousRouteNode);
1081            currentRouteNode.getPreviousNodes().add(previousRouteNode);
1082        }
1083
1084        String nextNodeName = null;
1085        boolean hasNextNodeAttrib;
1086
1087        try {
1088            hasNextNodeAttrib = ((Boolean) getXPath().evaluate(NEXT_NODE_EXP, currentNode, XPathConstants.BOOLEAN)).booleanValue();
1089        } catch (XPathExpressionException xpee) {
1090            LOG.error("Error obtaining node nextNode attrib", xpee);
1091            throw xpee;
1092        }
1093
1094        if (hasNextNodeAttrib) {
1095            // if the node has a nextNode but is not a split node, the nextNode is used for its node
1096            if (!"split".equalsIgnoreCase(localName)) {
1097                try {
1098                    nextNodeName = (String) getXPath().evaluate(NEXT_NODE_EXP, currentNode, XPathConstants.STRING);
1099                } catch (XPathExpressionException xpee) {
1100                    LOG.error("Error obtaining node nextNode attrib", xpee);
1101                    throw xpee;
1102                }
1103
1104                context.previousNodeQName = context.nodeQName;
1105                createRouteNode(currentRouteNode, nextNodeName, routePathNode, routeNodesNode, documentType, cloneContext(context));
1106            } else {
1107                // if the node has a nextNode but is a split node, the nextNode must be used for that split node's join node
1108                nodesMap.put(context.nodeQName, currentRouteNode);
1109            }
1110
1111        } else {
1112            // if the node has no nextNode of its own and is not a join which gets its nextNode from its parent split node
1113            if (!"join".equalsIgnoreCase(localName)) {
1114                nodesMap.put(context.nodeQName, currentRouteNode);
1115
1116            } else {    // if join has a parent nextNode (on its split node) and join has not already walked this path
1117                boolean parentHasNextNodeAttrib;
1118                try {
1119                    parentHasNextNodeAttrib = ((Boolean) getXPath().evaluate(PARENT_NEXT_NODE_EXP, currentNode, XPathConstants.BOOLEAN)).booleanValue();
1120                } catch (XPathExpressionException xpee) {
1121                    LOG.error("Error obtaining parent node nextNode attrib", xpee);
1122                    throw xpee;
1123                }
1124
1125                if (parentHasNextNodeAttrib && !nodesMap.containsKey(context.nodeQName)) {
1126                    try {
1127                        nextNodeName = (String) getXPath().evaluate(PARENT_NEXT_NODE_EXP, currentNode, XPathConstants.STRING);
1128                    } catch (XPathExpressionException xpee) {
1129                        LOG.error("Error obtaining parent node nextNode attrib", xpee);
1130                        throw xpee;
1131                    }
1132
1133                    context.previousNodeQName = context.nodeQName;
1134                    createRouteNode(currentRouteNode, nextNodeName, routePathNode, routeNodesNode, documentType, cloneContext(context));
1135                } else {
1136                    // if join's parent split node does not have a nextNode
1137                    nodesMap.put(context.nodeQName, currentRouteNode);
1138                }
1139            }
1140        }
1141
1142        // handle nextAppDocStatus route node attribute
1143        String nextDocStatusName = null;
1144        boolean hasNextDocStatus;
1145
1146        try {
1147            hasNextDocStatus = ((Boolean) getXPath().evaluate(NEXT_DOC_STATUS_EXP, currentNode, XPathConstants.BOOLEAN)).booleanValue();
1148        } catch (XPathExpressionException xpee) {
1149            LOG.error("Error obtaining node nextAppDocStatus attrib", xpee);
1150            throw xpee;
1151        }
1152
1153        if (hasNextDocStatus){
1154                try {
1155                        nextDocStatusName = (String) getXPath().evaluate(NEXT_DOC_STATUS_EXP, currentNode, XPathConstants.STRING);
1156                } catch (XPathExpressionException xpee) {
1157                        LOG.error("Error obtaining node nextNode attrib", xpee);
1158                        throw xpee;
1159                }
1160                
1161                //validate against allowable values if defined
1162                if (documentType.getValidApplicationStatuses() != null  && documentType.getValidApplicationStatuses().size() > 0){
1163                        Iterator iter = documentType.getValidApplicationStatuses().iterator();
1164                        boolean statusValidated = false;
1165
1166                while (iter.hasNext())
1167                        {
1168                                ApplicationDocumentStatus myAppDocStat = (ApplicationDocumentStatus) iter.next();
1169                                if (nextDocStatusName.compareToIgnoreCase(myAppDocStat.getStatusName()) == 0)
1170                                {
1171                                        statusValidated = true;
1172                                        break;
1173                                }
1174                        }
1175
1176                        if (!statusValidated){
1177                                        XmlException xpee = new XmlException("AppDocStatus value " +  nextDocStatusName + " not allowable.");
1178                                        LOG.error("Error validating nextAppDocStatus name: " +  nextDocStatusName + " against acceptable values.", xpee);
1179                                        throw xpee; 
1180                        }
1181                }
1182
1183                currentRouteNode.setNextDocStatus(nextDocStatusName);
1184        }
1185
1186        return currentRouteNode;
1187    }
1188
1189    private void getSplitNextNodes(Node splitNode, Node routePathNode, RouteNode splitRouteNode, Node routeNodesNode, DocumentType documentType, RoutePathContext context) throws XPathExpressionException, XmlException, GroupNotFoundException {
1190        NodeList splitBranchNodes;
1191
1192        try {
1193            splitBranchNodes = (NodeList) getXPath().evaluate("./branch", splitNode, XPathConstants.NODESET);
1194        } catch (XPathExpressionException xpee) {
1195            LOG.error("Error obtaining split node branch", xpee);
1196            throw xpee;
1197        }
1198
1199        String splitQName = context.nodeQName;
1200        String splitXPath = context.nodeXPath;
1201
1202        for (int i = 0; i < splitBranchNodes.getLength(); i++) {
1203            String branchName;
1204            try {
1205                branchName = (String) getXPath().evaluate("./@name", splitBranchNodes.item(i), XPathConstants.STRING);
1206            } catch (XPathExpressionException xpee) {
1207                LOG.error("Error obtaining branch name attribute", xpee);
1208                throw xpee;
1209            }
1210
1211            String name;
1212
1213            try {
1214                name = (String) getXPath().evaluate("./*[1]/@name", splitBranchNodes.item(i), XPathConstants.STRING);
1215            } catch (XPathExpressionException xpee) {
1216                LOG.error("Error obtaining first split branch node name", xpee);
1217                throw xpee;
1218            }
1219
1220            context.nodeQName = splitQName + branchName +":";
1221            context.nodeXPath = splitXPath  + "//" + "*[@name = '" + branchName +"']";
1222            context.branch = new BranchPrototype();
1223            context.branch.setName(branchName);
1224            context.previousNodeQName = splitQName;
1225
1226            createRouteNode(splitRouteNode, name, routePathNode, routeNodesNode, documentType, cloneContext(context));
1227        }
1228    }
1229
1230    private RouteNode makeRouteNodePrototype(String nodeTypeName, String nodeName, String nodeExpression, Node routeNodesNode, DocumentType documentType, RoutePathContext context) throws XPathExpressionException, GroupNotFoundException, XmlException {
1231        NodeList nodeList;
1232        try {
1233            nodeList = (NodeList) getXPath().evaluate(nodeExpression, routeNodesNode, XPathConstants.NODESET);
1234        } catch (XPathExpressionException xpee) {
1235            LOG.error("Error evaluating node expression: '" + nodeExpression + "'");
1236            throw xpee;
1237        }
1238
1239        if (nodeList.getLength() > 1) {
1240            throw new XmlException("More than one node under routeNodes has the same name of '" + nodeName + "'");
1241        }
1242
1243        if (nodeList.getLength() == 0) {
1244            throw new XmlException("No node definition was found with the name '" + nodeName + "'");
1245        }
1246
1247        Node node = nodeList.item(0);
1248
1249        RouteNode routeNode = new RouteNode();
1250        // set fields that all route nodes of all types should have defined
1251        routeNode.setDocumentType(documentType);
1252        routeNode.setRouteNodeName((String) getXPath().evaluate("./@name", node, XPathConstants.STRING));
1253        routeNode.setContentFragment(XmlJotter.jotNode(node));
1254
1255        if (XmlHelper.pathExists(xpath, "./activationType", node)) {
1256            routeNode.setActivationType(ActivationTypeEnum.parse((String) getXPath().evaluate("./activationType", node, XPathConstants.STRING)).getCode());
1257        } else {
1258            routeNode.setActivationType(DEFAULT_ACTIVATION_TYPE);
1259        }
1260
1261        Group exceptionWorkgroup = defaultExceptionWorkgroup;
1262
1263        String exceptionWg = null;
1264        String exceptionWorkgroupName = null;
1265        String exceptionWorkgroupNamespace = null;
1266
1267        if (XmlHelper.pathExists(xpath, "./" + EXCEPTION_GROUP_NAME, node)) {
1268                exceptionWorkgroupName = Utilities.substituteConfigParameters(
1269                                (String) getXPath().evaluate("./" + EXCEPTION_GROUP_NAME, node, XPathConstants.STRING)).trim();
1270                exceptionWorkgroupNamespace = Utilities.substituteConfigParameters(
1271                                (String) getXPath().evaluate("./" + EXCEPTION_GROUP_NAME + "/@" + NAMESPACE, node, XPathConstants.STRING)).trim();
1272        }
1273
1274        if (org.apache.commons.lang.StringUtils.isEmpty(exceptionWorkgroupName) && XmlHelper.pathExists(xpath, "./" + EXCEPTION_WORKGROUP_NAME, node)) {
1275                LOG.warn((new StringBuilder(160)).append("Document Type XML is using deprecated element '").append(EXCEPTION_WORKGROUP_NAME).append(
1276                                "', please use '").append(EXCEPTION_GROUP_NAME).append("' instead.").toString());
1277                // for backward compatibility we also need to be able to support exceptionWorkgroupName
1278                exceptionWg = Utilities.substituteConfigParameters((String) getXPath().evaluate("./" + EXCEPTION_WORKGROUP_NAME, node, XPathConstants.STRING));
1279                exceptionWorkgroupName = Utilities.parseGroupName(exceptionWg);
1280                exceptionWorkgroupNamespace = Utilities.parseGroupNamespaceCode(exceptionWg);
1281        }
1282
1283        if (org.apache.commons.lang.StringUtils.isEmpty(exceptionWorkgroupName) && XmlHelper.pathExists(xpath, "./" + EXCEPTION_WORKGROUP, node)) {
1284                LOG.warn((new StringBuilder(160)).append("Document Type XML is using deprecated element '").append(EXCEPTION_WORKGROUP).append(
1285                                "', please use '").append(EXCEPTION_GROUP_NAME).append("' instead.").toString());
1286            // for backward compatibility we also need to be able to support exceptionWorkgroup
1287            exceptionWg = Utilities.substituteConfigParameters((String) getXPath().evaluate("./" + EXCEPTION_WORKGROUP, node, XPathConstants.STRING));
1288            exceptionWorkgroupName = Utilities.parseGroupName(exceptionWg);
1289            exceptionWorkgroupNamespace = Utilities.parseGroupNamespaceCode(exceptionWg);
1290        }
1291
1292        if (org.apache.commons.lang.StringUtils.isEmpty(exceptionWorkgroupName)) {
1293            if (routeNode.getDocumentType().getDefaultExceptionWorkgroup() != null) {
1294                exceptionWorkgroupName = routeNode.getDocumentType().getDefaultExceptionWorkgroup().getName();
1295                exceptionWorkgroupNamespace = routeNode.getDocumentType().getDefaultExceptionWorkgroup().getNamespaceCode();
1296            }
1297        }
1298
1299        if (org.apache.commons.lang.StringUtils.isNotEmpty(exceptionWorkgroupName)
1300                && org.apache.commons.lang.StringUtils.isNotEmpty(exceptionWorkgroupNamespace)) {
1301            exceptionWorkgroup = getGroupService().getGroupByNamespaceCodeAndName(exceptionWorkgroupNamespace,
1302                    exceptionWorkgroupName);
1303            if (exceptionWorkgroup == null) {
1304                throw new GroupNotFoundException("Could not locate exception workgroup with namespace '" + exceptionWorkgroupNamespace + "' and name '" + exceptionWorkgroupName + "'");
1305            }
1306        } else {
1307            if (StringUtils.isEmpty(exceptionWorkgroupName) ^ StringUtils.isEmpty(exceptionWorkgroupNamespace)) {
1308                throw new GroupNotFoundException("Could not locate exception workgroup with namespace '" + exceptionWorkgroupNamespace + "' and name '" + exceptionWorkgroupName + "'");
1309            }
1310        }
1311
1312        if (exceptionWorkgroup != null) {
1313            routeNode.setExceptionWorkgroupName(exceptionWorkgroup.getName());
1314            routeNode.setExceptionWorkgroupId(exceptionWorkgroup.getId());
1315        }
1316
1317        if (((Boolean) getXPath().evaluate("./mandatoryRoute", node, XPathConstants.BOOLEAN)).booleanValue()) {
1318            routeNode.setMandatoryRouteInd(Boolean.valueOf((String)getXPath().evaluate("./mandatoryRoute", node, XPathConstants.STRING)));
1319        } else {
1320            routeNode.setMandatoryRouteInd(Boolean.FALSE);
1321        }
1322
1323        if (((Boolean) getXPath().evaluate("./finalApproval", node, XPathConstants.BOOLEAN)).booleanValue()) {
1324            routeNode.setFinalApprovalInd(Boolean.valueOf((String)getXPath().evaluate("./finalApproval", node, XPathConstants.STRING)));
1325        } else {
1326            routeNode.setFinalApprovalInd(Boolean.FALSE);
1327        }
1328
1329        // for every simple child element of the node, store a config parameter of the element name and text content
1330        NodeList children = node.getChildNodes();
1331        for (int i = 0; i < children.getLength(); i++) {
1332            Node n = children.item(i);
1333            if (n instanceof Element) {
1334                Element e = (Element) n;
1335                String name = e.getNodeName();
1336                String content = getTextContent(e);
1337                routeNode.getConfigParams().add(new RouteNodeConfigParam(routeNode, name, content));
1338            }
1339        }
1340
1341        // make sure a default rule selector is set
1342        Map<String, String> cfgMap = Utilities.getKeyValueCollectionAsMap(routeNode.getConfigParams());
1343        if (!cfgMap.containsKey(RouteNode.RULE_SELECTOR_CFG_KEY)) {
1344            routeNode.getConfigParams().add(new RouteNodeConfigParam(routeNode, RouteNode.RULE_SELECTOR_CFG_KEY, FlexRM.DEFAULT_RULE_SELECTOR));
1345        }
1346
1347        if (((Boolean) getXPath().evaluate("./ruleTemplate", node, XPathConstants.BOOLEAN)).booleanValue()) {
1348            String ruleTemplateName = (String) getXPath().evaluate("./ruleTemplate", node, XPathConstants.STRING);
1349            RuleTemplateBo ruleTemplate = KEWServiceLocator.getRuleTemplateService().findByRuleTemplateName(ruleTemplateName);
1350
1351            if (ruleTemplate == null) {
1352                throw new XmlException("Rule template for node '" + routeNode.getRouteNodeName() + "' not found: " + ruleTemplateName);
1353            }
1354
1355            routeNode.setRouteMethodName(ruleTemplateName);
1356            routeNode.setRouteMethodCode(KewApiConstants.ROUTE_LEVEL_FLEX_RM);
1357        } else if (((Boolean) getXPath().evaluate("./routeModule", node, XPathConstants.BOOLEAN)).booleanValue()) {
1358            routeNode.setRouteMethodName((String) getXPath().evaluate("./routeModule", node, XPathConstants.STRING));
1359            routeNode.setRouteMethodCode(KewApiConstants.ROUTE_LEVEL_ROUTE_MODULE);
1360        } else if (((Boolean) getXPath().evaluate("./peopleFlows", node, XPathConstants.BOOLEAN)).booleanValue()) {
1361            routeNode.setRouteMethodCode(KewApiConstants.ROUTE_LEVEL_PEOPLE_FLOW);
1362        } else if (((Boolean) getXPath().evaluate("./rulesEngine", node, XPathConstants.BOOLEAN)).booleanValue()) {
1363            // validate that the element has at least one of the two required attributes, XML schema does not provide a way for us to
1364            // check this so we must do so programatically
1365            Element rulesEngineElement = (Element)getXPath().evaluate("./rulesEngine", node, XPathConstants.NODE);
1366            String executorName = rulesEngineElement.getAttribute("executorName");
1367            String executorClass = rulesEngineElement.getAttribute("executorClass");
1368
1369            if (StringUtils.isBlank(executorName) && StringUtils.isBlank(executorClass)) {
1370                throw new XmlException("The rulesEngine declaration must have at least one of 'executorName' or 'executorClass' attributes.");
1371            } else if (StringUtils.isNotBlank(executorName) && StringUtils.isNotBlank(executorClass)) {
1372                throw new XmlException("Only one of 'executorName' or 'executorClass' may be declared on rulesEngine configuration, but both were declared.");
1373            }
1374
1375            routeNode.setRouteMethodCode(KewApiConstants.ROUTE_LEVEL_RULES_ENGINE);
1376        }
1377
1378        String nodeType = null;
1379
1380        if (((Boolean) getXPath().evaluate("./type", node, XPathConstants.BOOLEAN)).booleanValue()) {
1381            nodeType = (String) getXPath().evaluate("./type", node, XPathConstants.STRING);
1382        } else {
1383            String localName = (String) getXPath().evaluate("local-name(.)", node, XPathConstants.STRING);
1384
1385            if ("start".equalsIgnoreCase(localName)) {
1386                nodeType = "org.kuali.rice.kew.engine.node.InitialNode";
1387            } else if ("split".equalsIgnoreCase(localName)) {
1388                nodeType = "org.kuali.rice.kew.engine.node.SimpleSplitNode";
1389            } else if ("join".equalsIgnoreCase(localName)) {
1390                nodeType = "org.kuali.rice.kew.engine.node.SimpleJoinNode";
1391            } else if ("requests".equalsIgnoreCase(localName)) {
1392                nodeType = "org.kuali.rice.kew.engine.node.RequestsNode";
1393            } else if ("process".equalsIgnoreCase(localName)) {
1394                nodeType = "org.kuali.rice.kew.engine.node.SimpleSubProcessNode";
1395            } else if (NodeType.ROLE.getName().equalsIgnoreCase(localName)) {
1396                nodeType = RoleNode.class.getName();
1397            }
1398
1399        }
1400
1401        if (org.apache.commons.lang.StringUtils.isEmpty(nodeType)) {
1402            throw new XmlException("Could not determine node type for the node named '" + routeNode.getRouteNodeName() + "'");
1403        }
1404
1405        routeNode.setNodeType(nodeType);
1406
1407        String localName = (String) getXPath().evaluate("local-name(.)", node, XPathConstants.STRING);
1408
1409        if ("split".equalsIgnoreCase(localName)) {
1410            context.splitNodeStack.addFirst(routeNode);
1411        } else if ("join".equalsIgnoreCase(localName) && context.splitNodeStack.size() != 0) {
1412            // join node should have same branch prototype as split node
1413            RouteNode splitNode = (RouteNode)context.splitNodeStack.removeFirst();
1414            context.branch = splitNode.getBranch();
1415        } else if (NodeType.ROLE.getName().equalsIgnoreCase(localName)) {
1416            routeNode.setRouteMethodName(RoleRouteModule.class.getName());
1417            routeNode.setRouteMethodCode(KewApiConstants.ROUTE_LEVEL_ROUTE_MODULE);
1418        }
1419
1420        routeNode.setBranch(context.branch);
1421
1422        return routeNode;
1423    }
1424
1425    private static String getTextContent(org.w3c.dom.Element element) {
1426                NodeList children = element.getChildNodes();
1427        if (children.getLength() == 0) {
1428            return "";
1429        }
1430                Node node = children.item(0);
1431                return node.getNodeValue();
1432        }
1433
1434    private List getDocumentTypePolicies(NodeList documentTypePolicies, DocumentType documentType) throws XPathExpressionException, XmlException, ParserConfigurationException {
1435        List policies = new ArrayList();
1436        Set policyNames = new HashSet();
1437
1438        for (int i = 0; i < documentTypePolicies.getLength(); i++) {
1439            DocumentTypePolicy policy = new DocumentTypePolicy();
1440            policy.setDocumentType(documentType);
1441            try {
1442                String policyName = (String) getXPath().evaluate("./name", documentTypePolicies.item(i), XPathConstants.STRING);
1443                policy.setPolicyName(org.kuali.rice.kew.api.doctype.DocumentTypePolicy.fromCode(policyName).getCode().toUpperCase());
1444            } catch (XPathExpressionException xpee) {
1445                LOG.error("Error obtaining document type policy name", xpee);
1446                throw xpee;
1447            }
1448            try {
1449                if (((Boolean) getXPath().evaluate("./value", documentTypePolicies.item(i), XPathConstants.BOOLEAN)).booleanValue()) {
1450                    policy.setPolicyValue(Boolean.valueOf((String) getXPath().evaluate("./value", documentTypePolicies.item(i), XPathConstants.STRING)));
1451                } else {
1452                    policy.setPolicyValue(Boolean.FALSE);
1453                }
1454            } catch (XPathExpressionException xpee) {
1455                LOG.error("Error obtaining document type policy value", xpee);
1456                throw xpee;
1457            }
1458            try {
1459                String policyStringValue = (String) getXPath().evaluate("./stringValue", documentTypePolicies.item(i), XPathConstants.STRING);
1460                if (!StringUtils.isEmpty(policyStringValue)){
1461                        policy.setPolicyStringValue(policyStringValue.toUpperCase());
1462                        policy.setPolicyValue(Boolean.TRUE);
1463                        // if DocumentStatusPolicy, check against allowable values
1464                        if (KewApiConstants.DOCUMENT_STATUS_POLICY.equalsIgnoreCase(org.kuali.rice.kew.api.doctype.DocumentTypePolicy.fromCode(policy.getPolicyName()).getCode())){
1465                                boolean found = false;
1466                                for (int index=0; index<KewApiConstants.DOCUMENT_STATUS_POLICY_VALUES.length; index++) {
1467                                        if (KewApiConstants.DOCUMENT_STATUS_POLICY_VALUES[index].equalsIgnoreCase(policyStringValue)){
1468                                                found = true;
1469                                                break;
1470                                        }                                               
1471                                }
1472                                if (!found){
1473                                        throw new XmlException("Application Document Status string value: " + policyStringValue + " is invalid.");
1474                                }
1475                        }
1476                        
1477                } else {
1478                        //DocumentStatusPolicy requires a <stringValue> tag
1479                        if (KewApiConstants.DOCUMENT_STATUS_POLICY.equalsIgnoreCase(org.kuali.rice.kew.api.doctype.DocumentTypePolicy.fromCode(policy.getPolicyName()).getCode())){
1480                                throw new XmlException("Application Document Status Policy requires a <stringValue>");
1481                        }
1482
1483                    String customConfig = parseDocumentPolicyCustomXMLConfig(documentTypePolicies.item(i));
1484                    if (customConfig != null) {
1485                        policy.setPolicyStringValue(customConfig);
1486                    }
1487                }
1488            } catch (XPathExpressionException xpee) {
1489                LOG.error("Error obtaining document type policy string value", xpee);
1490                throw xpee;
1491            }
1492            
1493            if (!policyNames.add(policy.getPolicyName())) {
1494                throw new XmlException("Policy '" + policy.getPolicyName() + "' has already been defined on this document");
1495            } else {
1496                policies.add(policy);
1497            }
1498        }
1499
1500        return policies;
1501    }
1502
1503    /**
1504     * Parses any custom XML configuration if present and returns it as an XML string.
1505     * If no custom XML elements are present in the policy configuration, null is returned.
1506     * @param policyNode the document type policy Node
1507     * @return XML configuration string or null
1508     * @throws ParserConfigurationException
1509     */
1510    private static String parseDocumentPolicyCustomXMLConfig(Node policyNode) throws ParserConfigurationException {
1511        final Collection<String> KNOWN_POLICY_ELEMENTS = Arrays.asList(new String[] { "name", "value", "stringValue" });
1512        // Store any other elements as XML in the policy string value
1513        Document policyConfig = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
1514        Element root = policyConfig.createElement("config");
1515        policyConfig.appendChild(root);
1516        // add unknown nodes from the config document
1517        NodeList children = policyNode.getChildNodes();
1518        for (int j = 0; j < children.getLength(); j++) {
1519            Node c = children.item(j);
1520            if (c instanceof Element && !KNOWN_POLICY_ELEMENTS.contains(c.getNodeName())) {
1521                root.appendChild(policyConfig.importNode(c, true));
1522            }
1523        }
1524        // if there are in-fact custom xml configuration nodes, then go ahead and save the config doc as XML
1525        return (root.getChildNodes().getLength() > 0) ? XmlJotter.jotDocument(policyConfig) : null;
1526    }
1527
1528    private List getDocumentTypeAttributes(NodeList documentTypeAttributes, DocumentType documentType) throws XPathExpressionException, WorkflowException {
1529        List attributes = new ArrayList();
1530
1531        for (int i = 0; i < documentTypeAttributes.getLength(); i++) {
1532            DocumentTypeAttributeBo attribute = new DocumentTypeAttributeBo();
1533            attribute.setDocumentType(documentType);
1534            String ruleAttributeName;
1535
1536            try {
1537                ruleAttributeName = (String) getXPath().evaluate("./name", documentTypeAttributes.item(i), XPathConstants.STRING);
1538            } catch (XPathExpressionException xpee) {
1539                LOG.error("Error obtaining rule attribute name", xpee);
1540                throw xpee;
1541            }
1542
1543            RuleAttribute ruleAttribute = KEWServiceLocator.getRuleAttributeService().findByName(ruleAttributeName);
1544
1545            if (ruleAttribute == null) {
1546                throw new WorkflowException("Could not find rule attribute: " + ruleAttributeName);
1547            }
1548
1549            attribute.setDocumentType(documentType);
1550            attribute.setRuleAttribute(ruleAttribute);
1551            attribute.setOrderIndex(i+1);
1552            attributes.add(attribute);
1553        }
1554
1555        return attributes;
1556    }
1557
1558    private class RoutePathContext {
1559        public BranchPrototype branch;
1560        public LinkedList splitNodeStack = new LinkedList();
1561        public String nodeXPath = ".";
1562        public String nodeQName = ":";
1563        public String previousNodeQName = "";
1564    }
1565
1566    private RoutePathContext cloneContext(RoutePathContext context){
1567        RoutePathContext localContext = new RoutePathContext();
1568        localContext.branch            = context.branch;
1569        localContext.nodeQName         = context.nodeQName;
1570        localContext.nodeXPath         = context.nodeXPath;
1571        localContext.previousNodeQName = context.previousNodeQName;
1572
1573        return localContext;
1574    }
1575
1576    protected GroupService getGroupService() {
1577        return KimApiServiceLocator.getGroupService();
1578    }
1579
1580    /**
1581     * This is a helper class for indicating if an unprocessed document type node is "standard" or "routing."
1582     * 
1583     * @author Kuali Rice Team (rice.collab@kuali.org)
1584     */
1585    private class DocTypeNode {
1586        /** The Node that needs to be converted into a doc type. */
1587        public final Node docNode;
1588        /** A flag for indicating the document's type; true indicates "standard," false indicates "routing." */
1589        public final boolean isStandard;
1590        
1591        /**
1592         * Constructs a DocTypeNode instance containing the specified Node and flag.
1593         * 
1594         * @param newNode The unprocessed document type.
1595         * @param newFlag An indicator of what type of document this is (true for "standard," false for "routing").
1596         */
1597        public DocTypeNode(Node newNode, boolean newFlag) {
1598            docNode = newNode;
1599            isStandard = newFlag;
1600        }
1601    }
1602
1603    /**
1604     * Utility class used to produce an ascending sequence of integer values starting with 0
1605     */
1606    private static class Counter {
1607        private int value = 0;
1608
1609        public int nextValue() {
1610            return value++;
1611        }
1612    }
1613
1614    /**
1615     * <p>Helper class encapsulating parsing logic for building ApplicationDocumentStatus and
1616     * ApplicationDocumentStatusCategory from the document type DOM.</p>
1617     *
1618     * <p>Note that this class should not be instantiated.  Instead use the static utility method 
1619     * {@link org.kuali.rice.kew.xml.DocumentTypeXmlParser.ApplicationDocumentStatusParser#parseValidApplicationStatuses(org.kuali.rice.kew.doctype.bo.DocumentType, org.w3c.dom.Node, javax.xml.xpath.XPath)}.</p>
1620     */
1621    private static class ApplicationDocumentStatusParser {
1622
1623        // set used to make sure there are no problematic or confusing name collisions
1624        private final Set<String> uniqueStatusNames = new HashSet<String>();
1625
1626        private final List<ApplicationDocumentStatus> parsedStatuses = new ArrayList<ApplicationDocumentStatus>();
1627        private final List<ApplicationDocumentStatusCategory> parsedCategories = new ArrayList<ApplicationDocumentStatusCategory>();
1628
1629        Counter newStatusSequence = new Counter(); // used to order the statuses
1630
1631        /**
1632         * Do not instantiate, instead use the static utility method
1633         * {@link #parseValidApplicationStatuses(org.kuali.rice.kew.doctype.bo.DocumentType, org.w3c.dom.Node, javax.xml.xpath.XPath)}
1634         */
1635        @Deprecated  // deprecated to discourage improper use since private is not strong enough in an inner class
1636        private ApplicationDocumentStatusParser() { }
1637
1638        /**
1639         *
1640         * @param documentType
1641         * @param documentTypeNode
1642         * @param xpath
1643         * @throws XPathExpressionException
1644         */
1645        @SuppressWarnings("deprecated")
1646        public static void parseValidApplicationStatuses(DocumentType documentType, Node documentTypeNode, XPath xpath) throws XPathExpressionException {
1647            ApplicationDocumentStatusParser parser = new ApplicationDocumentStatusParser();
1648            parser.parseValidApplicationStatusesHelper(documentType, documentTypeNode, xpath);
1649        }
1650
1651        /**
1652         * Parses the validApplicationStatuses in the doc type (if there are any) and (<b>SIDE EFFECT</b>) adds the
1653         * results to the given documentType.
1654         *
1655         * @param documentType the documentType being built
1656         * @param documentTypeNode the DOM {@link Node} for the xml representation of the document type
1657         * @param xpath the XPath object to use for searching within the documentTypeNode
1658         * @throws XPathExpressionException
1659         */
1660        private void parseValidApplicationStatusesHelper(DocumentType documentType, Node documentTypeNode, XPath xpath)
1661                throws XPathExpressionException {
1662            /*
1663            * The following code does not need to apply the isOverwrite mode logic because it already checks to see if each node
1664            * is available on the ingested XML. If the node is ingested then it doesn't matter if we're in overwrite mode or not
1665            * the ingested code should save.
1666            */
1667            NodeList appDocStatusList = (NodeList) xpath.evaluate("./" + APP_DOC_STATUSES, documentTypeNode,
1668                    XPathConstants.NODESET);
1669            if (appDocStatusList.getLength() > 1) {
1670                // more than one <validDocumentStatuses> tag is invalid
1671                throw new XmlException("More than one " + APP_DOC_STATUSES + " node is present in a document type node");
1672
1673            } else if (appDocStatusList.getLength() > 0) {
1674
1675                // if there is exactly one <validDocumentStatuses> tag then parse it and use the values
1676                parseCategoriesAndStatusesHelper(documentType, appDocStatusList);
1677            }
1678        }
1679
1680        /**
1681         * <p>Parses the application document status content (both statuses and categories) from the given NodeList.</p>
1682         *
1683         * <p>SIDE EFFECT: sets the ValidApplicationStatuses and ApplicationStatusCategories on the DocumentType
1684         * argument</p>
1685         *
1686         * @param documentType the documentType that is being built from the DOM content
1687         * @param appDocStatusList the NodeList holding the children of the validApplicationStatuses element
1688         */
1689        private void parseCategoriesAndStatusesHelper(DocumentType documentType, NodeList appDocStatusList) {
1690
1691            NodeList appDocStatusChildren = appDocStatusList.item(0).getChildNodes();
1692
1693            for (int appDocStatusChildrenIndex = 0; appDocStatusChildrenIndex < appDocStatusChildren.getLength();
1694                 appDocStatusChildrenIndex ++) {
1695
1696                Node child = appDocStatusChildren.item(appDocStatusChildrenIndex);
1697
1698                if (STATUS.equals(child.getNodeName())) {
1699
1700                    ApplicationDocumentStatus status =
1701                            parseApplicationDocumentStatusHelper(documentType, child);
1702
1703                    parsedStatuses.add(status);
1704
1705                } else if (CATEGORY.equals(child.getNodeName())) {
1706
1707                    Node categoryNameAttribute = child.getAttributes().getNamedItem("name");
1708                    String categoryName = categoryNameAttribute.getNodeValue();
1709
1710                    // validate category names are all unique
1711                    validateUniqueName(categoryName);
1712
1713                    ApplicationDocumentStatusCategory category = new ApplicationDocumentStatusCategory();
1714                    category.setCategoryName(categoryName);
1715                    category.setDocumentType(documentType);
1716
1717                    // iterate through children
1718                    NodeList categoryChildren = child.getChildNodes();
1719                    for (int categoryChildIndex = 0; categoryChildIndex < categoryChildren.getLength();
1720                         categoryChildIndex++) {
1721                        Node categoryChild = categoryChildren.item(categoryChildIndex);
1722                        if (Node.ELEMENT_NODE == categoryChild.getNodeType()) {
1723                            ApplicationDocumentStatus status =
1724                                    parseApplicationDocumentStatusHelper(documentType, categoryChild);
1725                            status.setCategoryName(category.getCategoryName());
1726
1727                            parsedStatuses.add(status);
1728                        }
1729                    }
1730
1731                    parsedCategories.add(category);
1732                }
1733            }
1734
1735            documentType.setValidApplicationStatuses(parsedStatuses);
1736            documentType.setApplicationStatusCategories(parsedCategories);
1737        }
1738
1739        /**
1740         * Parse an element Node containg a single application document status name and return an
1741         * ApplicationDocumentStatus object for it.
1742         *
1743         * @param documentType the document type we are building
1744         * @param node the status element node
1745         * @return the ApplicationDocumentStatus constructed
1746         */
1747        private ApplicationDocumentStatus parseApplicationDocumentStatusHelper(DocumentType documentType, Node node) {
1748            String statusName = node.getTextContent();
1749            validateUniqueName(statusName);
1750
1751            ApplicationDocumentStatus status = new ApplicationDocumentStatus();
1752            status.setStatusName(statusName);
1753            status.setDocumentType(documentType);
1754            // order the statuses according to the order we encounter them
1755            status.setSequenceNumber(newStatusSequence.nextValue());
1756
1757            return status;
1758        }
1759
1760        /**
1761         * <p>check that the name has not been used already.
1762         *
1763         * <p>Side-effect: This check adds the name to the uniqueStatusNames Set.
1764         * @param name
1765         * @throws XmlException if the name has already been used for another status or category
1766         */
1767        private void validateUniqueName(String name) throws XmlException {
1768            // validate category names are all unique
1769            if (!uniqueStatusNames.add(name)) {
1770                throw new XmlException("duplicate application document status / category name in document type: "+name);
1771            }
1772        }
1773
1774    }
1775}