View Javadoc

1   /**
2    * Copyright 2005-2011 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  
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(DocumentRouteStatusChange statusChangeEvent) throws Exception {
59          try {
60          	if ( LOG.isInfoEnabled() ) {
61          		LOG.info(new StringBuffer("started handling route status change from ").append(statusChangeEvent.getOldRouteStatus()).append(" to ").append(statusChangeEvent.getNewRouteStatus()).append(" for document ").append(statusChangeEvent.getDocumentId()));
62          	}
63              establishGlobalVariables();
64              Document document = documentService.getByDocumentHeaderId(statusChangeEvent.getDocumentId());
65              if (document == null) {
66                  if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(statusChangeEvent.getNewRouteStatus())) {
67                      throw new RuntimeException("unable to load document " + statusChangeEvent.getDocumentId());
68                  }
69              }
70              else {
71                  document.doRouteStatusChange(statusChangeEvent);
72                  // PLEASE READ BEFORE YOU MODIFY:
73                  // we dont want to update the document on a Save, as this will cause an
74                  // OptimisticLockException in many cases, because the DB versionNumber will be
75                  // incremented one higher than the document in the browser, so when the user then
76                  // hits Submit or Save again, the versionNumbers are out of synch, and the
77                  // OptimisticLockException is thrown. This is not the optimal solution, and will
78                  // be a problem anytime where the user can continue to edit the document after a
79                  // workflow state change, without reloading the form.
80                  if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) {
81                      documentService.updateDocument(document);
82                  }
83              }
84              if ( LOG.isInfoEnabled() ) {
85              	LOG.info(new StringBuffer("finished handling route status change from ").append(statusChangeEvent.getOldRouteStatus()).append(" to ").append(statusChangeEvent.getNewRouteStatus()).append(" for document ").append(statusChangeEvent.getDocumentId()));
86              }
87          }
88          catch (Exception e) {
89              logAndRethrow("route status", e);
90          }
91          return new ProcessDocReport(true, "");
92      }
93  
94      /**
95       * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteLevelChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange)
96       */
97      public ProcessDocReport doRouteLevelChange(DocumentRouteLevelChange levelChangeEvent) throws Exception {
98          // on route level change we'll serialize the XML for the document. we
99          // are doing this here cause it's a heavy hitter, and we
100         // want to avoid the user waiting for this during sync processing
101         try {
102         	if ( LOG.isDebugEnabled() ) {
103         		LOG.debug(new StringBuffer("started handling route level change from ").append(levelChangeEvent.getOldNodeName()).append(" to ").append(levelChangeEvent.getNewNodeName()).append(" for document ").append(levelChangeEvent.getDocumentId()));
104         	}
105             establishGlobalVariables();
106             Document document = documentService.getByDocumentHeaderId(levelChangeEvent.getDocumentId());
107             if (document == null) {
108                 throw new RuntimeException("unable to load document " + levelChangeEvent.getDocumentId());
109             }
110             document.populateDocumentForRouting();
111             document.doRouteLevelChange(levelChangeEvent);
112             document.getDocumentHeader().getWorkflowDocument().saveDocumentData();
113             if ( LOG.isDebugEnabled() ) {
114             	LOG.debug(new StringBuffer("finished handling route level change from ").append(levelChangeEvent.getOldNodeName()).append(" to ").append(levelChangeEvent.getNewNodeName()).append(" for document ").append(levelChangeEvent.getDocumentId()));
115             }
116         }
117         catch (Exception e) {
118             logAndRethrow("route level", e);
119         }
120         return new ProcessDocReport(true, "");
121     }
122 
123     /**
124      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doDeleteRouteHeader(org.kuali.rice.kew.framework.postprocessor.DeleteEvent)
125      */
126     @Override
127     public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws Exception {
128         return new ProcessDocReport(true, "");
129     }
130 
131     /**
132      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doActionTaken(org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent)
133      */
134     @Override
135     public ProcessDocReport doActionTaken(ActionTakenEvent event) throws Exception {
136         try {
137         	if ( LOG.isDebugEnabled() ) {
138         		LOG.debug(new StringBuffer("started doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
139         	}
140             establishGlobalVariables();
141             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
142             if (ObjectUtils.isNull(document)) {
143                 // only throw an exception if we are not cancelling
144                 if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) {
145                     LOG.warn("doActionTaken() Unable to load document with id " + event.getDocumentId() + 
146                             " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(event.getActionTaken().getActionTaken()));
147 //                    throw new RuntimeException("unable to load document " + event.getDocumentId());
148                 }
149             } else {
150                 document.doActionTaken(event);
151                 if ( LOG.isDebugEnabled() ) {
152                 	LOG.debug(new StringBuffer("finished doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId()));
153                 }
154             }
155         }
156         catch (Exception e) {
157             logAndRethrow("do action taken", e);
158         }
159         return new ProcessDocReport(true, "");
160     }
161 
162     /**
163      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
164      * found the {@link Document#afterWorkflowEngineProcess(boolean)} method will be invoked on it
165      * 
166      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#afterProcess(org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent)
167      */
168     @Override
169     public ProcessDocReport afterProcess(AfterProcessEvent event) throws Exception {
170         try {
171         	if ( LOG.isDebugEnabled() ) {
172         		LOG.debug(new StringBuffer("started after process method for document ").append(event.getDocumentId()));
173         	}
174             establishGlobalVariables();
175             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
176             if (ObjectUtils.isNull(document)) {
177                 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
178                 LOG.warn("afterProcess() Unable to load document with id " + event.getDocumentId() + "... ignoring post processing");
179             } else {
180                 document.afterWorkflowEngineProcess(event.isSuccessfullyProcessed());
181                 if ( LOG.isDebugEnabled() ) {
182                 	LOG.debug(new StringBuffer("finished after process method for document ").append(event.getDocumentId()));
183                 }
184             }
185         }
186         catch (Exception e) {
187             logAndRethrow("after process", e);
188         }
189         return new ProcessDocReport(true, "");
190     }
191 
192     /**
193      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
194      * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
195      * 
196      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent)
197      */
198     @Override
199     public ProcessDocReport beforeProcess(BeforeProcessEvent event) throws Exception {
200         try {
201         	if ( LOG.isDebugEnabled() ) {
202         		LOG.debug(new StringBuffer("started before process method for document ").append(event.getDocumentId()));
203         	}
204             establishGlobalVariables();
205             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
206             if (ObjectUtils.isNull(document)) {
207                 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
208                 LOG.warn("beforeProcess() Unable to load document with id " + event.getDocumentId() + "... ignoring post processing");
209             } else {
210                 document.beforeWorkflowEngineProcess();
211                 if ( LOG.isDebugEnabled() ) {
212                 	LOG.debug(new StringBuffer("finished before process method for document ").append(event.getDocumentId()));
213                 }
214             }
215         }
216         catch (Exception e) {
217             logAndRethrow("before process", e);
218         }
219         return new ProcessDocReport(true, "");
220     }
221     
222     /**
223      * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is
224      * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it
225      * 
226      * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent)
227      */
228     public List<String> getDocumentIdsToLock(DocumentLockingEvent event) throws Exception {
229         try {
230         	if ( LOG.isDebugEnabled() ) {
231         		LOG.debug(new StringBuffer("started get document ids to lock method for document ").append(event.getDocumentId()));
232         	}
233             establishGlobalVariables();
234             Document document = documentService.getByDocumentHeaderId(event.getDocumentId());
235             if (ObjectUtils.isNull(document)) {
236                 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process
237                 LOG.warn("getDocumentIdsToLock() Unable to load document with id " + event.getDocumentId() + "... ignoring post processing");
238             } else {
239                 List<String> documentIdsToLock = document.getWorkflowEngineDocumentIdsToLock();
240                 if ( LOG.isDebugEnabled() ) {
241                 	LOG.debug(new StringBuffer("finished get document ids to lock method for document ").append(event.getDocumentId()));
242                 }
243                 if (documentIdsToLock == null) {
244                 	return null;
245                 }
246                 return documentIdsToLock;                
247             }
248         }
249         catch (Exception e) {
250             logAndRethrow("before process", e);
251         }
252         return null;
253     }
254 
255     private void logAndRethrow(String changeType, Exception e) throws RuntimeException {
256         LOG.error("caught exception while handling " + changeType + " change", e);
257         logOptimisticDetails(5, e);
258 
259         throw new RuntimeException("post processor caught exception while handling " + changeType + " change: " + e.getMessage(), e);
260     }
261 
262     /**
263      * Logs further details of OptimisticLockExceptions, using the given depth value to limit recursion Just In Case
264      *
265      * @param depth
266      * @param t
267      */
268     private void logOptimisticDetails(int depth, Throwable t) {
269         if ((depth > 0) && (t != null)) {
270             if (t instanceof OptimisticLockException) {
271                 OptimisticLockException o = (OptimisticLockException) t;
272 
273                 LOG.error("source of OptimisticLockException = " + o.getSourceObject().getClass().getName() + " ::= " + o.getSourceObject());
274             }
275             else {
276                 Throwable cause = t.getCause();
277                 if (cause != t) {
278                     logOptimisticDetails(--depth, cause);
279                 }
280             }
281         }
282     }
283 
284     /**
285      * Sets the documentService attribute value.
286      * @param documentService The documentService to set.
287      */
288     public final void setDocumentService(DocumentService documentService) {
289         this.documentService = documentService;
290     }
291 
292     /**
293      * Establishes the UserSession if one does not already exist.
294      */
295     protected void establishGlobalVariables() throws WorkflowException {
296         if (GlobalVariables.getUserSession() == null) {
297             GlobalVariables.setUserSession(new UserSession(KRADConstants.SYSTEM_USER));
298         }
299         GlobalVariables.clear();
300     }
301 
302 }