View Javadoc

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