View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.messaging.exceptionhandling;
17  
18  import java.lang.reflect.InvocationTargetException;
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.apache.log4j.MDC;
24  import org.kuali.rice.core.api.exception.RiceRuntimeException;
25  import org.kuali.rice.kew.actionitem.ActionItem;
26  import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
27  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
28  import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
29  import org.kuali.rice.kew.api.WorkflowRuntimeException;
30  import org.kuali.rice.kew.api.action.ActionRequestStatus;
31  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
32  import org.kuali.rice.kew.engine.RouteContext;
33  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
34  import org.kuali.rice.kew.exception.RouteManagerException;
35  import org.kuali.rice.kew.exception.WorkflowDocumentExceptionRoutingService;
36  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
37  import org.kuali.rice.kew.framework.postprocessor.PostProcessor;
38  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
39  import org.kuali.rice.kew.role.RoleRouteModule;
40  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
41  import org.kuali.rice.kew.service.KEWServiceLocator;
42  import org.kuali.rice.kew.api.KewApiConstants;
43  import org.kuali.rice.kew.util.PerformanceLogger;
44  import org.kuali.rice.krad.util.KRADConstants;
45  import org.kuali.rice.ksb.messaging.PersistedMessageBO;
46  import org.kuali.rice.ksb.service.KSBServiceLocator;
47  
48  
49  public class ExceptionRoutingServiceImpl implements WorkflowDocumentExceptionRoutingService {
50  
51      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ExceptionRoutingServiceImpl.class);
52  
53      public DocumentRouteHeaderValue placeInExceptionRouting(String errorMessage, PersistedMessageBO persistedMessage, String documentId) throws Exception {
54   	 	RouteNodeInstance nodeInstance = null;
55   	 	KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId);
56   	 	DocumentRouteHeaderValue document = KEWServiceLocator.getRouteHeaderService().getRouteHeader(documentId);
57   	 	RouteContext routeContext = establishRouteContext(document, null);
58   	 	List<RouteNodeInstance> activeNodeInstances = KEWServiceLocator.getRouteNodeService().getActiveNodeInstances(documentId);
59   	 	if (!activeNodeInstances.isEmpty()) {
60   	 		// take the first active nodeInstance found.
61   	 		nodeInstance = activeNodeInstances.get(0);
62   	 	}
63   	 	return placeInExceptionRouting(errorMessage, nodeInstance, persistedMessage, routeContext, document, true);
64   	 }
65      
66      public DocumentRouteHeaderValue placeInExceptionRouting(Throwable throwable, PersistedMessageBO persistedMessage, String documentId) throws Exception {
67      	return placeInExceptionRouting(throwable, persistedMessage, documentId, true);
68      }
69      
70      /**
71       * In our case here, our last ditch effort to put the document into exception routing will try to do so without invoking
72       * the Post Processor for do route status change to "Exception" status.
73       */
74      public void placeInExceptionRoutingLastDitchEffort(Throwable throwable, PersistedMessageBO persistedMessage, String documentId) throws Exception {
75      	placeInExceptionRouting(throwable, persistedMessage, documentId, false);
76      }
77      
78      protected DocumentRouteHeaderValue placeInExceptionRouting(Throwable throwable, PersistedMessageBO persistedMessage, String documentId, boolean invokePostProcessor) throws Exception {
79      	KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId);
80      	DocumentRouteHeaderValue document = KEWServiceLocator.getRouteHeaderService().getRouteHeader(documentId);
81      	throwable = unwrapRouteManagerExceptionIfPossible(throwable);
82          RouteContext routeContext = establishRouteContext(document, throwable);
83          RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
84      	Throwable cause = determineActualCause(throwable, 0);
85          String errorMessage = (cause != null && cause.getMessage() != null) ? cause.getMessage() : "";
86      	return placeInExceptionRouting(errorMessage, nodeInstance, persistedMessage, routeContext, document, invokePostProcessor);
87      }
88      
89      protected DocumentRouteHeaderValue placeInExceptionRouting(String errorMessage, RouteNodeInstance nodeInstance, PersistedMessageBO persistedMessage, RouteContext routeContext, DocumentRouteHeaderValue document, boolean invokePostProcessor) throws Exception {
90      	String documentId = document.getDocumentId();
91          MDC.put("docId", documentId);
92          PerformanceLogger performanceLogger = new PerformanceLogger(documentId);
93          try {
94  
95              // mark all active requests to initialized and delete the action items
96              List<ActionRequestValue> actionRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(documentId);
97              for (ActionRequestValue actionRequest : actionRequests) {
98                  if (actionRequest.isActive()) {
99                      actionRequest.setStatus(ActionRequestStatus.INITIALIZED.getCode());
100                     for (ActionItem actionItem : actionRequest.getActionItems()) {
101                         KEWServiceLocator.getActionListService().deleteActionItem(actionItem);
102                     }
103                     KEWServiceLocator.getActionRequestService().saveActionRequest(actionRequest);
104                 }
105             }
106 
107             LOG.debug("Generating exception request for doc : " + documentId);
108             if (errorMessage == null) {
109             	errorMessage = "";
110             }
111             if (errorMessage.length() > KewApiConstants.MAX_ANNOTATION_LENGTH) {
112                 errorMessage = errorMessage.substring(0, KewApiConstants.MAX_ANNOTATION_LENGTH);
113             }
114             List<ActionRequestValue> exceptionRequests = new ArrayList<ActionRequestValue>();
115             if (nodeInstance.getRouteNode().isExceptionGroupDefined()) {
116             	exceptionRequests = generateExceptionGroupRequests(routeContext);
117             } else {
118             	exceptionRequests = generateKimExceptionRequests(routeContext);
119             }
120             if (exceptionRequests.isEmpty()) {
121                 LOG.warn("Failed to generate exception requests for exception routing!");
122             }
123             document = activateExceptionRequests(routeContext, exceptionRequests, errorMessage, invokePostProcessor);
124 
125             if (persistedMessage == null) {
126                 LOG.warn("Attempting to delete null persisted message.");
127             } else {
128                 KSBServiceLocator.getMessageQueueService().delete(persistedMessage);
129             }
130         } finally {
131             performanceLogger.log("Time to generate exception request.");
132             MDC.remove("docId");
133         }
134 
135         return document;
136     }
137 
138     protected void notifyStatusChange(DocumentRouteHeaderValue routeHeader, String newStatusCode, String oldStatusCode) throws InvalidActionTakenException {
139         DocumentRouteStatusChange statusChangeEvent = new DocumentRouteStatusChange(routeHeader.getDocumentId(), routeHeader.getAppDocId(), oldStatusCode, newStatusCode);
140         try {
141             LOG.debug("Notifying post processor of status change "+oldStatusCode+"->"+newStatusCode);
142             PostProcessor postProcessor = routeHeader.getDocumentType().getPostProcessor();
143             ProcessDocReport report = postProcessor.doRouteStatusChange(statusChangeEvent);
144             if (!report.isSuccess()) {
145                 LOG.warn(report.getMessage(), report.getProcessException());
146                 throw new InvalidActionTakenException(report.getMessage());
147             }
148         } catch (Exception ex) {
149             LOG.warn(ex, ex);
150             throw new WorkflowRuntimeException(ex);
151         }
152     }
153     
154     protected List<ActionRequestValue> generateExceptionGroupRequests(RouteContext routeContext) {
155     	RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
156     	ActionRequestFactory arFactory = new ActionRequestFactory(routeContext.getDocument(), null);
157     	ActionRequestValue exceptionRequest = arFactory.createActionRequest(KewApiConstants.ACTION_REQUEST_COMPLETE_REQ, new Integer(0), new KimGroupRecipient(nodeInstance.getRouteNode().getExceptionWorkgroup()), "Exception Workgroup for route node " + nodeInstance.getName(), KewApiConstants.EXCEPTION_REQUEST_RESPONSIBILITY_ID, Boolean.TRUE, "");
158     	return Collections.singletonList(exceptionRequest);
159     }
160     
161     protected List<ActionRequestValue> generateKimExceptionRequests(RouteContext routeContext) throws Exception {
162     	RoleRouteModule roleRouteModule = new RoleRouteModule();
163     	roleRouteModule.setNamespace(KRADConstants.KUALI_RICE_WORKFLOW_NAMESPACE);
164     	roleRouteModule.setResponsibilityTemplateName(KewApiConstants.EXCEPTION_ROUTING_RESPONSIBILITY_TEMPLATE_NAME);
165     	List<ActionRequestValue> requests = roleRouteModule.findActionRequests(routeContext);
166         // let's ensure we are only dealing with root requests
167         requests = KEWServiceLocator.getActionRequestService().getRootRequests(requests);
168     	processExceptionRequests(requests);
169     	return requests;
170     }
171     
172     
173     
174     /**
175      * Takes the given list of Action Requests and ensures their attributes are set properly for exception
176      * routing requests.  Namely, this ensures that all "force action" values are set to "true".
177      */
178     protected void processExceptionRequests(List<ActionRequestValue> exceptionRequests) {
179     	if (exceptionRequests != null) {
180     		for (ActionRequestValue actionRequest : exceptionRequests) {
181     			processExceptionRequest(actionRequest);
182     		}
183     	}
184     }
185     
186     /**
187      * Processes a single exception request, ensuring that it's force action flag is set to true and it's node instance is set to null.
188      * It then recurses through any children requests.
189      */
190     protected void processExceptionRequest(ActionRequestValue actionRequest) {
191     	actionRequest.setForceAction(true);
192     	actionRequest.setNodeInstance(null);
193     	processExceptionRequests(actionRequest.getChildrenRequests());
194     }
195     
196     /**
197      * End IU Customization
198      * @param routeContext
199      * @param exceptionRequests
200      * @param exceptionMessage
201      * @throws Exception
202      */
203     
204     protected DocumentRouteHeaderValue activateExceptionRequests(RouteContext routeContext, List<ActionRequestValue> exceptionRequests, String exceptionMessage, boolean invokePostProcessor) throws Exception {
205     	setExceptionAnnotations(exceptionRequests, exceptionMessage);
206     	// TODO is there a reason we reload the document here?
207     	DocumentRouteHeaderValue rh = KEWServiceLocator.getRouteHeaderService().getRouteHeader(routeContext.getDocument().getDocumentId());
208     	String oldStatus = rh.getDocRouteStatus();
209     	rh.setDocRouteStatus(KewApiConstants.ROUTE_HEADER_EXCEPTION_CD);
210     	if (invokePostProcessor) {
211     		notifyStatusChange(rh, KewApiConstants.ROUTE_HEADER_EXCEPTION_CD, oldStatus);
212     	}
213     	DocumentRouteHeaderValue documentRouteHeaderValue = KEWServiceLocator.getRouteHeaderService().
214                                                         saveRouteHeader(rh);
215     	KEWServiceLocator.getActionRequestService().activateRequests(exceptionRequests);
216         return documentRouteHeaderValue;
217     }
218     
219     /**
220      * Sets the exception message as the annotation on the top-level Action Requests
221      */
222     protected void setExceptionAnnotations(List<ActionRequestValue> actionRequests, String exceptionMessage) {
223     	for (ActionRequestValue actionRequest : actionRequests) {
224     		actionRequest.setAnnotation(exceptionMessage);
225     	}
226     }
227 
228     private Throwable unwrapRouteManagerExceptionIfPossible(Throwable throwable) {
229     	if (throwable instanceof InvocationTargetException) {
230     		throwable = throwable.getCause();
231     	}
232     	if (throwable != null && (! (throwable instanceof RouteManagerException)) && throwable.getCause() instanceof RouteManagerException) {
233     		throwable = throwable.getCause();
234     	}
235     	return throwable;
236     }
237 
238     protected Throwable determineActualCause(Throwable throwable, int depth) {
239     	if (depth >= 10) {
240     		return throwable;
241     	}
242     	if ((throwable instanceof InvocationTargetException) || (throwable instanceof RouteManagerException)) {
243     		if (throwable.getCause() != null) {
244     			return determineActualCause(throwable.getCause(), ++depth);
245     		}
246     	}
247     	return throwable;
248     }
249     
250     protected RouteContext establishRouteContext(DocumentRouteHeaderValue document, Throwable throwable) {
251     	RouteContext routeContext = new RouteContext();
252         if (throwable instanceof RouteManagerException) {
253             RouteManagerException rmException = (RouteManagerException) throwable;
254             routeContext = rmException.getRouteContext();
255         } else {
256         	routeContext.setDocument(document);
257             List<RouteNodeInstance> activeNodeInstances = KEWServiceLocator.getRouteNodeService().getActiveNodeInstances(document.getDocumentId());
258             if (!activeNodeInstances.isEmpty()) {
259                 // take the first active nodeInstance found.
260                 RouteNodeInstance nodeInstance = (RouteNodeInstance) activeNodeInstances.get(0);
261                 routeContext.setNodeInstance(nodeInstance);
262             }
263         }
264         if (routeContext.getNodeInstance() == null) {
265             // get the initial node instance
266             routeContext.setNodeInstance((RouteNodeInstance) document.getInitialRouteNodeInstances().get(0));
267         }
268         return routeContext;
269     }
270 }