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