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