View Javadoc

1   /**
2    * Copyright 2005-2012 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.exception.WorkflowException;
22  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
23  import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
24  import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
25  import org.kuali.rice.kew.framework.postprocessor.DeleteEvent;
26  import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
27  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
28  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
29  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
30  import org.kuali.rice.krad.UserSession;
31  import org.kuali.rice.krad.document.Document;
32  import org.kuali.rice.krad.service.DocumentService;
33  import org.kuali.rice.krad.service.PostProcessorService;
34  import org.kuali.rice.krad.util.GlobalVariables;
35  import org.kuali.rice.krad.util.KRADConstants;
36  import org.kuali.rice.krad.util.ObjectUtils;
37  import org.springframework.transaction.annotation.Transactional;
38  
39  import java.util.List;
40  import java.util.concurrent.Callable;
41  
42  /**
43   * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents using the
44   * built into the document methods for handling route status and other routing changes that take place asyncronously and potentially
45   * on a different server.
46   */
47  @Transactional
48  public class PostProcessorServiceImpl implements PostProcessorService {
49  
50      private static Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class);
51  
52      private DocumentService documentService;
53  
54      /**
55       * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteStatusChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange)
56       */
57      @Override
58      public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception {
59          return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
60                  new Callable<ProcessDocReport>() {
61                      public ProcessDocReport call() throws Exception {
62  
63                          try {
64                              if (LOG.isInfoEnabled()) {
65                                  LOG.info(new StringBuffer("started handling route status change from ").append(
66                                          statusChangeEvent.getOldRouteStatus()).append(" to ").append(
67                                          statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
68                                          statusChangeEvent.getDocumentId()));
69                              }
70  
71                              Document document = documentService.getByDocumentHeaderId(
72                                      statusChangeEvent.getDocumentId());
73                              if (document == null) {
74                                  if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(
75                                          statusChangeEvent.getNewRouteStatus())) {
76                                      throw new RuntimeException(
77                                              "unable to load document " + statusChangeEvent.getDocumentId());
78                                  }
79                              } else {
80                                  document.doRouteStatusChange(statusChangeEvent);
81                                  // PLEASE READ BEFORE YOU MODIFY:
82                                  // we dont want to update the document on a Save, as this will cause an
83                                  // OptimisticLockException in many cases, because the DB versionNumber will be
84                                  // incremented one higher than the document in the browser, so when the user then
85                                  // hits Submit or Save again, the versionNumbers are out of synch, and the
86                                  // OptimisticLockException is thrown. This is not the optimal solution, and will
87                                  // be a problem anytime where the user can continue to edit the document after a
88                                  // workflow state change, without reloading the form.
89                                  if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) {
90                                      documentService.updateDocument(document);
91                                  }
92                              }
93                              if (LOG.isInfoEnabled()) {
94                                  LOG.info(new StringBuffer("finished handling route status change from ").append(
95                                          statusChangeEvent.getOldRouteStatus()).append(" to ").append(
96                                          statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
97                                          statusChangeEvent.getDocumentId()));
98                              }
99                          } catch (Exception e) {
100                             logAndRethrow("route status", e);
101                         }
102                         return new ProcessDocReport(true, "");
103                     }
104                 });
105     }
106 
107     /**
108      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteLevelChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange)
109      */
110     public ProcessDocReport doRouteLevelChange(final DocumentRouteLevelChange levelChangeEvent) throws Exception {
111         return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
112                 new Callable<ProcessDocReport>() {
113                     public ProcessDocReport call() throws Exception {
114 
115                         // on route level change we'll serialize the XML for the document. we
116                         // are doing this here cause it's a heavy hitter, and we
117                         // want to avoid the user waiting for this during sync processing
118                         try {
119                             if (LOG.isDebugEnabled()) {
120                                 LOG.debug(new StringBuffer("started handling route level change from ").append(
121                                         levelChangeEvent.getOldNodeName()).append(" to ").append(
122                                         levelChangeEvent.getNewNodeName()).append(" for document ").append(
123                                         levelChangeEvent.getDocumentId()));
124                             }
125 
126                             Document document = documentService.getByDocumentHeaderId(levelChangeEvent.getDocumentId());
127                             if (document == null) {
128                                 throw new RuntimeException(
129                                         "unable to load document " + levelChangeEvent.getDocumentId());
130                             }
131                             document.populateDocumentForRouting();
132                             document.doRouteLevelChange(levelChangeEvent);
133                             document.getDocumentHeader().getWorkflowDocument().saveDocumentData();
134                             if (LOG.isDebugEnabled()) {
135                                 LOG.debug(new StringBuffer("finished handling route level change from ").append(
136                                         levelChangeEvent.getOldNodeName()).append(" to ").append(
137                                         levelChangeEvent.getNewNodeName()).append(" for document ").append(
138                                         levelChangeEvent.getDocumentId()));
139                             }
140                         } catch (Exception e) {
141                             logAndRethrow("route level", e);
142                         }
143                         return new ProcessDocReport(true, "");
144                     }
145                 });
146     }
147 
148     /**
149      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doDeleteRouteHeader(org.kuali.rice.kew.framework.postprocessor.DeleteEvent)
150      */
151     @Override
152     public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws Exception {
153         return new ProcessDocReport(true, "");
154     }
155 
156     /**
157      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doActionTaken(org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent)
158      */
159     @Override
160     public ProcessDocReport doActionTaken(final ActionTakenEvent event) throws Exception {
161          return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
162             public ProcessDocReport call() throws Exception {
163                 try {
164                     if ( LOG.isDebugEnabled() ) {
165                         LOG.debug(new StringBuffer("started doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
166                     }
167                     Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
168                     if (ObjectUtils.isNull(document)) {
169                         // only throw an exception if we are not cancelling
170                         if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) {
171                             LOG.warn("doActionTaken() Unable to load document with id " + event.getDocumentId() +
172                                     " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(event.getActionTaken().getActionTaken()));
173         //                    throw new RuntimeException("unable to load document " + event.getDocumentId());
174                         }
175                     } else {
176                         document.doActionTaken(event);
177                         if ( LOG.isDebugEnabled() ) {
178                             LOG.debug(new StringBuffer("finished doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
179                         }
180                     }
181                 }
182                 catch (Exception e) {
183                     logAndRethrow("do action taken", e);
184                 }
185                 return new ProcessDocReport(true, "");
186 
187             }
188         });
189     }
190 
191     /**
192      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
193      * found the {@link Document#afterWorkflowEngineProcess(boolean)} method will be invoked on it
194      *
195      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#afterProcess(org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent)
196      */
197     @Override
198     public ProcessDocReport afterProcess(final AfterProcessEvent event) throws Exception {
199         return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
200                 new Callable<ProcessDocReport>() {
201                     public ProcessDocReport call() throws Exception {
202 
203                         try {
204                             if (LOG.isDebugEnabled()) {
205                                 LOG.debug(new StringBuffer("started after process method for document ").append(
206                                         event.getDocumentId()));
207                             }
208 
209                             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
210                             if (ObjectUtils.isNull(document)) {
211                                 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
212                                 LOG.warn("afterProcess() Unable to load document with id "
213                                         + event.getDocumentId()
214                                         + "... ignoring post processing");
215                             } else {
216                                 document.afterWorkflowEngineProcess(event.isSuccessfullyProcessed());
217                                 if (LOG.isDebugEnabled()) {
218                                     LOG.debug(new StringBuffer("finished after process method for document ").append(
219                                             event.getDocumentId()));
220                                 }
221                             }
222                         } catch (Exception e) {
223                             logAndRethrow("after process", e);
224                         }
225                         return new ProcessDocReport(true, "");
226                     }
227                 });
228     }
229 
230     /**
231      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
232      * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
233      *
234      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent)
235      */
236     @Override
237     public ProcessDocReport beforeProcess(final BeforeProcessEvent event) throws Exception {
238         return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
239                 new Callable<ProcessDocReport>() {
240                     public ProcessDocReport call() throws Exception {
241 
242                         try {
243                             if (LOG.isDebugEnabled()) {
244                                 LOG.debug(new StringBuffer("started before process method for document ").append(
245                                         event.getDocumentId()));
246                             }
247                             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
248                             if (ObjectUtils.isNull(document)) {
249                                 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
250                                 LOG.warn("beforeProcess() Unable to load document with id "
251                                         + event.getDocumentId()
252                                         + "... ignoring post processing");
253                             } else {
254                                 document.beforeWorkflowEngineProcess();
255                                 if (LOG.isDebugEnabled()) {
256                                     LOG.debug(new StringBuffer("finished before process method for document ").append(
257                                             event.getDocumentId()));
258                                 }
259                             }
260                         } catch (Exception e) {
261                             logAndRethrow("before process", e);
262                         }
263                         return new ProcessDocReport(true, "");
264                     }
265                 });
266     }
267 
268     /**
269      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
270      * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
271      *
272      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent)
273      */
274     public List<String> getDocumentIdsToLock(final DocumentLockingEvent event) throws Exception {
275         return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(),
276                 new Callable<List<String>>() {
277                     public List<String> call() throws Exception {
278 
279                         try {
280                             if (LOG.isDebugEnabled()) {
281                                 LOG.debug(new StringBuffer("started get document ids to lock method for document ")
282                                         .append(event.getDocumentId()));
283                             }
284                             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
285                             if (ObjectUtils.isNull(document)) {
286                                 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
287                                 LOG.warn("getDocumentIdsToLock() Unable to load document with id "
288                                         + event.getDocumentId()
289                                         + "... ignoring post processing");
290                             } else {
291                                 List<String> documentIdsToLock = document.getWorkflowEngineDocumentIdsToLock();
292                                 if (LOG.isDebugEnabled()) {
293                                     LOG.debug(new StringBuffer("finished get document ids to lock method for document ")
294                                             .append(event.getDocumentId()));
295                                 }
296                                 if (documentIdsToLock == null) {
297                                     return null;
298                                 }
299                                 return documentIdsToLock;
300                             }
301                         } catch (Exception e) {
302                             logAndRethrow("before process", e);
303                         }
304                         return null;
305                     }
306                 });
307     }
308 
309     private void logAndRethrow(String changeType, Exception e) throws RuntimeException {
310         LOG.error("caught exception while handling " + changeType + " change", e);
311         logOptimisticDetails(5, e);
312 
313         throw new RuntimeException("post processor caught exception while handling " + changeType + " change: " + e.getMessage(), e);
314     }
315 
316     /**
317      * Logs further details of OptimisticLockExceptions, using the given depth value to limit recursion Just In Case
318      *
319      * @param depth
320      * @param t
321      */
322     private void logOptimisticDetails(int depth, Throwable t) {
323         if ((depth > 0) && (t != null)) {
324             if (t instanceof OptimisticLockException) {
325                 OptimisticLockException o = (OptimisticLockException) t;
326 
327                 LOG.error("source of OptimisticLockException = " + o.getSourceObject().getClass().getName() + " ::= " + o.getSourceObject());
328             }
329             else {
330                 Throwable cause = t.getCause();
331                 if (cause != t) {
332                     logOptimisticDetails(--depth, cause);
333                 }
334             }
335         }
336     }
337 
338     /**
339      * Sets the documentService attribute value.
340      * @param documentService The documentService to set.
341      */
342     public final void setDocumentService(DocumentService documentService) {
343         this.documentService = documentService;
344     }
345 
346     /**
347      * Establishes the UserSession if one does not already exist.
348      */
349     protected UserSession establishPostProcessorUserSession() throws WorkflowException {
350        if (GlobalVariables.getUserSession() == null) {
351             return new UserSession(KRADConstants.SYSTEM_USER);
352         } else {
353             return GlobalVariables.getUserSession();
354         }
355     }
356 }