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 java.util.List;
19  import java.util.concurrent.Callable;
20  
21  import org.apache.commons.beanutils.PropertyUtils;
22  import org.apache.log4j.Logger;
23  import org.kuali.rice.kew.api.KewApiConstants;
24  import org.kuali.rice.kew.api.action.ActionType;
25  import org.kuali.rice.kew.api.exception.WorkflowException;
26  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
27  import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
28  import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
29  import org.kuali.rice.kew.framework.postprocessor.DeleteEvent;
30  import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
31  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
32  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
33  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
34  import org.kuali.rice.krad.UserSession;
35  import org.kuali.rice.krad.document.Document;
36  import org.kuali.rice.krad.service.DocumentService;
37  import org.kuali.rice.krad.service.PostProcessorService;
38  import org.kuali.rice.krad.util.GlobalVariables;
39  import org.kuali.rice.krad.util.KRADConstants;
40  import org.kuali.rice.krad.util.LegacyUtils;
41  import org.springframework.dao.OptimisticLockingFailureException;
42  import org.springframework.transaction.annotation.Transactional;
43  
44  /**
45   * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents
46   * using the built into the document methods for handling route status and other routing changes that take place
47   * asyncronously and potentially on a different server.
48   *
49   * @author Kuali Rice Team (rice.collab@kuali.org)
50   */
51  @Transactional
52  public class PostProcessorServiceImpl implements PostProcessorService {
53  
54      private static final Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class);
55  
56      private DocumentService documentService;
57  
58      @Override
59      public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception {
60          return LegacyUtils.doInLegacyContext(statusChangeEvent.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
61              @Override
62              public ProcessDocReport call() throws Exception {
63  
64                  try {
65                      if (LOG.isInfoEnabled()) {
66                          LOG.info(new StringBuilder("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(statusChangeEvent.getDocumentId());
73                      if (document == null) {
74                          if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(statusChangeEvent.getNewRouteStatus())) {
75                              throw new RuntimeException("unable to load document " + statusChangeEvent.getDocumentId());
76                          }
77                      } else {
78                          document.doRouteStatusChange(statusChangeEvent);
79                          // PLEASE READ BEFORE YOU MODIFY:
80                          // we dont want to update the document on a Save, as this will cause an
81                          // OptimisticLockException in many cases, because the DB versionNumber will be
82                          // incremented one higher than the document in the browser, so when the user then
83                          // hits Submit or Save again, the versionNumbers are out of synch, and the
84                          // OptimisticLockException is thrown. This is not the optimal solution, and will
85                          // be a problem anytime where the user can continue to edit the document after a
86                          // workflow state change, without reloading the form.
87                          if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) {
88                              document = documentService.updateDocument(document);
89  //                            document = KradDataServiceLocator.getDataObjectService().save(document, PersistenceOption.FLUSH);
90                          }
91  
92                      }
93                      if (LOG.isInfoEnabled()) {
94                          LOG.info(new StringBuilder("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     @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 }