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.log4j.Logger;
22  import org.apache.ojb.broker.OptimisticLockException;
23  import org.kuali.rice.kew.api.KewApiConstants;
24  import org.kuali.rice.kew.api.KewApiServiceLocator;
25  import org.kuali.rice.kew.api.action.ActionType;
26  import org.kuali.rice.kew.api.exception.WorkflowException;
27  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
28  import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
29  import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
30  import org.kuali.rice.kew.framework.postprocessor.DeleteEvent;
31  import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
32  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
33  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
34  import org.kuali.rice.kew.framework.postprocessor.IDocumentEvent;
35  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
36  import org.kuali.rice.krad.UserSession;
37  import org.kuali.rice.krad.datadictionary.DocumentEntry;
38  import org.kuali.rice.krad.document.Document;
39  import org.kuali.rice.krad.service.DocumentService;
40  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
41  import org.kuali.rice.krad.service.PostProcessorService;
42  import org.kuali.rice.krad.util.GlobalVariables;
43  import org.kuali.rice.krad.util.KRADConstants;
44  import org.kuali.rice.krad.util.LegacyUtils;
45  import org.springframework.transaction.annotation.Transactional;
46  
47  /**
48   * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents
49   * using the built into the document methods for handling route status and other routing changes that take place
50   * asyncronously and potentially on a different server.
51   *
52   * @author Kuali Rice Team (rice.collab@kuali.org)
53   */
54  @Transactional
55  public class PostProcessorServiceImpl implements PostProcessorService {
56  
57      private static Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class);
58  
59      private DocumentService documentService;
60  
61      @Override
62      public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception {
63          return LegacyUtils.doInLegacyContext(statusChangeEvent.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
64              @Override
65              public ProcessDocReport call() throws Exception {
66  
67                  try {
68                      if (LOG.isInfoEnabled()) {
69                          LOG.info(new StringBuilder("started handling route status change from ").append(
70                                  statusChangeEvent.getOldRouteStatus()).append(" to ").append(
71                                  statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
72                                  statusChangeEvent.getDocumentId()));
73                      }
74  
75                      Document document = documentService.getByDocumentHeaderId(statusChangeEvent.getDocumentId());
76                      if (document == null) {
77                          if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(statusChangeEvent.getNewRouteStatus())) {
78                              throw new RuntimeException("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                              document = documentService.updateDocument(document);
92  //                            document = KradDataServiceLocator.getDataObjectService().save(document, PersistenceOption.FLUSH);
93                          }
94  
95                      }
96                      if (LOG.isInfoEnabled()) {
97                          LOG.info(new StringBuilder("finished handling route status change from ").append(
98                                  statusChangeEvent.getOldRouteStatus()).append(" to ").append(
99                                  statusChangeEvent.getNewRouteStatus()).append(" for document ").append(
100                                 statusChangeEvent.getDocumentId()));
101                     }
102                 } catch (Exception e) {
103                     logAndRethrow("route status", e);
104                 }
105                 return new ProcessDocReport(true, "");
106             }
107         });
108     }
109 
110     @Override
111     public ProcessDocReport doRouteLevelChange(final DocumentRouteLevelChange levelChangeEvent) throws Exception {
112         return LegacyUtils.doInLegacyContext(levelChangeEvent.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
113             @Override
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 StringBuilder("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("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 StringBuilder("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     @Override
149     public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws Exception {
150         return new ProcessDocReport(true, "");
151     }
152 
153     @Override
154     public ProcessDocReport doActionTaken(final ActionTakenEvent event) throws Exception {
155         return LegacyUtils.doInLegacyContext(event.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
156             @Override
157             public ProcessDocReport call() throws Exception {
158                 try {
159                     if (LOG.isDebugEnabled()) {
160                         LOG.debug(new StringBuilder("started doing action taken for action taken code").append(
161                                 event.getActionTaken().getActionTaken()).append(" for document ").append(
162                                 event.getDocumentId()));
163                     }
164                     Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
165                     if (document == null) {
166                         // only throw an exception if we are not cancelling
167                         if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) {
168                             LOG.warn("doActionTaken() Unable to load document with id " + event.getDocumentId() +
169                                     " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(
170                                     event.getActionTaken().getActionTaken()));
171                         }
172                     } else {
173                         document.doActionTaken(event);
174                         if (LOG.isDebugEnabled()) {
175                             LOG.debug(new StringBuilder("finished doing action taken for action taken code").append(
176                                     event.getActionTaken().getActionTaken()).append(" for document ").append(
177                                     event.getDocumentId()));
178                         }
179                     }
180                 } catch (Exception e) {
181                     logAndRethrow("do action taken", e);
182                 }
183                 return new ProcessDocReport(true, "");
184 
185             }
186         });
187     }
188 
189     @Override
190     public ProcessDocReport afterActionTaken(final ActionType performed,
191             final ActionTakenEvent event) throws Exception {
192         return LegacyUtils.doInLegacyContext(event.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
193             @Override
194             public ProcessDocReport call() throws Exception {
195                 try {
196                     if (LOG.isDebugEnabled()) {
197                         LOG.debug(new StringBuilder("started doing after action taken for action performed code "
198                                 + performed.getCode()
199                                 + " and action taken code ").append(event.getActionTaken().getActionTaken()).append(
200                                 " for document ").append(event.getDocumentId()));
201                     }
202                     Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
203                     if (document == null) {
204                         // only throw an exception if we are not cancelling
205                         if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) {
206                             LOG.warn("afterActionTaken() Unable to load document with id " + event.getDocumentId() +
207                                     " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(
208                                     event.getActionTaken().getActionTaken()));
209                         }
210                     } else {
211                         document.afterActionTaken(performed, event);
212                         if (LOG.isDebugEnabled()) {
213                             LOG.debug(new StringBuilder("finished doing after action taken for action taken code")
214                                     .append(event.getActionTaken().getActionTaken()).append(" for document ").append(
215                                             event.getDocumentId()));
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
229      * is
230      * found the {@link Document#afterWorkflowEngineProcess(boolean)} method will be invoked on it
231      */
232     @Override
233     public ProcessDocReport afterProcess(final AfterProcessEvent event) throws Exception {
234         return LegacyUtils.doInLegacyContext(event.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
235             @Override
236             public ProcessDocReport call() throws Exception {
237 
238                 try {
239                     if (LOG.isDebugEnabled()) {
240                         LOG.debug(new StringBuilder("started after process method for document ").append(
241                                 event.getDocumentId()));
242                     }
243 
244                     Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
245                     if (document == null) {
246                         // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
247                         LOG.warn("afterProcess() Unable to load document with id "
248                                 + event.getDocumentId()
249                                 + "... ignoring post processing");
250                     } else {
251                         document.afterWorkflowEngineProcess(event.isSuccessfullyProcessed());
252                         if (LOG.isDebugEnabled()) {
253                             LOG.debug(new StringBuilder("finished after process method for document ").append(
254                                     event.getDocumentId()));
255                         }
256                     }
257                 } catch (Exception e) {
258                     logAndRethrow("after process", e);
259                 }
260                 return new ProcessDocReport(true, "");
261             }
262         });
263     }
264 
265     /**
266      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document
267      * is found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
268      */
269     @Override
270     public ProcessDocReport beforeProcess(final BeforeProcessEvent event) throws Exception {
271         return LegacyUtils.doInLegacyContext(event.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() {
272             @Override
273             public ProcessDocReport call() throws Exception {
274 
275                 try {
276                     if (LOG.isDebugEnabled()) {
277                         LOG.debug(new StringBuilder("started before process method for document ").append(
278                                 event.getDocumentId()));
279                     }
280                     Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
281                     if (document == null) {
282                         // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
283                         LOG.warn("beforeProcess() Unable to load document with id "
284                                 + event.getDocumentId()
285                                 + "... ignoring post processing");
286                     } else {
287                         document.beforeWorkflowEngineProcess();
288                         if (LOG.isDebugEnabled()) {
289                             LOG.debug(new StringBuilder("finished before process method for document ").append(
290                                     event.getDocumentId()));
291                         }
292                     }
293                 } catch (Exception e) {
294                     logAndRethrow("before process", e);
295                 }
296                 return new ProcessDocReport(true, "");
297             }
298         });
299     }
300 
301     /**
302      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document
303      * is
304      * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
305      */
306     @Override
307     public List<String> getDocumentIdsToLock(final DocumentLockingEvent event) throws Exception {
308         return LegacyUtils.doInLegacyContext(event.getDocumentId(), establishPostProcessorUserSession(), new Callable<List<String>>() {
309             @Override
310             public List<String> call() throws Exception {
311 
312                 try {
313                     if (LOG.isDebugEnabled()) {
314                         LOG.debug(new StringBuilder("started get document ids to lock method for document ").append(
315                                 event.getDocumentId()));
316                     }
317                     Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
318                     if (document == null) {
319                         // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
320                         LOG.warn("getDocumentIdsToLock() Unable to load document with id "
321                                 + event.getDocumentId()
322                                 + "... ignoring post processing");
323                     } else {
324                         List<String> documentIdsToLock = document.getWorkflowEngineDocumentIdsToLock();
325                         if (LOG.isDebugEnabled()) {
326                             LOG.debug(new StringBuilder("finished get document ids to lock method for document ").append(
327                                     event.getDocumentId()));
328                         }
329                         if (documentIdsToLock == null) {
330                             return null;
331                         }
332                         return documentIdsToLock;
333                     }
334                 } catch (Exception e) {
335                     logAndRethrow("before process", e);
336                 }
337                 return null;
338             }
339         });
340     }
341 
342     private void logAndRethrow(String changeType, Exception e) throws RuntimeException {
343         LOG.error("caught exception while handling " + changeType + " change", e);
344         logOptimisticDetails(5, e);
345 
346         throw new RuntimeException("post processor caught exception while handling " + changeType + " change: " + e.getMessage(), e);
347     }
348 
349     /**
350      * Logs further details of OptimisticLockExceptions, using the given depth value to limit recursion Just In Case
351      *
352      * @param depth
353      * @param t
354      */
355     private void logOptimisticDetails(int depth, Throwable t) {
356         if ((depth > 0) && (t != null)) {
357             if (t instanceof OptimisticLockException) {
358                 OptimisticLockException o = (OptimisticLockException) t;
359 
360                 LOG.error("source of OptimisticLockException = " + o.getSourceObject().getClass().getName() + " ::= " + o.getSourceObject());
361             }
362             else {
363                 Throwable cause = t.getCause();
364                 if (cause != t) {
365                     logOptimisticDetails(--depth, cause);
366                 }
367             }
368         }
369     }
370 
371     /**
372      * Sets the documentService attribute value.
373      * @param documentService The documentService to set.
374      */
375     public final void setDocumentService(DocumentService documentService) {
376         this.documentService = documentService;
377     }
378 
379     /**
380      * Establishes the UserSession if one does not already exist.
381      */
382     protected UserSession establishPostProcessorUserSession() throws WorkflowException {
383        if (GlobalVariables.getUserSession() == null) {
384             return new UserSession(KRADConstants.SYSTEM_USER);
385         } else {
386             return GlobalVariables.getUserSession();
387         }
388     }
389 
390 }