Coverage Report - org.kuali.student.lum.workflow.node.OrganizationDynamicNode
 
Classes in this File Line Coverage Branch Coverage Complexity
OrganizationDynamicNode
0%
0/130
0%
0/48
3.538
 
 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.api.exception.RiceRuntimeException;
 18  
 import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
 19  
 //import org.kuali.rice.core.util.xml.XmlJotter;
 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.core.organization.dto.OrgInfo;
 38  
 import org.kuali.student.core.organization.dto.OrgOrgRelationInfo;
 39  
 import org.kuali.student.core.organization.service.OrganizationService;
 40  
 import org.kuali.student.lum.workflow.qualifierresolver.AbstractOrganizationServiceQualifierResolver;
 41  
 import org.kuali.student.lum.workflow.qualifierresolver.OrganizationCurriculumCommitteeQualifierResolver;
 42  
 import org.w3c.dom.Document;
 43  
 import org.w3c.dom.Element;
 44  
 import org.w3c.dom.NodeList;
 45  
 
 46  
 /**
 47  
  * A Dynamic Node implementation that will use the KS Organization Hierarchy to
 48  
  * dynamically generate route paths based on the organizations sent to Workflow
 49  
  * for each document
 50  
  * 
 51  
  */
 52  0
 public class OrganizationDynamicNode implements DynamicNode {
 53  0
     private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(OrganizationDynamicNode.class);
 54  
 
 55  
     // name of the prototype node used by the dynamically created node instances
 56  
     protected static final String ORG_HIERARCHY_NODE = "Org Hierarchy Review";
 57  
     // node state key that will be used to store the organization ids in each node instance
 58  
     public static final String NODE_STATE_ORG_ID_KEY = "org.id.key";
 59  
 
 60  
     /**
 61  
      * The following 4 properties should match the RoleRouteModule constants
 62  
      * which are currently set to 'protected'
 63  
      * 
 64  
      * Created https://jira.kuali.org/browse/KULRICE-4448 to track change to
 65  
      * Rice
 66  
      */
 67  
     public static final String QUALIFIER_RESOLVER_ELEMENT = "qualifierResolver";
 68  
     public static final String QUALIFIER_RESOLVER_CLASS_ELEMENT = "qualifierResolverClass";
 69  
     public static final String RESPONSIBILITY_TEMPLATE_NAME_ELEMENT = "responsibilityTemplateName";
 70  
     public static final String NAMESPACE_ELEMENT = "namespace";
 71  
 
 72  
     private OrganizationService organizationService;
 73  
 
 74  
     public OrganizationService getOrganizationService() {
 75  0
         if (null == organizationService) {
 76  0
             organizationService = (OrganizationService) GlobalResourceLoader.getService(new QName("http://student.kuali.org/wsdl/organization", "OrganizationService"));
 77  
         }
 78  0
         return organizationService;
 79  
     }
 80  
 
 81  
     public void setOrganizationService(OrganizationService organizationService) {
 82  0
         this.organizationService = organizationService;
 83  0
     }
 84  
 
 85  
     @Override
 86  
     public DynamicResult transitioningInto(RouteContext context, RouteNodeInstance dynamicNodeInstance, RouteHelper helper) throws Exception {
 87  0
         LOG.debug("Entering transitioningInto");
 88  0
         DocumentType docType = setUpDocumentType(context.getDocument().getDocumentType(), dynamicNodeInstance);
 89  
         // String prototypeNodeName = RouteNodes.DEPT.getNodeName();
 90  0
         String prototypeNodeName = ORG_HIERARCHY_NODE;
 91  0
         RouteNode roleNodePrototype = docType.getNamedProcess(prototypeNodeName).getInitialRouteNode();
 92  0
         if (roleNodePrototype == null) {
 93  0
             throw new WorkflowException("Couldn't locate node for name: " + prototypeNodeName);
 94  
         }
 95  
 
 96  0
         List<String> orgIds = getInitialOrganizationIdsForRouting(context, dynamicNodeInstance, helper);
 97  0
         if ((orgIds != null) && (orgIds.size() > 1)) {
 98  0
             throw new RuntimeException("Found a total of " + orgIds.size() + " organizations for routing on document when only one is allowed.");
 99  
         }
 100  0
         DynamicResult result = new DynamicResult(orgIds == null || orgIds.isEmpty(), null);
 101  0
         for (String orgId : orgIds) {
 102  0
             RouteNodeInstance nodeInstance = generateNextNodeInstance(orgId, roleNodePrototype, context, dynamicNodeInstance.getBranch(), helper);
 103  0
             LOG.debug("Exiting transitioningInto with " + ((nodeInstance == null) ? "no" : "a") + " valid next node instance");
 104  0
             if (nodeInstance != null) {
 105  0
                 result.setNextNodeInstance(nodeInstance);
 106  
             }
 107  0
         }
 108  0
         return result;
 109  
     }
 110  
 
 111  
     /**
 112  
      * This method is used by the {@link #transitioningInto(RouteContext, RouteNodeInstance, RouteHelper)} method and
 113  
      * the organization id values returned will be used to generate node instances that will begin the dynamic
 114  
      * organization routing.
 115  
      * 
 116  
      * @param context
 117  
      *            - RouteContext class that holds data about the current document's routing and data
 118  
      * @param dynamicNodeInstance
 119  
      *            - The initial instance of the dynamic node as determined by the route node configuration
 120  
      * @param helper
 121  
      *            - RouteHelper convenience class used to make some routing operations a bit easier
 122  
      * @return A list of organization ids that will be used to create next node instances in the routing of the
 123  
      *         document. By default these are the organizations set in the the current document's Document Content xml
 124  
      *         by Kuali Student at the point of Save and/or Submit
 125  
      */
 126  
     protected List<String> getInitialOrganizationIdsForRouting(RouteContext context, RouteNodeInstance dynamicNodeInstance, RouteHelper helper) {
 127  0
         List<String> orgIds = getOrganizationIdsFromDocumentContent(context);
 128  0
         if ((orgIds != null) && (orgIds.size() > 1)) {
 129  0
             throw new RuntimeException("Found a total of " + orgIds.size() + " organizations for routing on document when only one is allowed.");
 130  
         }
 131  
 
 132  
         try {
 133  0
             for (String orgId : orgIds) {
 134  0
                 OrgInfo orgInfo = getOrganizationService().getOrganization(orgId);
 135  0
                 LOG.debug("Org on Document: " + getOrgInfoForPrint(orgInfo));
 136  0
                 List<OrgOrgRelationInfo> orgRelationInfos = getOrganizationService().getOrgOrgRelationsByOrg(orgId);
 137  0
                 for (OrgOrgRelationInfo orgOrgRelationInfo : orgRelationInfos) {
 138  0
                     LOG.debug("---- Org Relation:");
 139  0
                     LOG.debug("------------ Org ID: " + orgOrgRelationInfo.getOrgId());
 140  0
                     orgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getRelatedOrgId());
 141  0
                     LOG.debug("------------ Related Org on Document: " + getOrgInfoForPrint(orgInfo));
 142  0
                     LOG.debug("------------ Relation State: " + orgOrgRelationInfo.getState());
 143  0
                     LOG.debug("------------ Relation Type: " + orgOrgRelationInfo.getType());
 144  
                 }
 145  0
                 List<OrgOrgRelationInfo> relatedOrgRelationInfos = getOrganizationService().getOrgOrgRelationsByRelatedOrg(orgId);
 146  0
                 for (OrgOrgRelationInfo orgOrgRelationInfo : relatedOrgRelationInfos) {
 147  0
                     LOG.debug("---- Related Org Relation:");
 148  0
                     LOG.debug("------------ Related Org ID: " + orgOrgRelationInfo.getRelatedOrgId());
 149  0
                     orgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getOrgId());
 150  0
                     LOG.debug("------------ Org of Relation: " + getOrgInfoForPrint(orgInfo));
 151  0
                     LOG.debug("------------ Relation State: " + orgOrgRelationInfo.getState());
 152  0
                     LOG.debug("------------ Relation Type: " + orgOrgRelationInfo.getType());
 153  
                 }
 154  0
             }
 155  0
         } catch (Exception e) {
 156  0
             LOG.error(e);
 157  0
             throw new RuntimeException("Caught Exception using Organization Service", e);
 158  0
         }
 159  0
         return orgIds;
 160  
     }
 161  
 
 162  
     /**
 163  
      * Method to fetch the organization ids from the KEW document content xml
 164  
      * 
 165  
      * @param context
 166  
      *            - RouteContext class that holds data about the current document's routing and data
 167  
      * @return A list of organization ids that are listed in the xml (may have duplicates if duplicates are allowed by
 168  
      *         KS code)
 169  
      */
 170  
     protected List<String> getOrganizationIdsFromDocumentContent(RouteContext context) {
 171  0
         Document xmlContent = context.getDocumentContent().getDocument();
 172  0
         XPath xPath = XPathHelper.newXPath();
 173  
         try {
 174  0
             List<String> orgIds = new ArrayList<String>();
 175  0
             NodeList orgElements = (NodeList) xPath.evaluate("/documentContent/applicationContent/" + AbstractOrganizationServiceQualifierResolver.DOCUMENT_CONTENT_XML_ROOT_ELEMENT_NAME + "/orgId", xmlContent,
 176  
                     XPathConstants.NODESET);
 177  0
             for (int index = 0; index < orgElements.getLength(); index++) {
 178  0
                 Element attributeElement = (Element) orgElements.item(index);
 179  0
                 String attributeValue = attributeElement.getTextContent();
 180  0
                 orgIds.add(attributeValue);
 181  
             }
 182  0
             if (LOG.isDebugEnabled()) {
 183  0
                 LOG.debug("Found " + orgElements.getLength() + " organization ids to parse for routing:" + XmlJotter.jotDocument(xmlContent));
 184  
             }
 185  0
             return orgIds;
 186  0
         } catch (XPathExpressionException e) {
 187  0
             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  0
         LOG.debug("Variables for transitioningOutOf");
 194  0
         RouteNodeInstance processInstance = context.getNodeInstance().getProcess();
 195  
 
 196  0
         List<String> relatedOrgIds = getNextOrganizationIdsForRouting(context, helper);
 197  
         // dynamic routing is complete if there are no more related org ids
 198  0
         DynamicResult result = new DynamicResult(relatedOrgIds.isEmpty(), null);
 199  0
         for (String relatedOrgId : relatedOrgIds) {
 200  0
             RouteNodeInstance nodeInstance = generateNextNodeInstance(relatedOrgId, context, processInstance.getBranch(), helper);
 201  0
             result.getNextNodeInstances().add(nodeInstance);
 202  0
         }
 203  0
         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  0
         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  0
         RouteNodeInstance currentNode = context.getNodeInstance();
 232  0
         String currentNodeName = currentNode.getName();
 233  0
         LOG.debug("currentNodeName = '" + currentNodeName + "'");
 234  0
         NodeState currentNodeOrgIdState = currentNode.getNodeState(NODE_STATE_ORG_ID_KEY);
 235  0
         LOG.debug("currentNodeOrgIdState is " + ((currentNodeOrgIdState != null) ? "not " : "") + "null");
 236  0
         String currentNodeOrgId = (currentNodeOrgIdState != null) ? currentNodeOrgIdState.getValue() : null;
 237  0
         LOG.debug("currentNodeOrgId = '" + currentNodeOrgId + "'");
 238  0
         Set<String> relatedOrgIds = new HashSet<String>();
 239  
         try {
 240  0
             List<OrgOrgRelationInfo> relatedOrgRelationInfos = getOrganizationService().getOrgOrgRelationsByRelatedOrg(currentNodeOrgId);
 241  0
             for (OrgOrgRelationInfo orgOrgRelationInfo : relatedOrgRelationInfos) {
 242  0
                 if (StringUtils.equals("Active", orgOrgRelationInfo.getState())) {
 243  0
                     if (StringUtils.equals(AbstractOrganizationServiceQualifierResolver.KUALI_ORG_TYPE_CURRICULUM_PARENT, orgOrgRelationInfo.getType())) {
 244  0
                         LOG.debug("---- Related Org Relation:");
 245  0
                         OrgInfo referenceOrgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getRelatedOrgId());
 246  0
                         OrgInfo nextNodeOrgInfo = getOrganizationService().getOrganization(orgOrgRelationInfo.getOrgId());
 247  0
                         LOG.debug("------------ Reference Org: " + getOrgInfoForPrint(referenceOrgInfo));
 248  0
                         LOG.debug("------------ Org for Next Node: " + getOrgInfoForPrint(nextNodeOrgInfo));
 249  0
                         relatedOrgIds.add(nextNodeOrgInfo.getId());
 250  0
                     }
 251  
                 }
 252  
             }
 253  0
         } catch (Exception e) {
 254  0
             LOG.error("Exception caught attempting to use org hierarchy routing", e);
 255  0
             throw new RuntimeException("Exception caught attempting to use org hierarchy routing", e);
 256  0
         }
 257  0
         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  0
         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  0
         LOG.debug("Adding new node with name '" + routeNodeDefinition.getRouteNodeName() + "'");
 275  0
         RouteNodeInstance actualRouteNodeInstance = helper.getNodeFactory().createRouteNodeInstance(context.getDocument().getDocumentId(), routeNodeDefinition);
 276  0
         actualRouteNodeInstance.setBranch(branch);
 277  0
         actualRouteNodeInstance.addNodeState(new NodeState(NODE_STATE_ORG_ID_KEY, orgId));
 278  0
         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  0
         boolean altered = false;
 294  
         // add the org hierarchy review node
 295  0
         if (documentType.getNamedProcess(ORG_HIERARCHY_NODE) == null) {
 296  0
             RouteNode hierarchyNode = getKimRoleNode(ORG_HIERARCHY_NODE, dynamicNodeInstance);
 297  0
             documentType.addProcess(getPrototypeProcess(hierarchyNode, documentType));
 298  0
             altered = true;
 299  
         }
 300  0
         if (altered) {
 301  
             // side step normal version etc. because it can cause exceptions
 302  0
             KEWServiceLocator.getDocumentTypeService().save(documentType);
 303  
         }
 304  0
         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  0
         RouteNode roleNode = new RouteNode();
 315  0
         roleNode.setFinalApprovalInd(Boolean.FALSE);
 316  0
         roleNode.setMandatoryRouteInd(Boolean.FALSE);
 317  0
         roleNode.setActivationType(KewApiConstants.ROUTE_LEVEL_PARALLEL);
 318  0
         roleNode.setDocumentType(dynamicNodeInstance.getRouteNode().getDocumentType());
 319  0
         roleNode.setNodeType(RoleNode.class.getName());
 320  0
         roleNode.setRouteMethodName(RoleRouteModule.class.getName());
 321  0
         roleNode.setRouteMethodCode(KewApiConstants.ROUTE_LEVEL_ROUTE_MODULE);
 322  0
         roleNode.setRouteNodeName(routeNodeName);
 323  0
         roleNode.setContentFragment("<" + QUALIFIER_RESOLVER_CLASS_ELEMENT + ">" + OrganizationCurriculumCommitteeQualifierResolver.class.getName() + "</" + QUALIFIER_RESOLVER_CLASS_ELEMENT + ">");
 324  0
         return roleNode;
 325  
     }
 326  
 
 327  
     protected ProcessDefinitionBo getPrototypeProcess(RouteNode node, DocumentType documentType) {
 328  0
         ProcessDefinitionBo process = new ProcessDefinitionBo();
 329  0
         process.setDocumentType(documentType);
 330  0
         process.setInitial(false);
 331  0
         process.setInitialRouteNode(node);
 332  0
         process.setName(node.getRouteNodeName());
 333  0
         return process;
 334  
     }
 335  
 
 336  
 }