View Javadoc

1   /**
2    * 
3    */
4   package org.kuali.student.lum.workflow.node;
5   
6   import java.util.ArrayList;
7   import java.util.HashSet;
8   import java.util.List;
9   import java.util.Set;
10  
11  import javax.xml.namespace.QName;
12  import javax.xml.xpath.XPath;
13  import javax.xml.xpath.XPathConstants;
14  import javax.xml.xpath.XPathExpressionException;
15  
16  import org.apache.commons.lang.StringUtils;
17  import org.kuali.rice.core.exception.RiceRuntimeException;
18  import org.kuali.rice.core.resourceloader.GlobalResourceLoader;
19  import org.kuali.rice.kew.doctype.bo.DocumentType;
20  import org.kuali.rice.kew.engine.RouteContext;
21  import org.kuali.rice.kew.engine.RouteHelper;
22  import org.kuali.rice.kew.engine.node.Branch;
23  import org.kuali.rice.kew.engine.node.DynamicNode;
24  import org.kuali.rice.kew.engine.node.DynamicResult;
25  import org.kuali.rice.kew.engine.node.NodeState;
26  import org.kuali.rice.kew.engine.node.Process;
27  import org.kuali.rice.kew.engine.node.RoleNode;
28  import org.kuali.rice.kew.engine.node.RouteNode;
29  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
30  import org.kuali.rice.kew.exception.WorkflowException;
31  import org.kuali.rice.kew.role.RoleRouteModule;
32  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
33  import org.kuali.rice.kew.service.KEWServiceLocator;
34  import org.kuali.rice.kew.util.KEWConstants;
35  import org.kuali.rice.kew.util.XmlHelper;
36  import org.kuali.student.core.organization.dto.OrgInfo;
37  import org.kuali.student.core.organization.dto.OrgOrgRelationInfo;
38  import org.kuali.student.core.organization.service.OrganizationService;
39  import org.kuali.student.lum.workflow.qualifierresolver.AbstractOrganizationServiceQualifierResolver;
40  import org.kuali.student.lum.workflow.qualifierresolver.OrganizationCurriculumCommitteeQualifierResolver;
41  import org.w3c.dom.Document;
42  import org.w3c.dom.Element;
43  import org.w3c.dom.NodeList;
44  
45  /**
46   * A Dynamic Node implementation that will use the KS Organization Hierarchy to
47   * dynamically generate route paths based on the organizations sent to Workflow
48   * for each document
49   * 
50   */
51  public class OrganizationDynamicNode implements DynamicNode {
52      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(OrganizationDynamicNode.class);
53  
54      // name of the prototype node used by the dynamically created node instances
55      protected static final String ORG_HIERARCHY_NODE = "Org Hierarchy Review";
56      // node state key that will be used to store the organization ids in each node instance
57      public static final String NODE_STATE_ORG_ID_KEY = "org.id.key";
58  
59      /**
60       * The following 4 properties should match the RoleRouteModule constants
61       * which are currently set to 'protected'
62       * 
63       * Created https://jira.kuali.org/browse/KULRICE-4448 to track change to
64       * Rice
65       */
66      public static final String QUALIFIER_RESOLVER_ELEMENT = "qualifierResolver";
67      public static final String QUALIFIER_RESOLVER_CLASS_ELEMENT = "qualifierResolverClass";
68      public static final String RESPONSIBILITY_TEMPLATE_NAME_ELEMENT = "responsibilityTemplateName";
69      public static final String NAMESPACE_ELEMENT = "namespace";
70  
71      private OrganizationService organizationService;
72  
73      public OrganizationService getOrganizationService() {
74          if (null == organizationService) {
75              organizationService = (OrganizationService) GlobalResourceLoader.getService(new QName("http://student.kuali.org/wsdl/organization", "OrganizationService"));
76          }
77          return organizationService;
78      }
79  
80      public void setOrganizationService(OrganizationService organizationService) {
81          this.organizationService = organizationService;
82      }
83  
84      @Override
85      public DynamicResult transitioningInto(RouteContext context, RouteNodeInstance dynamicNodeInstance, RouteHelper helper) throws Exception {
86          LOG.debug("Entering transitioningInto");
87          DocumentType docType = setUpDocumentType(context.getDocument().getDocumentType(), dynamicNodeInstance);
88          // String prototypeNodeName = RouteNodes.DEPT.getNodeName();
89          String prototypeNodeName = ORG_HIERARCHY_NODE;
90          RouteNode roleNodePrototype = docType.getNamedProcess(prototypeNodeName).getInitialRouteNode();
91          if (roleNodePrototype == null) {
92              throw new WorkflowException("Couldn't locate node for name: " + prototypeNodeName);
93          }
94  
95          List<String> orgIds = getInitialOrganizationIdsForRouting(context, dynamicNodeInstance, helper);
96          if ((orgIds != null) && (orgIds.size() > 1)) {
97              throw new RuntimeException("Found a total of " + orgIds.size() + " organizations for routing on document when only one is allowed.");
98          }
99          DynamicResult result = new DynamicResult(orgIds == null || orgIds.isEmpty(), null);
100         for (String orgId : orgIds) {
101             RouteNodeInstance nodeInstance = generateNextNodeInstance(orgId, roleNodePrototype, context, dynamicNodeInstance.getBranch(), helper);
102             LOG.debug("Exiting transitioningInto with " + ((nodeInstance == null) ? "no" : "a") + " valid next node instance");
103             if (nodeInstance != null) {
104                 result.setNextNodeInstance(nodeInstance);
105             }
106         }
107         return result;
108     }
109 
110     /**
111      * This method is used by the {@link #transitioningInto(RouteContext, RouteNodeInstance, RouteHelper)} method and
112      * the organization id values returned will be used to generate node instances that will begin the dynamic
113      * organization routing.
114      * 
115      * @param context
116      *            - RouteContext class that holds data about the current document's routing and data
117      * @param dynamicNodeInstance
118      *            - The initial instance of the dynamic node as determined by the route node configuration
119      * @param helper
120      *            - RouteHelper convenience class used to make some routing operations a bit easier
121      * @return A list of organization ids that will be used to create next node instances in the routing of the
122      *         document. By default these are the organizations set in the the current document's Document Content xml
123      *         by Kuali Student at the point of Save and/or Submit
124      */
125     protected List<String> getInitialOrganizationIdsForRouting(RouteContext context, RouteNodeInstance dynamicNodeInstance, RouteHelper helper) {
126         List<String> orgIds = getOrganizationIdsFromDocumentContent(context);
127         if ((orgIds != null) && (orgIds.size() > 1)) {
128             throw new RuntimeException("Found a total of " + orgIds.size() + " organizations for routing on document when only one is allowed.");
129         }
130 
131         try {
132             for (String orgId : orgIds) {
133                 OrgInfo orgInfo = getOrganizationService().getOrganization(orgId);
134                 LOG.debug("Org on Document: " + getOrgInfoForPrint(orgInfo));
135                 List<OrgOrgRelationInfo> orgRelationInfos = getOrganizationService().getOrgOrgRelationsByOrg(orgId);
136                 for (OrgOrgRelationInfo orgOrgRelationInfo : orgRelationInfos) {
137                     LOG.debug("---- Org Relation:");
138                     LOG.debug("------------ Org ID: " + orgOrgRelationInfo.getOrgId());
139                     orgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getRelatedOrgId());
140                     LOG.debug("------------ Related Org on Document: " + getOrgInfoForPrint(orgInfo));
141                     LOG.debug("------------ Relation State: " + orgOrgRelationInfo.getState());
142                     LOG.debug("------------ Relation Type: " + orgOrgRelationInfo.getType());
143                 }
144                 List<OrgOrgRelationInfo> relatedOrgRelationInfos = getOrganizationService().getOrgOrgRelationsByRelatedOrg(orgId);
145                 for (OrgOrgRelationInfo orgOrgRelationInfo : relatedOrgRelationInfos) {
146                     LOG.debug("---- Related Org Relation:");
147                     LOG.debug("------------ Related Org ID: " + orgOrgRelationInfo.getRelatedOrgId());
148                     orgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getOrgId());
149                     LOG.debug("------------ Org of Relation: " + getOrgInfoForPrint(orgInfo));
150                     LOG.debug("------------ Relation State: " + orgOrgRelationInfo.getState());
151                     LOG.debug("------------ Relation Type: " + orgOrgRelationInfo.getType());
152                 }
153             }
154         } catch (Exception e) {
155             LOG.error(e);
156             throw new RuntimeException("Caught Exception using Organization Service", e);
157         }
158         return orgIds;
159     }
160 
161     /**
162      * Method to fetch the organization ids from the KEW document content xml
163      * 
164      * @param context
165      *            - RouteContext class that holds data about the current document's routing and data
166      * @return A list of organization ids that are listed in the xml (may have duplicates if duplicates are allowed by
167      *         KS code)
168      */
169     protected List<String> getOrganizationIdsFromDocumentContent(RouteContext context) {
170         Document xmlContent = context.getDocumentContent().getDocument();
171         XPath xPath = XPathHelper.newXPath();
172         try {
173             List<String> orgIds = new ArrayList<String>();
174             NodeList orgElements = (NodeList) xPath.evaluate("/documentContent/applicationContent/" + AbstractOrganizationServiceQualifierResolver.DOCUMENT_CONTENT_XML_ROOT_ELEMENT_NAME + "/orgId", xmlContent,
175                     XPathConstants.NODESET);
176             for (int index = 0; index < orgElements.getLength(); index++) {
177                 Element attributeElement = (Element) orgElements.item(index);
178                 String attributeValue = attributeElement.getTextContent();
179                 orgIds.add(attributeValue);
180             }
181             if (LOG.isDebugEnabled()) {
182                 LOG.debug("Found " + orgElements.getLength() + " organization ids to parse for routing:");
183                 XmlHelper.printDocumentStructure(xmlContent);
184             }
185             return orgIds;
186         } catch (XPathExpressionException e) {
187             throw new RiceRuntimeException("Encountered an issue executing XPath.", e);
188         }
189     }
190 
191     @Override
192     public DynamicResult transitioningOutOf(RouteContext context, RouteHelper helper) throws Exception {
193         LOG.debug("Variables for transitioningOutOf");
194         RouteNodeInstance processInstance = context.getNodeInstance().getProcess();
195 
196         List<String> relatedOrgIds = getNextOrganizationIdsForRouting(context, helper);
197         // dynamic routing is complete if there are no more related org ids
198         DynamicResult result = new DynamicResult(relatedOrgIds.isEmpty(), null);
199         for (String relatedOrgId : relatedOrgIds) {
200             RouteNodeInstance nodeInstance = generateNextNodeInstance(relatedOrgId, context, processInstance.getBranch(), helper);
201             result.getNextNodeInstances().add(nodeInstance);
202         }
203         return result;
204     }
205 
206     /**
207      * Convenience method to get a consistent organization data in order to print to the log
208      */
209     protected String getOrgInfoForPrint(OrgInfo orgInfo) {
210         return orgInfo.getId() + " - " + orgInfo.getShortName() + " (" + orgInfo.getLongName() + ")";
211     }
212 
213     /**
214      * This method is used by the {@link #transitioningOutOf(RouteContext, RouteHelper)} method and the organization id
215      * values returned will be used to generate node instances that will continue the dynamic organization routing.
216      * 
217      * The default implementation retrieves the organization from the previous route node and uses the
218      * {@link OrganizationService#getOrgOrgRelationsByRelatedOrg(String)} method to find all organization relations for
219      * it. That list is then parsed to find all organization relations that are both active and of the relation type
220      * that matches {@link AbstractOrganizationServiceQualifierResolver#KUALI_ORG_TYPE_CURRICULUM_PARENT}. A unique list of those
221      * organization ids is returned.
222      * 
223      * @param context
224      *            - RouteContext class that holds data about the current document's routing and data
225      * @param helper
226      *            - RouteHelper convenience class used to make some routing operations a bit easier
227      * @return A list of organization ids that will be used to create next node instances in the routing of the
228      *         document.
229      */
230     protected List<String> getNextOrganizationIdsForRouting(RouteContext context, RouteHelper helper) {
231         RouteNodeInstance currentNode = context.getNodeInstance();
232         String currentNodeName = currentNode.getName();
233         LOG.debug("currentNodeName = '" + currentNodeName + "'");
234         NodeState currentNodeOrgIdState = currentNode.getNodeState(NODE_STATE_ORG_ID_KEY);
235         LOG.debug("currentNodeOrgIdState is " + ((currentNodeOrgIdState != null) ? "not " : "") + "null");
236         String currentNodeOrgId = (currentNodeOrgIdState != null) ? currentNodeOrgIdState.getValue() : null;
237         LOG.debug("currentNodeOrgId = '" + currentNodeOrgId + "'");
238         Set<String> relatedOrgIds = new HashSet<String>();
239         try {
240             List<OrgOrgRelationInfo> relatedOrgRelationInfos = getOrganizationService().getOrgOrgRelationsByRelatedOrg(currentNodeOrgId);
241             for (OrgOrgRelationInfo orgOrgRelationInfo : relatedOrgRelationInfos) {
242                 if (StringUtils.equals("Active", orgOrgRelationInfo.getState())) {
243                     if (StringUtils.equals(AbstractOrganizationServiceQualifierResolver.KUALI_ORG_TYPE_CURRICULUM_PARENT, orgOrgRelationInfo.getType())) {
244                         LOG.debug("---- Related Org Relation:");
245                         OrgInfo referenceOrgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getRelatedOrgId());
246                         OrgInfo nextNodeOrgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getOrgId());
247                         LOG.debug("------------ Reference Org: " + getOrgInfoForPrint(referenceOrgInfo));
248                         LOG.debug("------------ Org for Next Node: " + getOrgInfoForPrint(nextNodeOrgInfo));
249                         relatedOrgIds.add(nextNodeOrgInfo.getId());
250                     }
251                 }
252             }
253         } catch (Exception e) {
254             LOG.error("Exception caught attempting to use org hierarchy routing", e);
255             throw new RuntimeException("Exception caught attempting to use org hierarchy routing", e);
256         }
257         return new ArrayList<String>(relatedOrgIds);
258     }
259 
260     /**
261      * Generates a new node instance for the given organization id using the default prototype 'role' route node
262      * definition created by the {@link #setUpDocumentType(DocumentType, RouteNodeInstance)} method.
263      * 
264      */
265     protected RouteNodeInstance generateNextNodeInstance(String orgId, RouteContext context, Branch branch, RouteHelper helper) {
266         return generateNextNodeInstance(orgId, helper.getNodeFactory().getRouteNode(context, ORG_HIERARCHY_NODE), context, branch, helper);
267     }
268 
269     /**
270      * Generates a new node instance for the given organization id using the given route node definition.
271      * 
272      */
273     protected RouteNodeInstance generateNextNodeInstance(String orgId, RouteNode routeNodeDefinition, RouteContext context, Branch branch, RouteHelper helper) {
274         LOG.debug("Adding new node with name '" + routeNodeDefinition.getRouteNodeName() + "'");
275         RouteNodeInstance actualRouteNodeInstance = helper.getNodeFactory().createRouteNodeInstance(context.getDocument().getRouteHeaderId(), routeNodeDefinition);
276         actualRouteNodeInstance.setBranch(branch);
277         actualRouteNodeInstance.addNodeState(new NodeState(NODE_STATE_ORG_ID_KEY, orgId));
278         return actualRouteNodeInstance;
279     }
280 
281     /**
282      * Method verifies that the Organization Hierarchy Review node exists on the document type. If it does not exist it
283      * will add it and save the document type. This node is required because it will be used as a prototype for any
284      * generated 'role' nodes (also known as KIM Responsibility Review Nodes).
285      * 
286      * @param documentType
287      *            - DocumentType object that needs nodes defined but may not have them defined
288      * @param dynamicNodeInstance
289      *            - The node instance that represents the dynamic node as defined in the document type configuration
290      *            (the node that tells KEW to look at this class for the node processing)
291      */
292     protected DocumentType setUpDocumentType(DocumentType documentType, RouteNodeInstance dynamicNodeInstance) {
293         boolean altered = false;
294         // add the org hierarchy review node
295         if (documentType.getNamedProcess(ORG_HIERARCHY_NODE) == null) {
296             RouteNode hierarchyNode = getKimRoleNode(ORG_HIERARCHY_NODE, dynamicNodeInstance);
297             documentType.addProcess(getPrototypeProcess(hierarchyNode, documentType));
298             altered = true;
299         }
300         if (altered) {
301             // side step normal version etc. because it can cause exceptions
302             KEWServiceLocator.getDocumentTypeService().save(documentType);
303         }
304         return KEWServiceLocator.getDocumentTypeService().findByName(documentType.getName());
305     }
306 
307     /**
308      * Method generates the {@link RouteNode} definition that will be used as a prototype for any dynamically created route node instances for this dynamic node class.
309      * 
310      * @param routeNodeName - The name to be used for the new route node definition
311      * @param dynamicNodeInstance - used to set up the {@link DocumentType} on the generated route node definition 
312      */
313     protected RouteNode getKimRoleNode(String routeNodeName, RouteNodeInstance dynamicNodeInstance) {
314         RouteNode roleNode = new RouteNode();
315         roleNode.setFinalApprovalInd(Boolean.FALSE);
316         roleNode.setMandatoryRouteInd(Boolean.FALSE);
317         roleNode.setActivationType(KEWConstants.ROUTE_LEVEL_PARALLEL);
318         roleNode.setDocumentType(dynamicNodeInstance.getRouteNode().getDocumentType());
319         roleNode.setNodeType(RoleNode.class.getName());
320         roleNode.setRouteMethodName(RoleRouteModule.class.getName());
321         roleNode.setRouteMethodCode(KEWConstants.ROUTE_LEVEL_ROUTE_MODULE);
322         roleNode.setRouteNodeName(routeNodeName);
323         roleNode.setContentFragment("<" + QUALIFIER_RESOLVER_CLASS_ELEMENT + ">" + OrganizationCurriculumCommitteeQualifierResolver.class.getName() + "</" + QUALIFIER_RESOLVER_CLASS_ELEMENT + ">");
324         return roleNode;
325     }
326 
327     protected Process getPrototypeProcess(RouteNode node, DocumentType documentType) {
328         Process process = new Process();
329         process.setDocumentType(documentType);
330         process.setInitial(false);
331         process.setInitialRouteNode(node);
332         process.setName(node.getRouteNodeName());
333         return process;
334     }
335 
336 }