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