View Javadoc

1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.service.impl;
17  
18  import org.apache.log4j.Logger;
19  import org.apache.ojb.broker.OptimisticLockException;
20  import org.kuali.rice.kew.api.KewApiConstants;
21  import org.kuali.rice.kew.api.action.ActionType;
22  import org.kuali.rice.kew.api.exception.WorkflowException;
23  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
24  import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
25  import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
26  import org.kuali.rice.kew.framework.postprocessor.DeleteEvent;
27  import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
28  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
29  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
30  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
31  import org.kuali.rice.krad.UserSession;
32  import org.kuali.rice.krad.document.Document;
33  import org.kuali.rice.krad.service.DocumentService;
34  import org.kuali.rice.krad.service.PostProcessorService;
35  import org.kuali.rice.krad.util.GlobalVariables;
36  import org.kuali.rice.krad.util.KRADConstants;
37  import org.kuali.rice.krad.util.ObjectUtils;
38  import org.springframework.transaction.annotation.Transactional;
39  
40  import java.util.List;
41  import java.util.concurrent.Callable;
42  
43  /**
44   * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents using the
45   * built into the document methods for handling route status and other routing changes that take place asyncronously and potentially
46   * on a different server.
47   */
48  @Transactional
49  public class PostProcessorServiceImpl implements PostProcessorService {
50  
51      private static Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class);
52  
53      private DocumentService documentService;
54  
55      /**
56       * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteStatusChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange)
57       */
58      @Override
59      public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception {
60          return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
61                  new Callable<ProcessDocReport>() {
62                      public ProcessDocReport call() throws Exception {
63  
64                          try {
65                              if (LOG.isInfoEnabled()) {
66                                  LOG.info(new StringBuffer("started handling route status change from ").append(
67                                          statusChangeEvent.getOldRouteStatus()).append(" to ").append(
68                                          statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
69                                          statusChangeEvent.getDocumentId()));
70                              }
71  
72                              Document document = documentService.getByDocumentHeaderId(
73                                      statusChangeEvent.getDocumentId());
74                              if (document == null) {
75                                  if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(
76                                          statusChangeEvent.getNewRouteStatus())) {
77                                      throw new RuntimeException(
78                                              "unable to load document " + statusChangeEvent.getDocumentId());
79                                  }
80                              } else {
81                                  document.doRouteStatusChange(statusChangeEvent);
82                                  // PLEASE READ BEFORE YOU MODIFY:
83                                  // we dont want to update the document on a Save, as this will cause an
84                                  // OptimisticLockException in many cases, because the DB versionNumber will be
85                                  // incremented one higher than the document in the browser, so when the user then
86                                  // hits Submit or Save again, the versionNumbers are out of synch, and the
87                                  // OptimisticLockException is thrown. This is not the optimal solution, and will
88                                  // be a problem anytime where the user can continue to edit the document after a
89                                  // workflow state change, without reloading the form.
90                                  if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) {
91                                      documentService.updateDocument(document);
92                                  }
93                              }
94                              if (LOG.isInfoEnabled()) {
95                                  LOG.info(new StringBuffer("finished handling route status change from ").append(
96                                          statusChangeEvent.getOldRouteStatus()).append(" to ").append(
97                                          statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
98                                          statusChangeEvent.getDocumentId()));
99                              }
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 }