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.export;
017
018import org.apache.commons.lang.StringUtils;
019import org.jdom.Document;
020import org.jdom.Element;
021import org.jdom.JDOMException;
022import org.jdom.input.SAXBuilder;
023import org.kuali.rice.core.api.impex.ExportDataSet;
024import org.kuali.rice.core.api.util.xml.XmlException;
025import org.kuali.rice.core.api.util.xml.XmlHelper;
026import org.kuali.rice.core.api.util.xml.XmlRenderer;
027import org.kuali.rice.core.framework.impex.xml.XmlExporter;
028import org.kuali.rice.kew.api.WorkflowRuntimeException;
029import org.kuali.rice.kew.doctype.ApplicationDocumentStatus;
030import org.kuali.rice.kew.doctype.ApplicationDocumentStatusCategory;
031import org.kuali.rice.kew.doctype.DocumentTypeAttributeBo;
032import org.kuali.rice.kew.doctype.DocumentTypePolicy;
033import org.kuali.rice.kew.doctype.bo.DocumentType;
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.RouteNode;
038import org.kuali.rice.kew.api.exception.ResourceUnavailableException;
039import org.kuali.rice.kew.export.KewExportDataSet;
040import org.kuali.rice.kew.service.KEWServiceLocator;
041import org.kuali.rice.kew.api.KewApiConstants;
042import org.kuali.rice.kim.api.group.Group;
043
044import java.io.IOException;
045import java.io.StringReader;
046import java.util.Collection;
047import java.util.Collections;
048import java.util.Comparator;
049import java.util.Iterator;
050import java.util.List;
051
052import static org.kuali.rice.core.api.impex.xml.XmlConstants.*;
053
054/**
055 * Exports {@link DocumentType}s to XML.
056 *
057 * @see DocumentType
058 *
059 * @author Kuali Rice Team (rice.collab@kuali.org)
060 */
061public class DocumentTypeXmlExporter implements XmlExporter {
062
063    protected final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(getClass());
064
065    private XmlRenderer renderer = new XmlRenderer(DOCUMENT_TYPE_NAMESPACE);
066
067        @Override
068        public boolean supportPrettyPrint() {
069                return true;
070        }
071
072        public Element export(ExportDataSet exportDataSet) {
073        KewExportDataSet dataSet = KewExportDataSet.fromExportDataSet(exportDataSet);
074        if (!dataSet.getDocumentTypes().isEmpty()) {
075            Collections.sort(dataSet.getDocumentTypes(), new DocumentTypeParentComparator());
076            Element rootElement = renderer.renderElement(null, DOCUMENT_TYPES);
077            rootElement.setAttribute(SCHEMA_LOCATION_ATTR, DOCUMENT_TYPE_SCHEMA_LOCATION, SCHEMA_NAMESPACE);
078            for (Iterator iterator = dataSet.getDocumentTypes().iterator(); iterator.hasNext();) {
079                DocumentType documentType = (DocumentType) iterator.next();
080                exportDocumentType(rootElement, documentType);
081            }
082            return rootElement;
083        }
084        return null;
085    }
086
087    private void exportDocumentType(Element parent, DocumentType documentType) {
088        Element docTypeElement = renderer.renderElement(parent, DOCUMENT_TYPE);
089        List flattenedNodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(documentType, false);
090        // derive a default exception workgroup by looking at how the nodes are configured
091        boolean hasDefaultExceptionWorkgroup = hasDefaultExceptionWorkgroup(flattenedNodes);
092        renderer.renderTextElement(docTypeElement, NAME, documentType.getName());
093        if (documentType.getParentDocType() != null) {
094            renderer.renderTextElement(docTypeElement, PARENT, documentType.getParentDocType().getName());
095        }
096        renderer.renderTextElement(docTypeElement, DESCRIPTION, documentType.getDescription());
097        renderer.renderTextElement(docTypeElement, LABEL, documentType.getLabel());
098        if (!StringUtils.isBlank(documentType.getActualApplicationId())) {
099            renderer.renderTextElement(docTypeElement, APPLICATION_ID, documentType.getActualApplicationId());
100        }
101        renderer.renderTextElement(docTypeElement, POST_PROCESSOR_NAME, documentType.getPostProcessorName());
102
103        renderer.renderTextElement(docTypeElement, AUTHORIZER, documentType.getAuthorizer());
104
105        Group superUserWorkgroup = documentType.getSuperUserWorkgroupNoInheritence();
106        if (superUserWorkgroup != null) {
107                Element superUserGroupElement = renderer.renderTextElement(docTypeElement, SUPER_USER_GROUP_NAME, superUserWorkgroup.getName().trim());
108                superUserGroupElement.setAttribute(NAMESPACE, superUserWorkgroup.getNamespaceCode().trim());
109        }
110        Group blanketWorkgroup = documentType.getBlanketApproveWorkgroup();
111        if (blanketWorkgroup != null){
112                Element blanketGroupElement = renderer.renderTextElement(docTypeElement, BLANKET_APPROVE_GROUP_NAME, blanketWorkgroup.getName().trim());
113                blanketGroupElement.setAttribute(NAMESPACE, blanketWorkgroup.getNamespaceCode().trim());
114        }
115        if (documentType.getBlanketApprovePolicy() != null){
116                renderer.renderTextElement(docTypeElement, BLANKET_APPROVE_POLICY, documentType.getBlanketApprovePolicy());
117        }
118        Group reportingWorkgroup = documentType.getReportingWorkgroup();
119        if (reportingWorkgroup != null) {
120                Element reportingGroupElement = renderer.renderTextElement(docTypeElement, REPORTING_GROUP_NAME, reportingWorkgroup.getName().trim());
121                reportingGroupElement.setAttribute(NAMESPACE, reportingWorkgroup.getNamespaceCode().trim());
122        }
123        if (!flattenedNodes.isEmpty() && hasDefaultExceptionWorkgroup) {
124                Group exceptionWorkgroup = ((RouteNode)flattenedNodes.get(0)).getExceptionWorkgroup();
125                if (exceptionWorkgroup != null) {
126                        Element exceptionGroupElement = renderer.renderTextElement(docTypeElement, DEFAULT_EXCEPTION_GROUP_NAME, exceptionWorkgroup.getName().trim());
127                        exceptionGroupElement.setAttribute(NAMESPACE, exceptionWorkgroup.getNamespaceCode().trim());
128                }
129        }
130        if (StringUtils.isNotBlank(documentType.getUnresolvedDocHandlerUrl())) {
131            renderer.renderTextElement(docTypeElement, DOC_HANDLER, documentType.getUnresolvedDocHandlerUrl());
132        }
133        if (!StringUtils.isBlank(documentType.getUnresolvedHelpDefinitionUrl())) {
134            renderer.renderTextElement(docTypeElement, HELP_DEFINITION_URL, documentType.getUnresolvedHelpDefinitionUrl());
135        }
136        if (!StringUtils.isBlank(documentType.getUnresolvedDocSearchHelpUrl())) {
137            renderer.renderTextElement(docTypeElement, DOC_SEARCH_HELP_URL, documentType.getUnresolvedDocSearchHelpUrl());
138        }
139        if (!StringUtils.isBlank(documentType.getActualNotificationFromAddress())) {
140                renderer.renderTextElement(docTypeElement, NOTIFICATION_FROM_ADDRESS, documentType.getActualNotificationFromAddress());
141        }
142        renderer.renderBooleanElement(docTypeElement, ACTIVE, documentType.getActive(), true);
143        exportPolicies(docTypeElement, documentType.getDocumentTypePolicies());
144        exportAttributes(docTypeElement, documentType.getDocumentTypeAttributes());
145        exportSecurity(docTypeElement, documentType.getDocumentTypeSecurityXml());
146        if (!StringUtils.isBlank(documentType.getRoutingVersion())) {
147                renderer.renderTextElement(docTypeElement, ROUTING_VERSION, documentType.getRoutingVersion());
148        }
149        exportApplicationStatusCategories(docTypeElement, documentType);
150        ProcessDefinitionBo process = null;
151        if (documentType.getProcesses().size() > 0) {
152            process = (ProcessDefinitionBo)documentType.getProcesses().get(0);
153        }
154        if (process != null && process.getInitialRouteNode() != null) {
155            exportRouteData(docTypeElement, documentType, flattenedNodes, hasDefaultExceptionWorkgroup);
156        } else {
157            renderer.renderElement(docTypeElement, ROUTE_PATHS);
158        }
159    }
160
161    private void exportPolicies(Element parent, Collection policies) {
162        if (!policies.isEmpty()) {
163            Element policiesElement = renderer.renderElement(parent, POLICIES);
164            for (Iterator iterator = policies.iterator(); iterator.hasNext();) {
165                DocumentTypePolicy policy = (DocumentTypePolicy) iterator.next();
166                Element policyElement = renderer.renderElement(policiesElement, POLICY);
167                renderer.renderTextElement(policyElement, NAME, policy.getPolicyName());
168                if (StringUtils.isNotEmpty(policy.getPolicyStringValue())) {
169                    renderer.renderTextElement(policyElement, STRING_VALUE, policy.getPolicyStringValue());
170                }  else {
171                    renderer.renderBooleanElement(policyElement, VALUE, policy.getPolicyValue(), false);
172                }
173            }
174        }
175    }
176
177    private void exportAttributes(Element parent, List attributes) {
178        if (!attributes.isEmpty()) {
179            Element attributesElement = renderer.renderElement(parent, ATTRIBUTES);
180            for (Iterator iterator = attributes.iterator(); iterator.hasNext();) {
181                DocumentTypeAttributeBo attribute = (DocumentTypeAttributeBo) iterator.next();
182                Element attributeElement = renderer.renderElement(attributesElement, ATTRIBUTE);
183                renderer.renderTextElement(attributeElement, NAME, attribute.getRuleAttribute().getName());
184            }
185        }
186    }
187
188    private void exportSecurity(Element parent, String securityXML) {
189      if (!org.apache.commons.lang.StringUtils.isEmpty(securityXML)) {
190        try {
191          org.jdom.Document securityDoc = new SAXBuilder().build(new StringReader(securityXML));
192          XmlHelper.propagateNamespace(securityDoc.getRootElement(), DOCUMENT_TYPE_NAMESPACE);
193          parent.addContent(securityDoc.getRootElement().detach());
194        } catch (IOException e) {
195          throw new WorkflowRuntimeException("Error parsing doctype security XML.");
196        } catch (JDOMException e) {
197          throw new WorkflowRuntimeException("Error parsing doctype security XML.");
198        }
199      }
200    }
201
202    private void exportApplicationStatusCategories(Element parent, DocumentType documentType) {
203        List<ApplicationDocumentStatusCategory> appDocStatCategories = documentType.getApplicationStatusCategories();
204        List<ApplicationDocumentStatus> appDocStats = documentType.getValidApplicationStatuses();
205
206        if (appDocStatCategories != null && !appDocStatCategories.isEmpty()) {
207            Element appDocStatCategoriesElement = renderer.renderElement(parent, APP_DOC_STATUSES);
208            for (Iterator iterator = appDocStatCategories.iterator(); iterator.hasNext();) {
209                ApplicationDocumentStatusCategory appDocStatCategory = (ApplicationDocumentStatusCategory) iterator.next();
210                Element appStatusCatElement = renderer.renderElement(appDocStatCategoriesElement, CATEGORY);
211                appStatusCatElement.setAttribute(NAME, appDocStatCategory.getCategoryName().trim());
212                if(appDocStats != null) {
213                    for (Iterator iterator2 = appDocStats.iterator(); iterator2.hasNext();) {
214                        ApplicationDocumentStatus appDocStat = (ApplicationDocumentStatus) iterator2.next();
215                        if  (StringUtils.equals(appDocStat.getCategoryName(), appDocStatCategory.getCategoryName())) {
216                            renderer.renderTextElement(appStatusCatElement, STATUS, appDocStat.getStatusName());
217                        }
218                    }
219                }
220            }
221
222            for (Iterator iterator = appDocStats.iterator(); iterator.hasNext();) {
223                ApplicationDocumentStatus appDocStat = (ApplicationDocumentStatus) iterator.next();
224                if  (StringUtils.isEmpty(appDocStat.getCategoryName())) {
225                    renderer.renderTextElement(appDocStatCategoriesElement, STATUS, appDocStat.getStatusName());
226                }
227            }
228        }
229    }
230
231    private void exportRouteData(Element parent, DocumentType documentType, List flattenedNodes, boolean hasDefaultExceptionWorkgroup) {
232        if (!flattenedNodes.isEmpty()) {
233            Element routePathsElement = renderer.renderElement(parent, ROUTE_PATHS);
234            for (Iterator iterator = documentType.getProcesses().iterator(); iterator.hasNext();) {
235                ProcessDefinitionBo process = (ProcessDefinitionBo) iterator.next();
236                Element routePathElement = renderer.renderElement(routePathsElement, ROUTE_PATH);
237                if (!process.isInitial() && process.getInitialRouteNode() != null) {
238                    renderer.renderAttribute(routePathElement, INITIAL_NODE, process.getInitialRouteNode().getRouteNodeName());
239                    renderer.renderAttribute(routePathElement, PROCESS_NAME, process.getName());
240                }
241                exportProcess(routePathElement, process);
242            }
243
244            Element routeNodesElement = renderer.renderElement(parent, ROUTE_NODES);
245            for (Iterator iterator = flattenedNodes.iterator(); iterator.hasNext();) {
246                RouteNode node = (RouteNode) iterator.next();
247                exportRouteNode(routeNodesElement, node, hasDefaultExceptionWorkgroup);
248            }
249        }
250    }
251
252    /* default exception workgroup is not stored independently in db, so derive
253     * one from the definition itself: if all nodes have the *same* exception workgroup name
254     * defined, then this is tantamount to a *default* exception workgroup and can be
255     * used as such.
256     */
257    private boolean hasDefaultExceptionWorkgroup(List flattenedNodes) {
258        boolean hasDefaultExceptionWorkgroup = true;
259        String exceptionWorkgroupName = null;
260        for (Iterator iterator = flattenedNodes.iterator(); iterator.hasNext();) {
261            RouteNode node = (RouteNode) iterator.next();
262            if (exceptionWorkgroupName == null) {
263                exceptionWorkgroupName = node.getExceptionWorkgroupName();
264            }
265            if (exceptionWorkgroupName == null || !exceptionWorkgroupName.equals(node.getExceptionWorkgroupName())) {
266                hasDefaultExceptionWorkgroup = false;
267                break;
268            }
269        }
270        return hasDefaultExceptionWorkgroup;
271    }
272
273    private void exportProcess(Element parent, ProcessDefinitionBo process) {
274        exportNodeGraph(parent, process.getInitialRouteNode(), null);
275    }
276
277    private void exportNodeGraph(Element parent, RouteNode node, SplitJoinContext splitJoinContext) {
278        NodeType nodeType = null;
279
280        if (node != null) {
281            String contentFragment = node.getContentFragment();
282            // some of the older versions of rice do not have content fragments
283            if(contentFragment == null || "".equals(contentFragment)){
284                nodeType = getNodeTypeForNode(node);
285            }else{
286                // I'm not sure if this should be the default implementation because
287                // it uses a string comparison instead of a classpath check.
288                nodeType = this.getNodeTypeForNodeFromFragment(node);
289            }
290
291            if (nodeType.isAssignableFrom(NodeType.SPLIT)) {
292                exportSplitNode(parent, node, nodeType, splitJoinContext);
293            } else if (nodeType.isAssignableFrom(NodeType.JOIN)) {
294                exportJoinNode(parent, node, nodeType, splitJoinContext);
295            } else {
296                exportSimpleNode(parent, node, nodeType, splitJoinContext);
297            }
298        }
299    }
300
301    private void exportSimpleNode(Element parent, RouteNode node, NodeType nodeType, SplitJoinContext splitJoinContext) {
302        Element simpleElement = renderNodeElement(parent, node, nodeType);
303        if (node.getNextNodes().size() > 1) {
304            throw new WorkflowRuntimeException("Simple node cannot have more than one next node: " + node.getRouteNodeName());
305        }
306        if (node.getNextNodes().size() == 1) {
307            RouteNode nextNode = (RouteNode)node.getNextNodes().get(0);
308            renderer.renderAttribute(simpleElement, NEXT_NODE, nextNode.getRouteNodeName());
309            exportNodeGraph(parent, nextNode, splitJoinContext);
310        }
311    }
312
313    private void exportSplitNode(Element parent, RouteNode node, NodeType nodeType, SplitJoinContext splitJoinContext) {
314        Element splitElement = renderNodeElement(parent, node, nodeType);
315        SplitJoinContext newSplitJoinContext = new SplitJoinContext(node);
316        for (Iterator iterator = node.getNextNodes().iterator(); iterator.hasNext();) {
317            RouteNode nextNode = (RouteNode) iterator.next();
318            BranchPrototype branch = nextNode.getBranch();
319            if (branch == null) {
320                throw new WorkflowRuntimeException("Found a split next node with no associated branch prototype: " + nextNode.getRouteNodeName());
321            }
322            exportBranch(splitElement, nextNode, branch, newSplitJoinContext);
323        }
324        RouteNode joinNode = newSplitJoinContext.joinNode;
325        if (joinNode == null) {
326            if (node.getNextNodes().size() > 0) {
327                throw new WorkflowRuntimeException("Could not locate the join node for the given split node " + node.getRouteNodeName());
328            }
329        } else {
330            renderNodeElement(splitElement, joinNode, newSplitJoinContext.joinNodeType);
331            if (joinNode.getNextNodes().size() > 1) {
332                throw new WorkflowRuntimeException("Join node cannot have more than one next node: " + joinNode.getRouteNodeName());
333            }
334            if (joinNode.getNextNodes().size() == 1) {
335                RouteNode nextNode = (RouteNode)joinNode.getNextNodes().get(0);
336                renderer.renderAttribute(splitElement, NEXT_NODE, nextNode.getRouteNodeName());
337                exportNodeGraph(parent, nextNode, splitJoinContext);
338            }
339        }
340    }
341
342    private void exportBranch(Element parent, RouteNode node, BranchPrototype branch, SplitJoinContext splitJoinContext) {
343        Element branchElement = renderer.renderElement(parent, BRANCH);
344        renderer.renderAttribute(branchElement, NAME, branch.getName());
345        exportNodeGraph(branchElement, node, splitJoinContext);
346    }
347
348    private void exportJoinNode(Element parent, RouteNode node, NodeType nodeType, SplitJoinContext splitJoinContext) {
349        if (splitJoinContext == null) {
350            // this is the case where a join node is defined as part of a sub process to be used by a dynamic node, in this case it is
351            // not associated with a proper split node.
352            if (!node.getNextNodes().isEmpty()) {
353                throw new WorkflowRuntimeException("Could not export join node with next nodes that is not contained within a split.");
354            }
355            renderNodeElement(parent, node, nodeType);
356        } else if (splitJoinContext.joinNode == null) {
357            // this is the case where we are "inside" the split node in the XML, by setting up this context, the calling code knows that
358            // when it renders all of the branches of the split node, it can then use this context info to render the join node before
359            // closing the split
360            splitJoinContext.joinNode = node;
361            splitJoinContext.joinNodeType = nodeType;
362        }
363    }
364
365    private Element renderNodeElement(Element parent, RouteNode node, NodeType nodeType) {
366        String nodeName = nodeType.getName();
367        // if it's a request activation node, be sure to export it as a simple node
368        if (nodeType.equals(NodeType.REQUEST_ACTIVATION)) {
369            nodeName = NodeType.SIMPLE.getName();
370        }
371        Element nodeElement = renderer.renderElement(parent, nodeName);
372        renderer.renderAttribute(nodeElement, NAME, node.getRouteNodeName());
373        return nodeElement;
374    }
375
376    /**
377     * Exists for backward compatibility for nodes which don't have a content fragment.
378     */
379    private void exportRouteNodeOld(Element parent, RouteNode node, boolean hasDefaultExceptionWorkgroup) {
380        NodeType nodeType = getNodeTypeForNode(node);
381        Element nodeElement = renderer.renderElement(parent, nodeType.getName());
382        renderer.renderAttribute(nodeElement, NAME, node.getRouteNodeName());
383        if (!hasDefaultExceptionWorkgroup) {
384                if (!StringUtils.isBlank(node.getExceptionWorkgroupName())) {
385                        Element exceptionGroupElement = renderer.renderTextElement(nodeElement, EXCEPTION_GROUP_NAME, node.getExceptionWorkgroupName());
386                        exceptionGroupElement.setAttribute(NAMESPACE, node.getExceptionWorkgroup().getNamespaceCode());
387                }
388        }
389        if (supportsActivationType(nodeType) && !StringUtils.isBlank(node.getActivationType())) {
390            renderer.renderTextElement(nodeElement, ACTIVATION_TYPE, node.getActivationType());
391        }
392        if (supportsRouteMethod(nodeType)) {
393            exportRouteMethod(nodeElement, node);
394            renderer.renderBooleanElement(nodeElement, MANDATORY_ROUTE, node.getMandatoryRouteInd(), false);
395            renderer.renderBooleanElement(nodeElement, FINAL_APPROVAL, node.getFinalApprovalInd(), false);
396        }
397        if (nodeType.isCustomNode(node.getNodeType())) {
398            renderer.renderTextElement(nodeElement, TYPE, node.getNodeType());
399        }
400    }
401
402    private void exportRouteNode(Element parent, RouteNode node, boolean hasDefaultExceptionWorkgroup) {
403        String contentFragment = node.getContentFragment();
404        if (StringUtils.isBlank(contentFragment)) {
405            exportRouteNodeOld(parent, node, hasDefaultExceptionWorkgroup);
406        } else {
407            try {
408                Document document = XmlHelper.buildJDocument(new StringReader(contentFragment));
409                Element rootElement = document.detachRootElement();
410                XmlHelper.propagateNamespace(rootElement, parent.getNamespace());
411                parent.addContent(rootElement);
412            } catch (XmlException e) {
413                throw new WorkflowRuntimeException("Failed to load the content fragment.", e);
414            }
415        }
416    }
417
418
419    private NodeType getNodeTypeForNode(RouteNode node) {
420        NodeType nodeType = null;
421        String errorMessage = "Could not determine proper XML element for the given node type: " + node.getNodeType();
422
423        try {
424            nodeType = NodeType.fromClassName(node.getNodeType());
425        } catch (ResourceUnavailableException e) {
426            throw new WorkflowRuntimeException(errorMessage, e);
427        }
428        if (nodeType == null) {
429            throw new WorkflowRuntimeException(errorMessage);
430        }
431        return nodeType;
432    }
433
434    /**
435     *
436     * This method will find the base node type via the content fragment of the node.
437     * Basically, it reads the node type, start, split, join, etc and then assigns
438     * the base type to it.  This is necessary because there are cases where the
439     * passed in nodeType will no be in the classpath. It should, in theory do
440     * the same thing as getNodeTypeForNode.
441     *
442     * @param node
443     * @return
444     */
445    private NodeType getNodeTypeForNodeFromFragment(RouteNode node) {
446        NodeType nodeType = null;
447        String contentFragment = node.getContentFragment();
448        String errorMessage = "Could not determine proper XML element for the given node type: " + node.getNodeType();
449
450        for (Iterator<NodeType> iterator = NodeType.getTypeList().iterator(); iterator.hasNext();) {
451                NodeType nType = iterator.next();
452                // checks for something like <start
453                // or <split
454                // we may want to switch this out for something a little more robust.
455                if(contentFragment.startsWith("<" + nType.getName())){
456                        nodeType = nType;
457                }
458        }
459
460        if (nodeType == null) {
461            throw new WorkflowRuntimeException(errorMessage);
462        }
463        return nodeType;
464    }
465
466    /**
467     * Any node can support activation type, this use to not be the case but now it is.
468     */
469    private boolean supportsActivationType(NodeType nodeType) {
470        return true;
471    }
472
473    /**
474     * Any node can support route methods, this use to not be the case but now it is.
475     */
476    private boolean supportsRouteMethod(NodeType nodeType) {
477        return true;
478    }
479
480    private void exportRouteMethod(Element parent, RouteNode node) {
481        if (!StringUtils.isBlank(node.getRouteMethodName())) {
482            String routeMethodCode = node.getRouteMethodCode();
483            String elementName = null;
484            if (KewApiConstants.ROUTE_LEVEL_FLEX_RM.equals(routeMethodCode)) {
485                elementName = RULE_TEMPLATE;
486            } else if (KewApiConstants.ROUTE_LEVEL_ROUTE_MODULE.equals(routeMethodCode)) {
487                elementName = ROUTE_MODULE;
488            } else {
489                throw new WorkflowRuntimeException("Invalid route method code '"+routeMethodCode+"' for node " + node.getRouteNodeName());
490            }
491            renderer.renderTextElement(parent, elementName, node.getRouteMethodName());
492        }
493    }
494
495    private class DocumentTypeParentComparator implements Comparator {
496
497        public int compare(Object object1, Object object2) {
498            DocumentType docType1 = (DocumentType)object1;
499            DocumentType docType2 = (DocumentType)object2;
500            Integer depth1 = getDepth(docType1);
501            Integer depth2 = getDepth(docType2);
502            return depth1.compareTo(depth2);
503        }
504
505        private Integer getDepth(DocumentType docType) {
506                int depth = 0;
507                while ((docType = docType.getParentDocType()) != null) {
508                        depth++;
509                }
510                return Integer.valueOf(depth);
511        }
512
513    }
514
515    private class SplitJoinContext {
516        public RouteNode splitNode;
517        public RouteNode joinNode;
518        public NodeType joinNodeType;
519        public SplitJoinContext(RouteNode splitNode) {
520            this.splitNode = splitNode;
521        }
522    }
523
524
525}