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