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