001/** 002 * Copyright 2005-2015 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.messaging.exceptionhandling; 017 018import java.lang.reflect.InvocationTargetException; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.List; 022 023import org.apache.log4j.MDC; 024import org.kuali.rice.core.api.exception.RiceRuntimeException; 025import org.kuali.rice.kew.actionitem.ActionItem; 026import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 027import org.kuali.rice.kew.actionrequest.ActionRequestValue; 028import org.kuali.rice.kew.actionrequest.KimGroupRecipient; 029import org.kuali.rice.kew.api.WorkflowRuntimeException; 030import org.kuali.rice.kew.api.action.ActionRequestStatus; 031import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 032import org.kuali.rice.kew.engine.RouteContext; 033import org.kuali.rice.kew.engine.node.RouteNodeInstance; 034import org.kuali.rice.kew.exception.RouteManagerException; 035import org.kuali.rice.kew.exception.WorkflowDocumentExceptionRoutingService; 036import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 037import org.kuali.rice.kew.framework.postprocessor.PostProcessor; 038import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 039import org.kuali.rice.kew.role.RoleRouteModule; 040import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 041import org.kuali.rice.kew.service.KEWServiceLocator; 042import org.kuali.rice.kew.api.KewApiConstants; 043import org.kuali.rice.kew.util.PerformanceLogger; 044import org.kuali.rice.krad.util.KRADConstants; 045import org.kuali.rice.ksb.messaging.PersistedMessageBO; 046import org.kuali.rice.ksb.service.KSBServiceLocator; 047 048 049public class ExceptionRoutingServiceImpl implements WorkflowDocumentExceptionRoutingService { 050 051 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ExceptionRoutingServiceImpl.class); 052 053 public DocumentRouteHeaderValue placeInExceptionRouting(String errorMessage, PersistedMessageBO persistedMessage, String documentId) throws Exception { 054 RouteNodeInstance nodeInstance = null; 055 KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId); 056 DocumentRouteHeaderValue document = KEWServiceLocator.getRouteHeaderService().getRouteHeader(documentId); 057 RouteContext routeContext = establishRouteContext(document, null); 058 List<RouteNodeInstance> activeNodeInstances = KEWServiceLocator.getRouteNodeService().getActiveNodeInstances(documentId); 059 if (!activeNodeInstances.isEmpty()) { 060 // take the first active nodeInstance found. 061 nodeInstance = activeNodeInstances.get(0); 062 } 063 return placeInExceptionRouting(errorMessage, nodeInstance, persistedMessage, routeContext, document, true); 064 } 065 066 public DocumentRouteHeaderValue placeInExceptionRouting(Throwable throwable, PersistedMessageBO persistedMessage, String documentId) throws Exception { 067 return placeInExceptionRouting(throwable, persistedMessage, documentId, true); 068 } 069 070 /** 071 * In our case here, our last ditch effort to put the document into exception routing will try to do so without invoking 072 * the Post Processor for do route status change to "Exception" status. 073 */ 074 public void placeInExceptionRoutingLastDitchEffort(Throwable throwable, PersistedMessageBO persistedMessage, String documentId) throws Exception { 075 placeInExceptionRouting(throwable, persistedMessage, documentId, false); 076 } 077 078 protected DocumentRouteHeaderValue placeInExceptionRouting(Throwable throwable, PersistedMessageBO persistedMessage, String documentId, boolean invokePostProcessor) throws Exception { 079 KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId); 080 DocumentRouteHeaderValue document = KEWServiceLocator.getRouteHeaderService().getRouteHeader(documentId); 081 throwable = unwrapRouteManagerExceptionIfPossible(throwable); 082 RouteContext routeContext = establishRouteContext(document, throwable); 083 RouteNodeInstance nodeInstance = routeContext.getNodeInstance(); 084 Throwable cause = determineActualCause(throwable, 0); 085 String errorMessage = (cause != null && cause.getMessage() != null) ? cause.getMessage() : ""; 086 return placeInExceptionRouting(errorMessage, nodeInstance, persistedMessage, routeContext, document, invokePostProcessor); 087 } 088 089 protected DocumentRouteHeaderValue placeInExceptionRouting(String errorMessage, RouteNodeInstance nodeInstance, PersistedMessageBO persistedMessage, RouteContext routeContext, DocumentRouteHeaderValue document, boolean invokePostProcessor) throws Exception { 090 String documentId = document.getDocumentId(); 091 MDC.put("docId", documentId); 092 PerformanceLogger performanceLogger = new PerformanceLogger(documentId); 093 try { 094 095 // mark all active requests to initialized and delete the action items 096 List<ActionRequestValue> actionRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(documentId); 097 for (ActionRequestValue actionRequest : actionRequests) { 098 if (actionRequest.isActive()) { 099 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}