001/** 002 * Copyright 2005-2017 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}