View Javadoc

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