001/** 002 * Copyright 2005-2016 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 void placeInExceptionRouting(String errorMessage, PersistedMessageBO persistedMessage, String documentId) throws Exception { 054 RouteNodeInstance nodeInstance = null; 055 KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId, true); 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 placeInExceptionRouting(errorMessage, nodeInstance, persistedMessage, routeContext, document, true); 064 } 065 066 public void placeInExceptionRouting(Throwable throwable, PersistedMessageBO persistedMessage, String documentId) throws Exception { 067 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 void placeInExceptionRouting(Throwable throwable, PersistedMessageBO persistedMessage, String documentId, boolean invokePostProcessor) throws Exception { 079 KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId, true); 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 placeInExceptionRouting(errorMessage, nodeInstance, persistedMessage, routeContext, document, invokePostProcessor); 087 } 088 089 protected void 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 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 136 protected void notifyStatusChange(DocumentRouteHeaderValue routeHeader, String newStatusCode, String oldStatusCode) throws InvalidActionTakenException { 137 DocumentRouteStatusChange statusChangeEvent = new DocumentRouteStatusChange(routeHeader.getDocumentId(), routeHeader.getAppDocId(), oldStatusCode, newStatusCode); 138 try { 139 LOG.debug("Notifying post processor of status change "+oldStatusCode+"->"+newStatusCode); 140 PostProcessor postProcessor = routeHeader.getDocumentType().getPostProcessor(); 141 ProcessDocReport report = postProcessor.doRouteStatusChange(statusChangeEvent); 142 if (!report.isSuccess()) { 143 LOG.warn(report.getMessage(), report.getProcessException()); 144 throw new InvalidActionTakenException(report.getMessage()); 145 } 146 } catch (Exception ex) { 147 LOG.warn(ex, ex); 148 throw new WorkflowRuntimeException(ex); 149 } 150 } 151 152 protected List<ActionRequestValue> generateExceptionGroupRequests(RouteContext routeContext) { 153 RouteNodeInstance nodeInstance = routeContext.getNodeInstance(); 154 ActionRequestFactory arFactory = new ActionRequestFactory(routeContext.getDocument(), null); 155 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, ""); 156 return Collections.singletonList(exceptionRequest); 157 } 158 159 protected List<ActionRequestValue> generateKimExceptionRequests(RouteContext routeContext) throws Exception { 160 RoleRouteModule roleRouteModule = new RoleRouteModule(); 161 roleRouteModule.setNamespace(KRADConstants.KUALI_RICE_WORKFLOW_NAMESPACE); 162 roleRouteModule.setResponsibilityTemplateName(KewApiConstants.EXCEPTION_ROUTING_RESPONSIBILITY_TEMPLATE_NAME); 163 List<ActionRequestValue> requests = roleRouteModule.findActionRequests(routeContext); 164 processExceptionRequests(requests); 165 return requests; 166 } 167 168 169 170 /** 171 * Takes the given list of Action Requests and ensures their attributes are set properly for exception 172 * routing requests. Namely, this ensures that all "force action" values are set to "true". 173 */ 174 protected void processExceptionRequests(List<ActionRequestValue> exceptionRequests) { 175 if (exceptionRequests != null) { 176 for (ActionRequestValue actionRequest : exceptionRequests) { 177 processExceptionRequest(actionRequest); 178 } 179 } 180 } 181 182 /** 183 * 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. 184 * It then recurses through any children requests. 185 */ 186 protected void processExceptionRequest(ActionRequestValue actionRequest) { 187 actionRequest.setForceAction(true); 188 actionRequest.setNodeInstance(null); 189 processExceptionRequests(actionRequest.getChildrenRequests()); 190 } 191 192 /** 193 * End IU Customization 194 * @param routeContext 195 * @param exceptionRequests 196 * @param exceptionMessage 197 * @throws Exception 198 */ 199 200 protected void activateExceptionRequests(RouteContext routeContext, List<ActionRequestValue> exceptionRequests, String exceptionMessage, boolean invokePostProcessor) throws Exception { 201 setExceptionAnnotations(exceptionRequests, exceptionMessage); 202 // TODO is there a reason we reload the document here? 203 DocumentRouteHeaderValue rh = KEWServiceLocator.getRouteHeaderService().getRouteHeader(routeContext.getDocument().getDocumentId()); 204 String oldStatus = rh.getDocRouteStatus(); 205 rh.setDocRouteStatus(KewApiConstants.ROUTE_HEADER_EXCEPTION_CD); 206 if (invokePostProcessor) { 207 notifyStatusChange(rh, KewApiConstants.ROUTE_HEADER_EXCEPTION_CD, oldStatus); 208 } 209 KEWServiceLocator.getRouteHeaderService().saveRouteHeader(rh); 210 KEWServiceLocator.getActionRequestService().activateRequests(exceptionRequests); 211 } 212 213 /** 214 * Sets the exception message as the annotation on the top-level Action Requests 215 */ 216 protected void setExceptionAnnotations(List<ActionRequestValue> actionRequests, String exceptionMessage) { 217 for (ActionRequestValue actionRequest : actionRequests) { 218 actionRequest.setAnnotation(exceptionMessage); 219 } 220 } 221 222 private Throwable unwrapRouteManagerExceptionIfPossible(Throwable throwable) { 223 if (throwable instanceof InvocationTargetException) { 224 throwable = throwable.getCause(); 225 } 226 if (throwable != null && (! (throwable instanceof RouteManagerException)) && throwable.getCause() instanceof RouteManagerException) { 227 throwable = throwable.getCause(); 228 } 229 return throwable; 230 } 231 232 protected Throwable determineActualCause(Throwable throwable, int depth) { 233 if (depth >= 10) { 234 return throwable; 235 } 236 if ((throwable instanceof InvocationTargetException) || (throwable instanceof RouteManagerException)) { 237 if (throwable.getCause() != null) { 238 return determineActualCause(throwable.getCause(), ++depth); 239 } 240 } 241 return throwable; 242 } 243 244 protected RouteContext establishRouteContext(DocumentRouteHeaderValue document, Throwable throwable) { 245 RouteContext routeContext = new RouteContext(); 246 if (throwable instanceof RouteManagerException) { 247 RouteManagerException rmException = (RouteManagerException) throwable; 248 routeContext = rmException.getRouteContext(); 249 } else { 250 routeContext.setDocument(document); 251 List<RouteNodeInstance> activeNodeInstances = KEWServiceLocator.getRouteNodeService().getActiveNodeInstances(document.getDocumentId()); 252 if (!activeNodeInstances.isEmpty()) { 253 // take the first active nodeInstance found. 254 RouteNodeInstance nodeInstance = (RouteNodeInstance) activeNodeInstances.get(0); 255 routeContext.setNodeInstance(nodeInstance); 256 } 257 } 258 if (routeContext.getNodeInstance() == null) { 259 // get the initial node instance 260 routeContext.setNodeInstance((RouteNodeInstance) document.getInitialRouteNodeInstances().get(0)); 261 } 262 return routeContext; 263 } 264}