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 java.util.List; 019import java.util.concurrent.Callable; 020 021import org.apache.commons.beanutils.PropertyUtils; 022import org.apache.log4j.Logger; 023import org.kuali.rice.kew.api.KewApiConstants; 024import org.kuali.rice.kew.api.action.ActionType; 025import org.kuali.rice.kew.api.exception.WorkflowException; 026import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent; 027import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent; 028import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent; 029import org.kuali.rice.kew.framework.postprocessor.DeleteEvent; 030import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent; 031import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange; 032import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 033import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 034import org.kuali.rice.krad.UserSession; 035import org.kuali.rice.krad.document.Document; 036import org.kuali.rice.krad.service.DocumentService; 037import org.kuali.rice.krad.service.PostProcessorService; 038import org.kuali.rice.krad.util.GlobalVariables; 039import org.kuali.rice.krad.util.KRADConstants; 040import org.kuali.rice.krad.util.LegacyUtils; 041import org.springframework.dao.OptimisticLockingFailureException; 042import org.springframework.transaction.annotation.Transactional; 043 044/** 045 * This class is the postProcessor for the Kuali application, and it is responsible for plumbing events up to documents 046 * using the built into the document methods for handling route status and other routing changes that take place 047 * asyncronously and potentially on a different server. 048 * 049 * @author Kuali Rice Team (rice.collab@kuali.org) 050 */ 051@Transactional 052public class PostProcessorServiceImpl implements PostProcessorService { 053 054 private static final Logger LOG = Logger.getLogger(PostProcessorServiceImpl.class); 055 056 private DocumentService documentService; 057 058 @Override 059 public ProcessDocReport doRouteStatusChange(final DocumentRouteStatusChange statusChangeEvent) throws Exception { 060 return LegacyUtils.doInLegacyContext(statusChangeEvent.getDocumentId(), establishPostProcessorUserSession(), new Callable<ProcessDocReport>() { 061 @Override 062 public ProcessDocReport call() throws Exception { 063 064 try { 065 if (LOG.isInfoEnabled()) { 066 LOG.info(new StringBuilder("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(statusChangeEvent.getDocumentId()); 073 if (document == null) { 074 if (!KewApiConstants.ROUTE_HEADER_CANCEL_CD.equals(statusChangeEvent.getNewRouteStatus())) { 075 throw new RuntimeException("unable to load document " + statusChangeEvent.getDocumentId()); 076 } 077 } else { 078 document.doRouteStatusChange(statusChangeEvent); 079 // PLEASE READ BEFORE YOU MODIFY: 080 // we dont want to update the document on a Save, as this will cause an 081 // OptimisticLockException in many cases, because the DB versionNumber will be 082 // incremented one higher than the document in the browser, so when the user then 083 // hits Submit or Save again, the versionNumbers are out of synch, and the 084 // OptimisticLockException is thrown. This is not the optimal solution, and will 085 // be a problem anytime where the user can continue to edit the document after a 086 // workflow state change, without reloading the form. 087 if (!document.getDocumentHeader().getWorkflowDocument().isSaved()) { 088 document = documentService.updateDocument(document); 089// document = KradDataServiceLocator.getDataObjectService().save(document, PersistenceOption.FLUSH); 090 } 091 092 } 093 if (LOG.isInfoEnabled()) { 094 LOG.info(new StringBuilder("finished handling route status change from ").append( 095 statusChangeEvent.getOldRouteStatus()).append(" to ").append( 096 statusChangeEvent.getNewRouteStatus()).append(" for document ").append( 097 statusChangeEvent.getDocumentId())); 098 } 099 } 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}