001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.service.impl; 017 018import org.apache.log4j.Logger; 019import org.apache.ojb.broker.OptimisticLockException; 020import org.kuali.rice.kew.api.KewApiConstants; 021import org.kuali.rice.kew.api.action.ActionType; 022import org.kuali.rice.kew.api.exception.WorkflowException; 023import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent; 024import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent; 025import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent; 026import org.kuali.rice.kew.framework.postprocessor.DeleteEvent; 027import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent; 028import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange; 029import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 030import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 031import org.kuali.rice.krad.UserSession; 032import org.kuali.rice.krad.document.Document; 033import org.kuali.rice.krad.service.DocumentService; 034import org.kuali.rice.krad.service.PostProcessorService; 035import org.kuali.rice.krad.util.GlobalVariables; 036import org.kuali.rice.krad.util.KRADConstants; 037import org.kuali.rice.krad.util.ObjectUtils; 038import org.springframework.transaction.annotation.Transactional; 039 040import java.util.List; 041import java.util.concurrent.Callable; 042 043/** 044 * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents using the 045 * built into the document methods for handling route status and other routing changes that take place asyncronously and potentially 046 * on a different server. 047 */ 048@Transactional 049public class PostProcessorServiceImpl implements PostProcessorService { 050 051 private static Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class); 052 053 private DocumentService documentService; 054 055 /** 056 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteStatusChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange) 057 */ 058 @Override 059 public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception { 060 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), 061 new Callable<ProcessDocReport>() { 062 public ProcessDocReport call() throws Exception { 063 064 try { 065 if (LOG.isInfoEnabled()) { 066 LOG.info(new StringBuffer("started handling route status change from ").append( 067 statusChangeEvent.getOldRouteStatus()).append(" to ").append( 068 statusChangeEvent.getNewRouteStatus()).append(" for document ").append( 069 statusChangeEvent.getDocumentId())); 070 } 071 072 Document document = documentService.getByDocumentHeaderId( 073 statusChangeEvent.getDocumentId()); 074 if (document == null) { 075 if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals( 076 statusChangeEvent.getNewRouteStatus())) { 077 throw new RuntimeException( 078 "unable to load document " + statusChangeEvent.getDocumentId()); 079 } 080 } else { 081 document.doRouteStatusChange(statusChangeEvent); 082 // PLEASE READ BEFORE YOU MODIFY: 083 // we dont want to update the document on a Save, as this will cause an 084 // OptimisticLockException in many cases, because the DB versionNumber will be 085 // incremented one higher than the document in the browser, so when the user then 086 // hits Submit or Save again, the versionNumbers are out of synch, and the 087 // OptimisticLockException is thrown. This is not the optimal solution, and will 088 // be a problem anytime where the user can continue to edit the document after a 089 // workflow state change, without reloading the form. 090 if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) { 091 documentService.updateDocument(document); 092 } 093 } 094 if (LOG.isInfoEnabled()) { 095 LOG.info(new StringBuffer("finished handling route status change from ").append( 096 statusChangeEvent.getOldRouteStatus()).append(" to ").append( 097 statusChangeEvent.getNewRouteStatus()).append(" for document ").append( 098 statusChangeEvent.getDocumentId())); 099 } 100 } catch (Exception e) { 101 logAndRethrow("route status", e); 102 } 103 return new ProcessDocReport(true, ""); 104 } 105 }); 106 } 107 108 /** 109 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doRouteLevelChange(org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange) 110 */ 111 public ProcessDocReport doRouteLevelChange(final DocumentRouteLevelChange levelChangeEvent) throws Exception { 112 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), 113 new Callable<ProcessDocReport>() { 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 StringBuffer("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( 130 "unable to load document " + levelChangeEvent.getDocumentId()); 131 } 132 document.populateDocumentForRouting(); 133 document.doRouteLevelChange(levelChangeEvent); 134 document.getDocumentHeader().getWorkflowDocument().saveDocumentData(); 135 if (LOG.isDebugEnabled()) { 136 LOG.debug(new StringBuffer("finished handling route level change from ").append( 137 levelChangeEvent.getOldNodeName()).append(" to ").append( 138 levelChangeEvent.getNewNodeName()).append(" for document ").append( 139 levelChangeEvent.getDocumentId())); 140 } 141 } catch (Exception e) { 142 logAndRethrow("route level", e); 143 } 144 return new ProcessDocReport(true, ""); 145 } 146 }); 147 } 148 149 /** 150 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doDeleteRouteHeader(org.kuali.rice.kew.framework.postprocessor.DeleteEvent) 151 */ 152 @Override 153 public ProcessDocReport doDeleteRouteHeader(DeleteEvent event) throws Exception { 154 return new ProcessDocReport(true, ""); 155 } 156 157 /** 158 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#doActionTaken(org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent) 159 */ 160 @Override 161 public ProcessDocReport doActionTaken(final ActionTakenEvent event) throws Exception { 162 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), new Callable<ProcessDocReport>() { 163 public ProcessDocReport call() throws Exception { 164 try { 165 if ( LOG.isDebugEnabled() ) { 166 LOG.debug(new StringBuffer("started doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId())); 167 } 168 Document document = documentService.getByDocumentHeaderId(event.getDocumentId()); 169 if (ObjectUtils.isNull(document)) { 170 // only throw an exception if we are not cancelling 171 if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) { 172 LOG.warn("doActionTaken() Unable to load document with id " + event.getDocumentId() + 173 " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(event.getActionTaken().getActionTaken())); 174 // throw new RuntimeException("unable to load document " + event.getDocumentId()); 175 } 176 } else { 177 document.doActionTaken(event); 178 if ( LOG.isDebugEnabled() ) { 179 LOG.debug(new StringBuffer("finished doing action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId())); 180 } 181 } 182 } 183 catch (Exception e) { 184 logAndRethrow("do action taken", e); 185 } 186 return new ProcessDocReport(true, ""); 187 188 } 189 }); 190 } 191 192 /** 193 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#afterActionTaken(org.kuali.rice.kew.api.action.ActionType, org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent) 194 */ 195 @Override 196 public ProcessDocReport afterActionTaken(final ActionType performed, final ActionTakenEvent event) throws Exception { 197 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), new Callable<ProcessDocReport>() { 198 public ProcessDocReport call() throws Exception { 199 try { 200 if ( LOG.isDebugEnabled() ) { 201 LOG.debug(new StringBuffer("started doing after action taken for action performed code " + performed.getCode() + " and action taken code ").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId())); 202 } 203 Document document = documentService.getByDocumentHeaderId(event.getDocumentId()); 204 if (ObjectUtils.isNull(document)) { 205 // only throw an exception if we are not cancelling 206 if (!KewApiConstants.ACTION_TAKEN_CANCELED.equals(event.getActionTaken())) { 207 LOG.warn("afterActionTaken() Unable to load document with id " + event.getDocumentId() + 208 " using action taken code '" + KewApiConstants.ACTION_TAKEN_CD.get(event.getActionTaken().getActionTaken())); 209 // throw new RuntimeException("unable to load document " + event.getDocumentId()); 210 } 211 } else { 212 document.afterActionTaken(performed, event); 213 if ( LOG.isDebugEnabled() ) { 214 LOG.debug(new StringBuffer("finished doing after action taken for action taken code").append(event.getActionTaken().getActionTaken()).append(" for document ").append(event.getDocumentId())); 215 } 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 is 229 * found the {@link Document#afterWorkflowEngineProcess(boolean)} method will be invoked on it 230 * 231 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#afterProcess(org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent) 232 */ 233 @Override 234 public ProcessDocReport afterProcess(final AfterProcessEvent event) throws Exception { 235 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), 236 new Callable<ProcessDocReport>() { 237 public ProcessDocReport call() throws Exception { 238 239 try { 240 if (LOG.isDebugEnabled()) { 241 LOG.debug(new StringBuffer("started after process method for document ").append( 242 event.getDocumentId())); 243 } 244 245 Document document = documentService.getByDocumentHeaderId(event.getDocumentId()); 246 if (ObjectUtils.isNull(document)) { 247 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process 248 LOG.warn("afterProcess() Unable to load document with id " 249 + event.getDocumentId() 250 + "... ignoring post processing"); 251 } else { 252 document.afterWorkflowEngineProcess(event.isSuccessfullyProcessed()); 253 if (LOG.isDebugEnabled()) { 254 LOG.debug(new StringBuffer("finished after process method for document ").append( 255 event.getDocumentId())); 256 } 257 } 258 } catch (Exception e) { 259 logAndRethrow("after process", e); 260 } 261 return new ProcessDocReport(true, ""); 262 } 263 }); 264 } 265 266 /** 267 * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is 268 * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it 269 * 270 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent) 271 */ 272 @Override 273 public ProcessDocReport beforeProcess(final BeforeProcessEvent event) throws Exception { 274 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), 275 new Callable<ProcessDocReport>() { 276 public ProcessDocReport call() throws Exception { 277 278 try { 279 if (LOG.isDebugEnabled()) { 280 LOG.debug(new StringBuffer("started before process method for document ").append( 281 event.getDocumentId())); 282 } 283 Document document = documentService.getByDocumentHeaderId(event.getDocumentId()); 284 if (ObjectUtils.isNull(document)) { 285 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process 286 LOG.warn("beforeProcess() Unable to load document with id " 287 + event.getDocumentId() 288 + "... ignoring post processing"); 289 } else { 290 document.beforeWorkflowEngineProcess(); 291 if (LOG.isDebugEnabled()) { 292 LOG.debug(new StringBuffer("finished before process method for document ").append( 293 event.getDocumentId())); 294 } 295 } 296 } catch (Exception e) { 297 logAndRethrow("before process", e); 298 } 299 return new ProcessDocReport(true, ""); 300 } 301 }); 302 } 303 304 /** 305 * This method first checks to see if the document can be retrieved by the {@link DocumentService}. If the document is 306 * found the {@link Document#beforeWorkflowEngineProcess()} method will be invoked on it 307 * 308 * @see org.kuali.rice.kew.framework.postprocessor.PostProcessor#beforeProcess(org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent) 309 */ 310 public List<String> getDocumentIdsToLock(final DocumentLockingEvent event) throws Exception { 311 return GlobalVariables.doInNewGlobalVariables(establishPostProcessorUserSession(), 312 new Callable<List<String>>() { 313 public List<String> call() throws Exception { 314 315 try { 316 if (LOG.isDebugEnabled()) { 317 LOG.debug(new StringBuffer("started get document ids to lock method for document ") 318 .append(event.getDocumentId())); 319 } 320 Document document = documentService.getByDocumentHeaderId(event.getDocumentId()); 321 if (ObjectUtils.isNull(document)) { 322 // no way to verify if this is the processing as a result of a cancel so assume null document is ok to process 323 LOG.warn("getDocumentIdsToLock() Unable to load document with id " 324 + event.getDocumentId() 325 + "... ignoring post processing"); 326 } else { 327 List<String> documentIdsToLock = document.getWorkflowEngineDocumentIdsToLock(); 328 if (LOG.isDebugEnabled()) { 329 LOG.debug(new StringBuffer("finished get document ids to lock method for document ") 330 .append(event.getDocumentId())); 331 } 332 if (documentIdsToLock == null) { 333 return null; 334 } 335 return documentIdsToLock; 336 } 337 } catch (Exception e) { 338 logAndRethrow("before process", e); 339 } 340 return null; 341 } 342 }); 343 } 344 345 private void logAndRethrow(String changeType, Exception e) throws RuntimeException { 346 LOG.error("caught exception while handling " + changeType + " change", e); 347 logOptimisticDetails(5, e); 348 349 throw new RuntimeException("post processor caught exception while handling " + changeType + " change: " + e.getMessage(), e); 350 } 351 352 /** 353 * Logs further details of OptimisticLockExceptions, using the given depth value to limit recursion Just In Case 354 * 355 * @param depth 356 * @param t 357 */ 358 private void logOptimisticDetails(int depth, Throwable t) { 359 if ((depth > 0) && (t != null)) { 360 if (t instanceof OptimisticLockException) { 361 OptimisticLockException o = (OptimisticLockException) t; 362 363 LOG.error("source of OptimisticLockException = " + o.getSourceObject().getClass().getName() + " ::= " + o.getSourceObject()); 364 } 365 else { 366 Throwable cause = t.getCause(); 367 if (cause != t) { 368 logOptimisticDetails(--depth, cause); 369 } 370 } 371 } 372 } 373 374 /** 375 * Sets the documentService attribute value. 376 * @param documentService The documentService to set. 377 */ 378 public final void setDocumentService(DocumentService documentService) { 379 this.documentService = documentService; 380 } 381 382 /** 383 * Establishes the UserSession if one does not already exist. 384 */ 385 protected UserSession establishPostProcessorUserSession() throws WorkflowException { 386 if (GlobalVariables.getUserSession() == null) { 387 return new UserSession(KRADConstants.SYSTEM_USER); 388 } else { 389 return GlobalVariables.getUserSession(); 390 } 391 } 392}