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