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}