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.kns.web.struts.action;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.apache.log4j.MDC;
21  import org.apache.ojb.broker.OptimisticLockException;
22  import org.apache.struts.Globals;
23  import org.apache.struts.action.Action;
24  import org.apache.struts.action.ActionForm;
25  import org.apache.struts.action.ActionForward;
26  import org.apache.struts.action.ActionMapping;
27  import org.apache.struts.action.InvalidCancelException;
28  import org.apache.struts.action.RequestProcessor;
29  import org.apache.struts.config.FormBeanConfig;
30  import org.apache.struts.config.ForwardConfig;
31  import org.apache.struts.util.RequestUtils;
32  import org.kuali.rice.core.api.CoreApiServiceLocator;
33  import org.kuali.rice.core.api.config.property.ConfigurationService;
34  import org.kuali.rice.core.api.util.RiceConstants;
35  import org.kuali.rice.core.api.util.RiceKeyConstants;
36  import org.kuali.rice.kns.exception.FileUploadLimitExceededException;
37  import org.kuali.rice.kns.service.KNSServiceLocator;
38  import org.kuali.rice.kns.service.SessionDocumentService;
39  import org.kuali.rice.kns.util.ErrorContainer;
40  import org.kuali.rice.kns.util.InfoContainer;
41  import org.kuali.rice.kns.util.KNSConstants;
42  import org.kuali.rice.kns.util.KNSGlobalVariables;
43  import org.kuali.rice.kns.util.WarningContainer;
44  import org.kuali.rice.kns.util.WebUtils;
45  import org.kuali.rice.kns.web.EditablePropertiesHistoryHolder;
46  import org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase;
47  import org.kuali.rice.kns.web.struts.form.KualiForm;
48  import org.kuali.rice.kns.web.struts.form.pojo.PojoForm;
49  import org.kuali.rice.krad.UserSession;
50  import org.kuali.rice.krad.document.Document;
51  import org.kuali.rice.krad.exception.ValidationException;
52  import org.kuali.rice.krad.util.GlobalVariables;
53  import org.kuali.rice.krad.util.KRADConstants;
54  import org.kuali.rice.krad.util.KRADUtils;
55  import org.kuali.rice.krad.util.LegacyUtils;
56  import org.kuali.rice.krad.util.MessageMap;
57  import org.springframework.transaction.PlatformTransactionManager;
58  import org.springframework.transaction.TransactionStatus;
59  import org.springframework.transaction.support.TransactionCallback;
60  import org.springframework.transaction.support.TransactionTemplate;
61  import org.springmodules.orm.ojb.OjbOperationException;
62  
63  import javax.servlet.ServletException;
64  import javax.servlet.http.HttpServletRequest;
65  import javax.servlet.http.HttpServletResponse;
66  import javax.servlet.http.HttpSession;
67  import java.io.IOException;
68  
69  /**
70   * This class handles setup of user session and restoring of action form.
71   * 
72   * @deprecated KNS Struts deprecated, use KRAD and the Spring MVC framework.
73   */
74  @Deprecated
75  public class KualiRequestProcessor extends RequestProcessor {
76  	
77  	private static final String MDC_DOC_ID = "docId";
78  	private static final String PREVIOUS_REQUEST_EDITABLE_PROPERTIES_GUID_PARAMETER_NAME = "actionEditablePropertiesGuid";
79  
80  	private static Logger LOG = Logger.getLogger(KualiRequestProcessor.class);
81  
82  	private SessionDocumentService sessionDocumentService;
83  	private PlatformTransactionManager transactionManager;
84  
85      @Override
86      public void process(final HttpServletRequest request,
87              final HttpServletResponse response) throws IOException, ServletException {
88          // indicates that we are running in legacy KNS context
89          LegacyUtils.beginLegacyContext();
90          try {
91              if (LOG.isInfoEnabled()) {
92                  LOG.info(new StringBuffer("Started processing request: '").append(request.getRequestURI()).append(
93                          "' w/ query string: '").append(request.getQueryString()).append("'"));
94              }
95  
96              try {
97                  strutsProcess(request, response);
98              } catch (FileUploadLimitExceededException e) {
99                  ActionForward actionForward = processException(request, response, e, e.getActionForm(),
100                         e.getActionMapping());
101                 processForwardConfig(request, response, actionForward);
102             } finally {
103                 KNSGlobalVariables.setKualiForm(null);
104             }
105 
106             try {
107                 ActionForm form = WebUtils.getKualiForm(request);
108 
109                 if (form != null && form instanceof KualiDocumentFormBase) {
110                     String docId = ((KualiDocumentFormBase) form).getDocId();
111                     if (docId != null) {
112                         MDC.put(MDC_DOC_ID, docId);
113                     }
114                 }
115 
116                 String refreshCaller = request.getParameter(KRADConstants.REFRESH_CALLER);
117                 if (form != null && KualiDocumentFormBase.class.isAssignableFrom(form.getClass()) && !KRADConstants
118                         .QUESTION_REFRESH.equalsIgnoreCase(refreshCaller)) {
119                     KualiDocumentFormBase docForm = (KualiDocumentFormBase) form;
120                     Document document = docForm.getDocument();
121                     String docFormKey = docForm.getFormKey();
122 
123                     UserSession userSession = (UserSession) request.getSession().getAttribute(
124                             KRADConstants.USER_SESSION_KEY);
125 
126                     if (WebUtils.isDocumentSession(document, docForm)) {
127                         getSessionDocumentService().setDocumentForm(docForm, userSession, request.getRemoteAddr());
128                     }
129 
130                     Boolean exitingDocument = (Boolean) request.getAttribute(KRADConstants.EXITING_DOCUMENT);
131 
132                     if (exitingDocument != null && exitingDocument.booleanValue()) {
133                         // remove KualiDocumentFormBase object from session and
134                         // table.
135                         getSessionDocumentService().purgeDocumentForm(docForm.getDocument().getDocumentNumber(),
136                                 docFormKey, userSession, request.getRemoteAddr());
137                     }
138                 }
139 
140                 if (LOG.isInfoEnabled()) {
141                     LOG.info(new StringBuffer("Finished processing request: '").append(request.getRequestURI()).append(
142                             "' w/ query string: '").append(request.getQueryString()).append("'"));
143                 }
144 
145             } finally {
146                 // MDC docId key is set above, and also during super.process() in the call to processActionForm
147                 MDC.remove(MDC_DOC_ID);
148             }
149         } finally {
150             LegacyUtils.endLegacyContext();
151         }
152     }
153 
154     @Override
155 	protected boolean processPreprocess(HttpServletRequest request, HttpServletResponse response) {
156         final UserSession session = KRADUtils.getUserSessionFromRequest(request);
157         
158         if (session == null) {
159         	throw new IllegalStateException("the user session has not been established");
160         }
161     	GlobalVariables.setUserSession(session);
162     	KNSGlobalVariables.clear();
163 		return true;
164 	}
165 	
166 	/**
167      * <p>ProcessDefinition an <code>HttpServletRequest</code> and create the
168      * corresponding <code>HttpServletResponse</code> or dispatch
169      * to another resource.</p>
170      *
171      * @param request The servlet request we are processing
172      * @param response The servlet response we are creating
173      *
174      * @exception IOException if an input/output error occurs
175      * @exception ServletException if a processing exception occurs
176      */
177     public void strutsProcess(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
178 
179         // Wrap multipart requests with a special wrapper
180         request = processMultipart(request);
181 
182         // Identify the path component we will use to select a mapping
183         String path = processPath(request, response);
184         if (path == null) {
185             return;
186         }
187         
188         if (log.isDebugEnabled()) {
189             log.debug("Processing a '" + request.getMethod() +
190                       "' for path '" + path + "'");
191         }
192 
193         // Select a Locale for the current user if requested
194         processLocale(request, response);
195 
196         // Set the content type and no-caching headers if requested
197         processContent(request, response);
198         processNoCache(request, response);
199 
200         // General purpose preprocessing hook
201         if (!processPreprocess(request, response)) {
202             return;
203         }
204         
205         this.processCachedMessages(request, response);
206 
207         // Identify the mapping for this request
208         ActionMapping mapping = processMapping(request, response, path);
209         if (mapping == null) {
210             return;
211         }
212 
213         // Check for any role required to perform this action
214         if (!processRoles(request, response, mapping)) {
215             return;
216         }
217 
218         processFormActionAndForward(request, response, mapping);
219 
220     }
221 
222     public void processFormActionAndForward(final HttpServletRequest request, final HttpServletResponse response, final ActionMapping mapping) throws ServletException, IOException {
223     	ActionForm form = processActionForm(request, response, mapping);
224         processPopulate(request, response, form, mapping);
225 
226         // Create or acquire the Action instance to process this request
227 		Action action = processActionCreate(request, response, mapping);
228 
229         if (action != null) {
230             // Call the Action instance itself
231 		    ActionForward forward = processActionPerform(request, response, action, form, mapping);
232 
233             if (forward != null) {
234                 if (forward.getRedirect() && forward.getName()!= null && forward.getName().equals(KRADConstants.KRAD_INITIATED_DOCUMENT_VIEW_NAME)) {
235                     LOG.info("Attempt to open a document with a status of \"Initiated\" detected");
236                     return;
237                 }
238                 // ProcessDefinition the returned ActionForward instance
239 			    processForwardConfig(request, response, forward);
240             }
241         }
242     }
243 
244 
245 	/**
246 	 * This method gets the document number from the request.  The request should have been processed already 
247 	 * before this is called if it is multipart.  
248 	 * 
249 	 * @param request
250 	 * @return the document number, or null if one can't be found in the request.
251 	 */
252 	private String getDocumentNumber(HttpServletRequest request) {
253 		String documentNumber = request.getParameter(KRADConstants.DOCUMENT_DOCUMENT_NUMBER);
254 
255 		// from lookup pages.
256 		if (documentNumber == null) {
257 			documentNumber = request.getParameter(KRADConstants.DOC_NUM);
258 		}
259 		
260 		if (documentNumber == null) {
261 			documentNumber = request.getParameter("documentId");
262 		}
263 		
264 		return documentNumber;
265 	}
266 
267 	/**
268 	 * Hooks into populate process to call form populate method if form is an
269 	 * instanceof PojoForm.
270 	 */
271 	@Override
272 	protected void processPopulate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws ServletException {
273 		if (form instanceof KualiForm) {
274 			// Add the ActionForm to GlobalVariables
275 			// This will allow developers to retrieve both the Document and any
276 			// request parameters that are not
277 			// part of the Form and make them available in ValueFinder classes
278 			// and other places where they are needed.
279 			KNSGlobalVariables.setKualiForm((KualiForm) form);
280 		}
281 
282 		// if not PojoForm, call struts populate
283 		if (!(form instanceof PojoForm)) {
284 			super.processPopulate(request, response, form, mapping);
285 			return;
286 		}
287 		
288 		final String previousRequestGuid = request.getParameter(KualiRequestProcessor.PREVIOUS_REQUEST_EDITABLE_PROPERTIES_GUID_PARAMETER_NAME);
289 
290 		((PojoForm)form).clearEditablePropertyInformation();
291 		((PojoForm)form).registerStrutsActionMappingScope(mapping.getScope());
292 		
293 		String multipart = mapping.getMultipartClass();
294 		if (multipart != null) {
295 			request.setAttribute(Globals.MULTIPART_KEY, multipart);
296 		}
297 
298 		form.setServlet(this.servlet);
299 		form.reset(mapping, request);
300 
301 		((PojoForm)form).setPopulateEditablePropertiesGuid(previousRequestGuid);
302 		// call populate on ActionForm
303 		((PojoForm) form).populate(request);
304 		request.setAttribute("UnconvertedValues", ((PojoForm) form).getUnconvertedValues().keySet());
305 		request.setAttribute("UnconvertedHash", ((PojoForm) form).getUnconvertedValues());
306 	}
307 
308 	/**
309 	 * Hooks into validate to catch any errors from the populate, and translate
310 	 * the ErrorMap to ActionMessages.
311 	 */
312 	@Override
313 	protected boolean processValidate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws IOException, ServletException, InvalidCancelException {
314 
315 		// skip form validate if we had errors from populate
316 		if (GlobalVariables.getMessageMap().hasNoErrors()) {
317 			if (form == null) {
318 				return (true);
319 			}
320 			// Was this request cancelled?
321 			if (request.getAttribute(Globals.CANCEL_KEY) != null) {
322 				if (LOG.isDebugEnabled()) {
323 					LOG.debug(" Cancelled transaction, skipping validation");
324 				}
325 				return (true);
326 			}
327 
328 			// Has validation been turned off for this mapping?
329 			if (!mapping.getValidate()) {
330 				return (true);
331 			}
332 
333 			// call super to call forms validate
334 			super.processValidate(request, response, form, mapping);
335 		}
336 
337 		publishMessages(request);
338 		if (!GlobalVariables.getMessageMap().hasNoErrors()) {
339 			// Special handling for multipart request
340 			if (form.getMultipartRequestHandler() != null) {
341 				if (LOG.isDebugEnabled()) {
342 					LOG.debug("  Rolling back multipart request");
343 				}
344 				form.getMultipartRequestHandler().rollback();
345 			}
346 
347 			// Fix state that could be incorrect because of validation failure
348 			if (form instanceof PojoForm) {
349 				((PojoForm) form).processValidationFail();
350 			}
351 
352 			// Was an input path (or forward) specified for this mapping?
353 			String input = mapping.getInput();
354 			if (input == null) {
355 				if (LOG.isDebugEnabled()) {
356 					LOG.debug("  Validation failed but no input form available");
357 				}
358 				response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, getInternal().getMessage("noInput", mapping.getPath()));
359 				return (false);
360 			}
361 
362 			if (moduleConfig.getControllerConfig().getInputForward()) {
363 				ForwardConfig forward = mapping.findForward(input);
364 				processForwardConfig(request, response, forward);
365 			} else {
366 				internalModuleRelativeForward(input, request, response);
367 			}
368 
369 			return (false);
370 		}
371 		return true;
372 	}
373 
374 	/**
375 	 * Checks for return from a lookup or question, and restores the action form
376 	 * stored under the request parameter docFormKey.
377 	 */
378 	@Override
379 	protected ActionForm processActionForm(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) {
380 		
381 		String documentNumber = getDocumentNumber(request);
382 		if (documentNumber != null) { MDC.put(MDC_DOC_ID, documentNumber); }
383 		
384 		UserSession userSession = (UserSession) request.getSession().getAttribute(KRADConstants.USER_SESSION_KEY);
385 
386 		String docFormKey = request.getParameter(KRADConstants.DOC_FORM_KEY);
387 		String methodToCall = request.getParameter(KRADConstants.DISPATCH_REQUEST_PARAMETER);
388 		String refreshCaller = request.getParameter(KRADConstants.REFRESH_CALLER);
389 //		String searchListRequestKey = request.getParameter(KRADConstants.SEARCH_LIST_REQUEST_KEY);
390 		String documentWebScope = request.getParameter(KRADConstants.DOCUMENT_WEB_SCOPE);
391 
392 		if (mapping.getPath().startsWith(KRADConstants.REFRESH_MAPPING_PREFIX) || KRADConstants.RETURN_METHOD_TO_CALL.equalsIgnoreCase(methodToCall) ||
393 				KRADConstants.QUESTION_REFRESH.equalsIgnoreCase(refreshCaller) || KRADConstants.TEXT_AREA_REFRESH.equalsIgnoreCase(refreshCaller) || KRADConstants
394                 .SESSION_SCOPE.equalsIgnoreCase(documentWebScope)) {
395 			ActionForm form = null;
396 			// check for search result storage and clear
397 			GlobalVariables.getUserSession().removeObjectsByPrefix(KRADConstants.SEARCH_LIST_KEY_PREFIX);
398 
399 			// We put different type of forms such as document form, lookup form
400 			// in session but we only store document form in
401 			// database.
402 			if (userSession.retrieveObject(docFormKey) != null) {
403 				LOG.debug("getDecomentForm KualiDocumentFormBase from session");
404 				form = (ActionForm) userSession.retrieveObject(docFormKey);
405 			} else if (StringUtils.isNotBlank(documentNumber)) {
406 				form = getSessionDocumentService().getDocumentForm(documentNumber, docFormKey, userSession, request.getRemoteAddr());
407 			}
408 			request.setAttribute(mapping.getAttribute(), form);
409 			if (!KRADConstants.SESSION_SCOPE.equalsIgnoreCase(documentWebScope)) {
410 				userSession.removeObject(docFormKey);
411 			}
412 			// we should check whether this is a multipart request because we
413 			// could have had a combination of query parameters and a multipart
414 			// request
415 			String contentType = request.getContentType();
416 			String method = request.getMethod();
417 			if (("POST".equalsIgnoreCase(method) && contentType != null && contentType.startsWith("multipart/form-data"))) {
418 				// this method parses the multipart request and adds new
419 				// non-file parameters into the request
420 				WebUtils.getMultipartParameters(request, null, form, mapping);
421 			}
422 			// The form can be null if the document is not a session document
423 			if (form != null) {
424 				return form;
425 			}
426 		}
427 
428 		// Rice has the ability to limit file upload sizes on a per-form basis,
429 		// so the max upload sizes may be accessed by calling methods on
430 		// PojoFormBase.
431 		// This requires that we are able know the file upload size limit (i.e.
432 		// retrieve a form instance) before we parse a mulitpart request.
433 		ActionForm form = super.processActionForm(request, response, mapping);
434 
435 		// for sessiondocument with multipart request
436 		String contentType = request.getContentType();
437 		String method = request.getMethod();
438 
439 		if ("GET".equalsIgnoreCase(method) && StringUtils.isNotBlank(methodToCall) && form instanceof PojoForm &&
440 				((PojoForm) form).getMethodToCallsToBypassSessionRetrievalForGETRequests().contains(methodToCall)) {
441 			return createNewActionForm(mapping, request);
442 		}
443 		
444 		// if we have a multipart request, parse it and return the stored form
445 		// from session if the doc form key is not blank. If it is blank, then
446 		// we just return the form
447 		// generated from the superclass processActionForm method. Either way,
448 		// we need to parse the mulitpart request now so that we may determine
449 		// what the value of the doc form key is.
450 		// This is generally against the contract of processActionForm, because
451 		// processPopulate should be responsible for parsing the mulitpart
452 		// request, but we need to parse it now
453 		// to determine the doc form key value.
454 		if (("POST".equalsIgnoreCase(method) && contentType != null && contentType.startsWith("multipart/form-data"))) {
455 			WebUtils.getMultipartParameters(request, null, form, mapping);
456 			docFormKey = request.getParameter(KRADConstants.DOC_FORM_KEY);
457 			documentWebScope = request.getParameter(KRADConstants.DOCUMENT_WEB_SCOPE);
458 
459 			documentNumber = getDocumentNumber(request);
460 
461 			if (KRADConstants.SESSION_SCOPE.equalsIgnoreCase(documentWebScope) ||
462 					(form instanceof KualiDocumentFormBase && WebUtils
463                             .isDocumentSession(((KualiDocumentFormBase) form).getDocument(),
464                                     (KualiDocumentFormBase) form))) {
465 
466 				Object userSessionObject = userSession.retrieveObject(docFormKey);
467 				if ( userSessionObject != null &&  userSessionObject instanceof ActionForm ) {
468 					LOG.debug("getDocumentForm KualiDocumentFormBase from session");
469 					form = (ActionForm) userSessionObject;
470 				} else {
471 					ActionForm tempForm = getSessionDocumentService().getDocumentForm(documentNumber, docFormKey, userSession, request.getRemoteAddr());
472 					if ( tempForm != null ) {
473 						form = tempForm;
474 					}
475 				}
476 
477 				request.setAttribute(mapping.getAttribute(), form);
478 				if (form != null) {
479 					return form;
480 				}
481 			}
482 		}
483 		return form;
484 	}
485 
486 	/**
487 	 * Hook into action perform to handle errors in the error map and catch
488 	 * exceptions.
489 	 * 
490 	 * <p>
491 	 * A transaction is started prior to the execution of the action. This
492 	 * allows for the action code to execute efficiently without the need for
493 	 * using PROPAGATION_SUPPORTS in the transaction definitions. The
494 	 * PROPAGATION_SUPPORTS propagation type does not work well with JTA.
495 	 */
496 	@Override
497 	protected ActionForward processActionPerform(final HttpServletRequest request, final HttpServletResponse response, final Action action, final ActionForm form, final ActionMapping mapping) throws IOException, ServletException {
498 		try {
499             TransactionTemplate template = new TransactionTemplate(getTransactionManager());
500 			ActionForward forward = null;
501 			try {
502 				forward = (ActionForward) template.execute(new TransactionCallback() {
503 					public Object doInTransaction(TransactionStatus status) {
504 						ActionForward actionForward = null;
505 						try {
506 							actionForward = action.execute(mapping, form, request, response);
507 						} catch (Exception e) {
508                             if (e.getMessage()!= null && e.getMessage().equals(KRADConstants.KRAD_INITIATED_DOCUMENT_VIEW_NAME)) {
509                                 ConfigurationService kualiConfigurationService = CoreApiServiceLocator
510                                         .getKualiConfigurationService();
511                                 StringBuffer sb = new StringBuffer();
512                                 sb.append(kualiConfigurationService.getPropertyValueAsString(KRADConstants.KRAD_URL_KEY));
513                                 sb.append(kualiConfigurationService.getPropertyValueAsString(KRADConstants.KRAD_INITIATED_DOCUMENT_URL_KEY));
514                                 return new ActionForward(KRADConstants.KRAD_INITIATED_DOCUMENT_VIEW_NAME, sb.toString() ,true);
515                             }
516 							// the doInTransaction method has no means for
517 							// throwing exceptions, so we will wrap the
518 							// exception in
519 							// a RuntimeException and re-throw. The one caveat
520 							// here is that this will always result in
521 							// the
522 							// transaction being rolled back (since
523 							// WrappedRuntimeException is a runtime exception).
524 							throw new WrappedRuntimeException(e);
525 						}
526 						if (status.isRollbackOnly()) {
527 							// this means that the struts action execution
528 							// caused the transaction to rollback, we want to
529 							// go ahead
530 							// and trigger the rollback by throwing an exception
531 							// here but then return the action forward
532 							// from this method
533 							throw new WrappedActionForwardRuntimeException(actionForward);
534 						}
535 						return actionForward;
536 					}
537 				});
538 			} catch (WrappedActionForwardRuntimeException e) {
539 				forward = e.getActionForward();
540 			}
541 
542 			publishMessages(request);
543 			saveMessages(request);
544 			saveAuditErrors(request);
545 
546 			if (form instanceof PojoForm) {
547 				if (((PojoForm)form).getEditableProperties() == null
548 						|| ((PojoForm)form).getEditableProperties().isEmpty()) {
549 					EditablePropertiesHistoryHolder holder = (EditablePropertiesHistoryHolder) GlobalVariables.getUserSession().getObjectMap().get(
550                             KRADConstants.EDITABLE_PROPERTIES_HISTORY_HOLDER_ATTR_NAME);
551 				    if (holder == null) {
552 				    	holder = new EditablePropertiesHistoryHolder();
553 				    }
554 
555 					final String guid = holder.addEditablePropertiesToHistory(((PojoForm)form).getEditableProperties());
556 				    ((PojoForm)form).setActionEditablePropertiesGuid(guid);
557 				    GlobalVariables.getUserSession().addObject(KRADConstants.EDITABLE_PROPERTIES_HISTORY_HOLDER_ATTR_NAME, holder);
558 				}
559 			}
560 
561 			return forward;
562 
563 		} catch (Exception e) {
564 			if (e instanceof WrappedRuntimeException) {
565 				e = (Exception) e.getCause();
566 			}
567 			if (e instanceof ValidationException) {
568 				// add a generic error message if there are none
569 				if (GlobalVariables.getMessageMap().hasNoErrors()) {
570 
571 					GlobalVariables.getMessageMap().putError(KRADConstants.GLOBAL_ERRORS, RiceKeyConstants.ERROR_CUSTOM, e.getMessage());
572 				}
573 
574 				if (form instanceof PojoForm) {
575 					if (((PojoForm)form).getEditableProperties() == null
576 							|| ((PojoForm)form).getEditableProperties().isEmpty()) {
577 					    EditablePropertiesHistoryHolder holder = (EditablePropertiesHistoryHolder) GlobalVariables.getUserSession().getObjectMap().get(
578                                 KRADConstants.EDITABLE_PROPERTIES_HISTORY_HOLDER_ATTR_NAME);
579 				        if (holder == null) {
580 				    	    holder = new EditablePropertiesHistoryHolder();
581 				        }
582 
583 					    final String guid = holder.addEditablePropertiesToHistory(((PojoForm)form).getEditableProperties());
584 				        ((PojoForm)form).setActionEditablePropertiesGuid(guid);
585 				        GlobalVariables.getUserSession().addObject(KRADConstants.EDITABLE_PROPERTIES_HISTORY_HOLDER_ATTR_NAME, holder);
586 					}
587 				}			
588 				// display error messages and return to originating page
589 				publishMessages(request);
590 				return mapping.findForward(RiceConstants.MAPPING_BASIC);
591 			}
592 
593 			publishMessages(request);
594 
595 			return (processException(request, response, e, form, mapping));
596 		}
597 	}
598 
599     private static class WrappedActionForwardRuntimeException extends RuntimeException {
600 		private ActionForward actionForward;
601 
602 		public WrappedActionForwardRuntimeException(ActionForward actionForward) {
603 			this.actionForward = actionForward;
604 		}
605 
606 		public ActionForward getActionForward() {
607 			return actionForward;
608 		}
609 	}
610 
611 	/**
612 	 * Adds more detailed logging for unhandled exceptions
613 	 * 
614 	 * @see org.apache.struts.action.RequestProcessor#processException(HttpServletRequest,
615 	 *      HttpServletResponse, Exception, ActionForm, ActionMapping)
616 	 */
617 	@Override
618 	protected ActionForward processException(HttpServletRequest request, HttpServletResponse response, Exception exception, ActionForm form, ActionMapping mapping) throws IOException, ServletException {
619 		ActionForward actionForward = null;
620 
621 		try {
622 			actionForward = super.processException(request, response, exception, form, mapping);
623 		} catch (IOException e) {
624 			logException(e);
625 			throw e;
626 		} catch (ServletException e) {
627 			// special case, to make OptimisticLockExceptions easier to read
628 			Throwable rootCause = e.getRootCause();
629 			if (rootCause instanceof OjbOperationException) {
630 				OjbOperationException ooe = (OjbOperationException) rootCause;
631 
632 				Throwable subcause = ooe.getCause();
633 				if (subcause instanceof OptimisticLockException) {
634 					OptimisticLockException ole = (OptimisticLockException) subcause;
635 
636 					StringBuffer message = new StringBuffer(e.getMessage());
637 
638 					Object sourceObject = ole.getSourceObject();
639 					if (sourceObject != null) {
640 						message.append(" (sourceObject is ");
641 						message.append(sourceObject.getClass().getName());
642 						message.append(")");
643 					}
644 
645 					e = new ServletException(message.toString(), rootCause);
646 				}
647 			}
648 
649 			logException(e);
650 			throw e;
651 		}
652 		return actionForward;
653 	}
654 
655 	private void logException(Exception e) {
656 		LOG.error("unhandled exception thrown by KualiRequestProcessor.processActionPerform", e);
657 	}
658 
659 	/**
660 	 * Checks for errors in the error map and transforms them to struts action
661 	 * messages then stores in the request.
662 	 */
663 	private void publishMessages(HttpServletRequest request) {
664 		MessageMap errorMap = GlobalVariables.getMessageMap();
665 		if (!errorMap.hasNoErrors()) {
666 			ErrorContainer errorContainer = new ErrorContainer(errorMap);
667 
668 			request.setAttribute("ErrorContainer", errorContainer);
669 			request.setAttribute(Globals.ERROR_KEY, errorContainer.getRequestErrors());
670 			request.setAttribute("ErrorPropertyList", errorContainer.getErrorPropertyList());
671 		}
672 		
673 		if (errorMap.hasWarnings()) {
674 			WarningContainer warningsContainer = new WarningContainer(errorMap);
675 			
676 			request.setAttribute("WarningContainer", warningsContainer);
677 			request.setAttribute("WarningActionMessages", warningsContainer.getRequestMessages());
678 			request.setAttribute("WarningPropertyList", warningsContainer.getMessagePropertyList());
679 		}
680 		
681 		if (errorMap.hasInfo()) {
682 			InfoContainer infoContainer = new InfoContainer(errorMap);
683 			
684 			request.setAttribute("InfoContainer", infoContainer);
685 			request.setAttribute("InfoActionMessages", infoContainer.getRequestMessages());
686 			request.setAttribute("InfoPropertyList", infoContainer.getMessagePropertyList());
687 		}
688 	}
689 
690 	/**
691 	 * Checks for messages in GlobalVariables and places list in request
692 	 * attribute.
693 	 */
694 	private void saveMessages(HttpServletRequest request) {
695 		if (!KNSGlobalVariables.getMessageList().isEmpty()) {
696 			request.setAttribute(KRADConstants.GLOBAL_MESSAGES, KNSGlobalVariables.getMessageList().toActionMessages());
697 		}
698 	}
699 
700 	/**
701 	 * Checks for messages in GlobalVariables and places list in request
702 	 * attribute.
703 	 */
704 	private void saveAuditErrors(HttpServletRequest request) {
705 		if (!KNSGlobalVariables.getAuditErrorMap().isEmpty()) {
706 			request.setAttribute(KNSConstants.AUDIT_ERRORS, KNSGlobalVariables.getAuditErrorMap());
707 		}
708 	}
709 
710 	/**
711 	 * A simple exception that allows us to wrap an exception that is thrown out
712 	 * of a transaction template.
713 	 */
714 	@SuppressWarnings("serial")
715 	private static class WrappedRuntimeException extends RuntimeException {
716 		public WrappedRuntimeException(Exception e) {
717 			super(e);
718 		}
719 	}
720 
721 	/**
722 	 * @return the sessionDocumentService
723 	 */
724 	public SessionDocumentService getSessionDocumentService() {
725 		if ( sessionDocumentService == null ) {
726 			sessionDocumentService = KNSServiceLocator.getSessionDocumentService();
727 		}
728 		return this.sessionDocumentService;
729 	}
730 
731 	/**
732 	 * @return the transactionManager
733 	 */
734 	public PlatformTransactionManager getTransactionManager() {
735 		if ( transactionManager == null ) {
736 			transactionManager = KNSServiceLocator.getTransactionManager();
737 		}
738 		return this.transactionManager;
739 	}
740 	
741 	private ActionForm createNewActionForm(ActionMapping mapping, HttpServletRequest request) {
742         String name = mapping.getName();
743         FormBeanConfig config = moduleConfig.findFormBeanConfig(name);
744         if (config == null) {
745             log.warn("No FormBeanConfig found under '" + name + "'");
746             return (null);
747         }
748         ActionForm instance = RequestUtils.createActionForm(config, servlet);
749         if ("request".equals(mapping.getScope())) {
750             request.setAttribute(mapping.getAttribute(), instance);
751         } else {
752             HttpSession session = request.getSession();
753             session.setAttribute(mapping.getAttribute(), instance);
754         }
755         return instance;
756 	}
757 }