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