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.krad.service.impl;
017
018import org.apache.log4j.Logger;
019import org.apache.ojb.broker.OptimisticLockException;
020import org.kuali.rice.kew.api.KewApiConstants;
021import org.kuali.rice.kew.api.action.ActionType;
022import org.kuali.rice.kew.api.exception.WorkflowException;
023import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
024import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
025import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
026import org.kuali.rice.kew.framework.postprocessor.DeleteEvent;
027import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
028import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
029import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
030import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
031import org.kuali.rice.krad.UserSession;
032import org.kuali.rice.krad.document.Document;
033import org.kuali.rice.krad.service.DocumentService;
034import org.kuali.rice.krad.service.PostProcessorService;
035import org.kuali.rice.krad.util.GlobalVariables;
036import org.kuali.rice.krad.util.KRADConstants;
037import org.kuali.rice.krad.util.ObjectUtils;
038import org.springframework.transaction.annotation.Transactional;
039
040import java.util.List;
041import java.util.concurrent.Callable;
042
043/**
044 * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents using the
045 * built into the document methods for handling route status and other routing changes that take place asyncronously and potentially
046 * on a different server.
047 */
048@Transactional
049public class PostProcessorServiceImpl implements PostProcessorService {
050
051    private static Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class);
052
053    private DocumentService documentService;
054
055    /**
056     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteStatusChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange)
057     */
058    @Override
059    public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception {
060        return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
061                new Callable<ProcessDocReport>() {
062                    public ProcessDocReport call() throws Exception {
063
064                        try {
065                            if (LOG.isInfoEnabled()) {
066                                LOG.info(new StringBuffer("started handling route status change from ").append(
067                                        statusChangeEvent.getOldRouteStatus()).append(" to ").append(
068                                        statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
069                                        statusChangeEvent.getDocumentId()));
070                            }
071
072                            Document document = documentService.getByDocumentHeaderId(
073                                    statusChangeEvent.getDocumentId());
074                            if (document == null) {
075                                if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(
076                                        statusChangeEvent.getNewRouteStatus())) {
077                                    throw new RuntimeException(
078                                            "unable to load document " + statusChangeEvent.getDocumentId());
079                                }
080                            } else {
081                                document.doRouteStatusChange(statusChangeEvent);
082                                // PLEASE READ BEFORE YOU MODIFY:
083                                // we dont want to update the document on a Save, as this will cause an
084                                // OptimisticLockException in many cases, because the DB versionNumber will be
085                                // incremented one higher than the document in the browser, so when the user then
086                                // hits Submit or Save again, the versionNumbers are out of synch, and the
087                                // OptimisticLockException is thrown. This is not the optimal solution, and will
088                                // be a problem anytime where the user can continue to edit the document after a
089                                // workflow state change, without reloading the form.
090                                if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) {
091                                    documentService.updateDocument(document);
092                                }
093                            }
094                            if (LOG.isInfoEnabled()) {
095                                LOG.info(new StringBuffer("finished handling route status change from ").append(
096                                        statusChangeEvent.getOldRouteStatus()).append(" to ").append(
097                                        statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
098                                        statusChangeEvent.getDocumentId()));
099                            }
100                        } catch (Exception e) {
101                            logAndRethrow("route status", e);
102                        }
103                        return new ProcessDocReport(true, "");
104                    }
105                });
106    }
107
108    /**
109     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteLevelChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange)
110     */
111    public ProcessDocReport doRouteLevelChange(final DocumentRouteLevelChange levelChangeEvent) throws Exception {
112        return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
113                new Callable<ProcessDocReport>() {
114                    public ProcessDocReport call() throws Exception {
115
116                        // on route level change we'll serialize the XML for the document. we
117                        // are doing this here cause it's a heavy hitter, and we
118                        // want to avoid the user waiting for this during sync processing
119                        try {
120                            if (LOG.isDebugEnabled()) {
121                                LOG.debug(new StringBuffer("started handling route level change from ").append(
122                                        levelChangeEvent.getOldNodeName()).append(" to ").append(
123                                        levelChangeEvent.getNewNodeName()).append(" for document ").append(
124                                        levelChangeEvent.getDocumentId()));
125                            }
126
127                            Document document = documentService.getByDocumentHeaderId(levelChangeEvent.getDocumentId());
128                            if (document == null) {
129                                throw new RuntimeException(
130                                        "unable to load document " + levelChangeEvent.getDocumentId());
131                            }
132                            document.populateDocumentForRouting();
133                            document.doRouteLevelChange(levelChangeEvent);
134                            document.getDocumentHeader().getWorkflowDocument().saveDocumentData();
135                            if (LOG.isDebugEnabled()) {
136                                LOG.debug(new StringBuffer("finished handling route level change from ").append(
137                                        levelChangeEvent.getOldNodeName()).append(" to ").append(
138                                        levelChangeEvent.getNewNodeName()).append(" for document ").append(
139                                        levelChangeEvent.getDocumentId()));
140                            }
141                        } catch (Exception e) {
142                            logAndRethrow("route level", e);
143                        }
144                        return new ProcessDocReport(true, "");
145                    }
146                });
147    }
148
149    /**
150     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doDeleteRouteHeader(org.kuali.rice.kew.framework.postprocessor.DeleteEvent)
151     */
152    @Override
153    public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws Exception {
154        return new ProcessDocReport(true, "");
155    }
156
157    /**
158     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doActionTaken(org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent)
159     */
160    @Override
161    public ProcessDocReport doActionTaken(final ActionTakenEvent event) throws Exception {
162         return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
163            public ProcessDocReport call() throws Exception {
164                try {
165                    if ( LOG.isDebugEnabled() ) {
166                        LOG.debug(new StringBuffer("started doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
167                    }
168                    Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
169                    if (ObjectUtils.isNull(document)) {
170                        // only throw an exception if we are not cancelling
171                        if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) {
172                            LOG.warn("doActionTaken() Unable to load document with id " + event.getDocumentId() +
173                                    " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(event.getActionTaken().getActionTaken()));
174        //                    throw new RuntimeException("unable to load document " + event.getDocumentId());
175                        }
176                    } else {
177                        document.doActionTaken(event);
178                        if ( LOG.isDebugEnabled() ) {
179                            LOG.debug(new StringBuffer("finished doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
180                        }
181                    }
182                }
183                catch (Exception e) {
184                    logAndRethrow("do action taken", e);
185                }
186                return new ProcessDocReport(true, "");
187
188            }
189        });
190    }
191
192    /**
193     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#afterActionTaken(org.kuali.rice.kew.api.action.ActionType, org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent)
194     */
195    @Override
196    public ProcessDocReport afterActionTaken(final ActionType performed, final ActionTakenEvent event) throws Exception {
197        return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
198            public ProcessDocReport call() throws Exception {
199                try {
200                    if ( LOG.isDebugEnabled() ) {
201                        LOG.debug(new StringBuffer("started doing after action taken for action performed code " + performed.getCode() + " and action taken code ").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
202                    }
203                    Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
204                    if (ObjectUtils.isNull(document)) {
205                        // only throw an exception if we are not cancelling
206                        if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) {
207                            LOG.warn("afterActionTaken() Unable to load document with id " + event.getDocumentId() +
208                                    " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(event.getActionTaken().getActionTaken()));
209                            //                    throw new RuntimeException("unable to load document " + event.getDocumentId());
210                        }
211                    } else {
212                        document.afterActionTaken(performed, event);
213                        if ( LOG.isDebugEnabled() ) {
214                            LOG.debug(new StringBuffer("finished doing after action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
215                        }
216                    }
217                }
218                catch (Exception e) {
219                    logAndRethrow("do action taken", e);
220                }
221                return new ProcessDocReport(true, "");
222
223            }
224        });
225    }
226
227    /**
228     * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
229     * found the {@link Document#afterWorkflowEngineProcess(boolean)} method will be invoked on it
230     *
231     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#afterProcess(org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent)
232     */
233    @Override
234    public ProcessDocReport afterProcess(final AfterProcessEvent event) throws Exception {
235        return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
236                new Callable<ProcessDocReport>() {
237                    public ProcessDocReport call() throws Exception {
238
239                        try {
240                            if (LOG.isDebugEnabled()) {
241                                LOG.debug(new StringBuffer("started after process method for document ").append(
242                                        event.getDocumentId()));
243                            }
244
245                            Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
246                            if (ObjectUtils.isNull(document)) {
247                                // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
248                                LOG.warn("afterProcess() Unable to load document with id "
249                                        + event.getDocumentId()
250                                        + "... ignoring post processing");
251                            } else {
252                                document.afterWorkflowEngineProcess(event.isSuccessfullyProcessed());
253                                if (LOG.isDebugEnabled()) {
254                                    LOG.debug(new StringBuffer("finished after process method for document ").append(
255                                            event.getDocumentId()));
256                                }
257                            }
258                        } catch (Exception e) {
259                            logAndRethrow("after process", e);
260                        }
261                        return new ProcessDocReport(true, "");
262                    }
263                });
264    }
265
266    /**
267     * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
268     * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
269     *
270     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent)
271     */
272    @Override
273    public ProcessDocReport beforeProcess(final BeforeProcessEvent event) throws Exception {
274        return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
275                new Callable<ProcessDocReport>() {
276                    public ProcessDocReport call() throws Exception {
277
278                        try {
279                            if (LOG.isDebugEnabled()) {
280                                LOG.debug(new StringBuffer("started before process method for document ").append(
281                                        event.getDocumentId()));
282                            }
283                            Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
284                            if (ObjectUtils.isNull(document)) {
285                                // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
286                                LOG.warn("beforeProcess() Unable to load document with id "
287                                        + event.getDocumentId()
288                                        + "... ignoring post processing");
289                            } else {
290                                document.beforeWorkflowEngineProcess();
291                                if (LOG.isDebugEnabled()) {
292                                    LOG.debug(new StringBuffer("finished before process method for document ").append(
293                                            event.getDocumentId()));
294                                }
295                            }
296                        } catch (Exception e) {
297                            logAndRethrow("before process", e);
298                        }
299                        return new ProcessDocReport(true, "");
300                    }
301                });
302    }
303
304    /**
305     * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
306     * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
307     *
308     * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent)
309     */
310    public List<String> getDocumentIdsToLock(final DocumentLockingEvent event) throws Exception {
311        return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
312                new Callable<List<String>>() {
313                    public List<String> call() throws Exception {
314
315                        try {
316                            if (LOG.isDebugEnabled()) {
317                                LOG.debug(new StringBuffer("started get document ids to lock method for document ")
318                                        .append(event.getDocumentId()));
319                            }
320                            Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
321                            if (ObjectUtils.isNull(document)) {
322                                // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
323                                LOG.warn("getDocumentIdsToLock() Unable to load document with id "
324                                        + event.getDocumentId()
325                                        + "... ignoring post processing");
326                            } else {
327                                List<String> documentIdsToLock = document.getWorkflowEngineDocumentIdsToLock();
328                                if (LOG.isDebugEnabled()) {
329                                    LOG.debug(new StringBuffer("finished get document ids to lock method for document ")
330                                            .append(event.getDocumentId()));
331                                }
332                                if (documentIdsToLock == null) {
333                                    return null;
334                                }
335                                return documentIdsToLock;
336                            }
337                        } catch (Exception e) {
338                            logAndRethrow("before process", e);
339                        }
340                        return null;
341                    }
342                });
343    }
344
345    private void logAndRethrow(String changeType, Exception e) throws RuntimeException {
346        LOG.error("caught exception while handling " + changeType + " change", e);
347        logOptimisticDetails(5, e);
348
349        throw new RuntimeException("post processor caught exception while handling " + changeType + " change: " + e.getMessage(), e);
350    }
351
352    /**
353     * Logs further details of OptimisticLockExceptions, using the given depth value to limit recursion Just In Case
354     *
355     * @param depth
356     * @param t
357     */
358    private void logOptimisticDetails(int depth, Throwable t) {
359        if ((depth > 0) && (t != null)) {
360            if (t instanceof OptimisticLockException) {
361                OptimisticLockException o = (OptimisticLockException) t;
362
363                LOG.error("source of OptimisticLockException = " + o.getSourceObject().getClass().getName() + " ::= " + o.getSourceObject());
364            }
365            else {
366                Throwable cause = t.getCause();
367                if (cause != t) {
368                    logOptimisticDetails(--depth, cause);
369                }
370            }
371        }
372    }
373
374    /**
375     * Sets the documentService attribute value.
376     * @param documentService The documentService to set.
377     */
378    public final void setDocumentService(DocumentService documentService) {
379        this.documentService = documentService;
380    }
381
382    /**
383     * Establishes the UserSession if one does not already exist.
384     */
385    protected UserSession establishPostProcessorUserSession() throws WorkflowException {
386       if (GlobalVariables.getUserSession() == null) {
387            return new UserSession(KRADConstants.SYSTEM_USER);
388        } else {
389            return GlobalVariables.getUserSession();
390        }
391    }
392}