View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kns.web.struts.action;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.apache.log4j.MDC;
21  import org.apache.ojb.broker.OptimisticLockException;
22  import org.apache.struts.Globals;
23  import org.apache.struts.action.Action;
24  import org.apache.struts.action.ActionForm;
25  import org.apache.struts.action.ActionForward;
26  import org.apache.struts.action.ActionMapping;
27  import org.apache.struts.action.InvalidCancelException;
28  import org.apache.struts.action.RequestProcessor;
29  import org.apache.struts.config.FormBeanConfig;
30  import org.apache.struts.config.ForwardConfig;
31  import org.apache.struts.util.RequestUtils;
32  import org.kuali.rice.core.api.CoreApiServiceLocator;
33  import org.kuali.rice.core.api.config.property.ConfigurationService;
34  import org.kuali.rice.core.api.util.RiceConstants;
35  import org.kuali.rice.core.api.util.RiceKeyConstants;
36  import org.kuali.rice.kns.exception.FileUploadLimitExceededException;
37  import org.kuali.rice.kns.service.KNSServiceLocator;
38  import org.kuali.rice.kns.service.SessionDocumentService;
39  import org.kuali.rice.kns.util.ErrorContainer;
40  import org.kuali.rice.kns.util.InfoContainer;
41  import org.kuali.rice.kns.util.KNSConstants;
42  import org.kuali.rice.kns.util.KNSGlobalVariables;
43  import org.kuali.rice.kns.util.WarningContainer;
44  import org.kuali.rice.kns.util.WebUtils;
45  import org.kuali.rice.kns.web.EditablePropertiesHistoryHolder;
46  import org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase;
47  import org.kuali.rice.kns.web.struts.form.KualiForm;
48  import org.kuali.rice.kns.web.struts.form.pojo.PojoForm;
49  import org.kuali.rice.krad.UserSession;
50  import org.kuali.rice.krad.document.Document;
51  import org.kuali.rice.krad.exception.ValidationException;
52  import org.kuali.rice.krad.util.GlobalVariables;
53  import org.kuali.rice.krad.util.KRADConstants;
54  import org.kuali.rice.krad.util.KRADUtils;
55  import org.kuali.rice.krad.util.LegacyUtils;
56  import org.kuali.rice.krad.util.MessageMap;
57  import org.springframework.transaction.PlatformTransactionManager;
58  import org.springframework.transaction.TransactionStatus;
59  import org.springframework.transaction.support.TransactionCallback;
60  import org.springframework.transaction.support.TransactionTemplate;
61  import org.springmodules.orm.ojb.OjbOperationException;
62  
63  import javax.servlet.ServletException;
64  import javax.servlet.http.HttpServletRequest;
65  import javax.servlet.http.HttpServletResponse;
66  import javax.servlet.http.HttpSession;
67  import java.io.IOException;
68  
69  /**
70   * This class handles setup of user session and restoring of action form.
71   * 
72   * 
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(final HttpServletRequest request,
86              final HttpServletResponse response) throws IOException, ServletException {
87          // indicates that we are running in legacy KNS context
88          LegacyUtils.beginLegacyContext();
89          try {
90              if (LOG.isInfoEnabled()) {
91                  LOG.info(new StringBuffer("Started processing request: '").append(request.getRequestURI()).append(
92                          "' w/ query string: '").append(request.getQueryString()).append("'"));
93              }
94  
95              try {
96                  strutsProcess(request, response);
97              } catch (FileUploadLimitExceededException e) {
98                  ActionForward actionForward = processException(request, response, e, e.getActionForm(),
99                          e.getActionMapping());
100                 processForwardConfig(request, response, actionForward);
101             } finally {
102                 KNSGlobalVariables.setKualiForm(null);
103             }
104 
105             try {
106                 ActionForm form = WebUtils.getKualiForm(request);
107 
108                 if (form != null && form instanceof KualiDocumentFormBase) {
109                     String docId = ((KualiDocumentFormBase) form).getDocId();
110                     if (docId != null) {
111                         MDC.put(MDC_DOC_ID, docId);
112                     }
113                 }
114 
115                 String refreshCaller = request.getParameter(KRADConstants.REFRESH_CALLER);
116                 if (form != null && KualiDocumentFormBase.class.isAssignableFrom(form.getClass()) && !KRADConstants
117                         .QUESTION_REFRESH.equalsIgnoreCase(refreshCaller)) {
118                     KualiDocumentFormBase docForm = (KualiDocumentFormBase) form;
119                     Document document = docForm.getDocument();
120                     String docFormKey = docForm.getFormKey();
121 
122                     UserSession userSession = (UserSession) request.getSession().getAttribute(
123                             KRADConstants.USER_SESSION_KEY);
124 
125                     if (WebUtils.isDocumentSession(document, docForm)) {
126                         getSessionDocumentService().setDocumentForm(docForm, userSession, request.getRemoteAddr());
127                     }
128 
129                     Boolean exitingDocument = (Boolean) request.getAttribute(KRADConstants.EXITING_DOCUMENT);
130 
131                     if (exitingDocument != null && exitingDocument.booleanValue()) {
132                         // remove KualiDocumentFormBase object from session and
133                         // table.
134                         getSessionDocumentService().purgeDocumentForm(docForm.getDocument().getDocumentNumber(),
135                                 docFormKey, userSession, request.getRemoteAddr());
136                     }
137                 }
138 
139                 if (LOG.isInfoEnabled()) {
140                     LOG.info(new StringBuffer("Finished processing request: '").append(request.getRequestURI()).append(
141                             "' w/ query string: '").append(request.getQueryString()).append("'"));
142                 }
143 
144             } finally {
145                 // MDC docId key is set above, and also during super.process() in the call to processActionForm
146                 MDC.remove(MDC_DOC_ID);
147             }
148         } finally {
149             LegacyUtils.endLegacyContext();
150         }
151     }
152 
153     @Override
154 	protected boolean processPreprocess(HttpServletRequest request, HttpServletResponse response) {
155         final UserSession session = KRADUtils.getUserSessionFromRequest(request);
156         
157         if (session == null) {
158         	throw new IllegalStateException("the user session has not been established");
159         }
160     	GlobalVariables.setUserSession(session);
161     	KNSGlobalVariables.clear();
162 		return true;
163 	}
164 	
165 	/**
166      * <p>ProcessDefinition an <code>HttpServletRequest</code> and create the
167      * corresponding <code>HttpServletResponse</code> or dispatch
168      * to another resource.</p>
169      *
170      * @param request The servlet request we are processing
171      * @param response The servlet response we are creating
172      *
173      * @exception IOException if an input/output error occurs
174      * @exception ServletException if a processing exception occurs
175      */
176     public void strutsProcess(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
177 
178         // Wrap multipart requests with a special wrapper
179         request = processMultipart(request);
180 
181         // Identify the path component we will use to select a mapping
182         String path = processPath(request, response);
183         if (path == null) {
184             return;
185         }
186         
187         if (log.isDebugEnabled()) {
188             log.debug("Processing a '" + request.getMethod() +
189                       "' for path '" + path + "'");
190         }
191 
192         // Select a Locale for the current user if requested
193         processLocale(request, response);
194 
195         // Set the content type and no-caching headers if requested
196         processContent(request, response);
197         processNoCache(request, response);
198 
199         // General purpose preprocessing hook
200         if (!processPreprocess(request, response)) {
201             return;
202         }
203         
204         this.processCachedMessages(request, response);
205 
206         // Identify the mapping for this request
207         ActionMapping mapping = processMapping(request, response, path);
208         if (mapping == null) {
209             return;
210         }
211 
212         // Check for any role required to perform this action
213         if (!processRoles(request, response, mapping)) {
214             return;
215         }
216 
217         processFormActionAndForward(request, response, mapping);
218 
219     }
220 
221     public void processFormActionAndForward(final HttpServletRequest request, final HttpServletResponse response, final ActionMapping mapping) throws ServletException, IOException {
222     	ActionForm form = processActionForm(request, response, mapping);
223         processPopulate(request, response, form, mapping);
224 
225         // Create or acquire the Action instance to process this request
226 		Action action = processActionCreate(request, response, mapping);
227 
228         if (action != null) {
229             // Call the Action instance itself
230 		    ActionForward forward = processActionPerform(request, response, action, form, mapping);
231 
232             if (forward != null) {
233                 if (forward.getRedirect() && forward.getName()!= null && forward.getName().equals(KRADConstants.KRAD_INITIATED_DOCUMENT_VIEW_NAME)) {
234                     LOG.info("Attempt to open a document with a status of \"Initiated\" detected");
235                     return;
236                 }
237                 // ProcessDefinition the returned ActionForward instance
238 			    processForwardConfig(request, response, forward);
239             }
240         }
241     }
242 
243 
244 	/**
245 	 * This method gets the document number from the request.  The request should have been processed already 
246 	 * before this is called if it is multipart.  
247 	 * 
248 	 * @param request
249 	 * @return the document number, or null if one can't be found in the request.
250 	 */
251 	private String getDocumentNumber(HttpServletRequest request) {
252 		String documentNumber = request.getParameter(KRADConstants.DOCUMENT_DOCUMENT_NUMBER);
253 
254 		// from lookup pages.
255 		if (documentNumber == null) {
256 			documentNumber = request.getParameter(KRADConstants.DOC_NUM);
257 		}
258 		
259 		if (documentNumber == null) {
260 			documentNumber = request.getParameter("documentId");
261 		}
262 		
263 		return documentNumber;
264 	}
265 
266 	/**
267 	 * Hooks into populate process to call form populate method if form is an
268 	 * instanceof PojoForm.
269 	 */
270 	@Override
271 	protected void processPopulate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws ServletException {
272 		if (form instanceof KualiForm) {
273 			// Add the ActionForm to GlobalVariables
274 			// This will allow developers to retrieve both the Document and any
275 			// request parameters that are not
276 			// part of the Form and make them available in ValueFinder classes
277 			// and other places where they are needed.
278 			KNSGlobalVariables.setKualiForm((KualiForm) form);
279 		}
280 
281 		// if not PojoForm, call struts populate
282 		if (!(form instanceof PojoForm)) {
283 			super.processPopulate(request, response, form, mapping);
284 			return;
285 		}
286 		
287 		final String previousRequestGuid = request.getParameter(KualiRequestProcessor.PREVIOUS_REQUEST_EDITABLE_PROPERTIES_GUID_PARAMETER_NAME);
288 
289 		((PojoForm)form).clearEditablePropertyInformation();
290 		((PojoForm)form).registerStrutsActionMappingScope(mapping.getScope());
291 		
292 		String multipart = mapping.getMultipartClass();
293 		if (multipart != null) {
294 			request.setAttribute(Globals.MULTIPART_KEY, multipart);
295 		}
296 
297 		form.setServlet(this.servlet);
298 		form.reset(mapping, request);
299 
300 		((PojoForm)form).setPopulateEditablePropertiesGuid(previousRequestGuid);
301 		// call populate on ActionForm
302 		((PojoForm) form).populate(request);
303 		request.setAttribute("UnconvertedValues", ((PojoForm) form).getUnconvertedValues().keySet());
304 		request.setAttribute("UnconvertedHash", ((PojoForm) form).getUnconvertedValues());
305 	}
306 
307 	/**
308 	 * Hooks into validate to catch any errors from the populate, and translate
309 	 * the ErrorMap to ActionMessages.
310 	 */
311 	@Override
312 	protected boolean processValidate(HttpServletRequest request, HttpServletResponse response, ActionForm form, ActionMapping mapping) throws IOException, ServletException, InvalidCancelException {
313 
314 		// skip form validate if we had errors from populate
315 		if (GlobalVariables.getMessageMap().hasNoErrors()) {
316 			if (form == null) {
317 				return (true);
318 			}
319 			// Was this request cancelled?
320 			if (request.getAttribute(Globals.CANCEL_KEY) != null) {
321 				if (LOG.isDebugEnabled()) {
322 					LOG.debug(" Cancelled transaction, skipping validation");
323 				}
324 				return (true);
325 			}
326 
327 			// Has validation been turned off for this mapping?
328 			if (!mapping.getValidate()) {
329 				return (true);
330 			}
331 
332 			// call super to call forms validate
333 			super.processValidate(request, response, form, mapping);
334 		}
335 
336 		publishMessages(request);
337 		if (!GlobalVariables.getMessageMap().hasNoErrors()) {
338 			// Special handling for multipart request
339 			if (form.getMultipartRequestHandler() != null) {
340 				if (LOG.isDebugEnabled()) {
341 					LOG.debug("  Rolling back multipart request");
342 				}
343 				form.getMultipartRequestHandler().rollback();
344 			}
345 
346 			// Fix state that could be incorrect because of validation failure
347 			if (form instanceof PojoForm) {
348 				((PojoForm) form).processValidationFail();
349 			}
350 
351 			// Was an input path (or forward) specified for this mapping?
352 			String input = mapping.getInput();
353 			if (input == null) {
354 				if (LOG.isDebugEnabled()) {
355 					LOG.debug("  Validation failed but no input form available");
356 				}
357 				response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, getInternal().getMessage("noInput", mapping.getPath()));
358 				return (false);
359 			}
360 
361 			if (moduleConfig.getControllerConfig().getInputForward()) {
362 				ForwardConfig forward = mapping.findForward(input);
363 				processForwardConfig(request, response, forward);
364 			} else {
365 				internalModuleRelativeForward(input, request, response);
366 			}
367 
368 			return (false);
369 		}
370 		return true;
371 	}
372 
373 	/**
374 	 * Checks for return from a lookup or question, and restores the action form
375 	 * stored under the request parameter docFormKey.
376 	 */
377 	@Override
378 	protected ActionForm processActionForm(HttpServletRequest request, HttpServletResponse response, ActionMapping mapping) {
379 		
380 		String documentNumber = getDocumentNumber(request);
381 		if (documentNumber != null) { MDC.put(MDC_DOC_ID, documentNumber); }
382 		
383 		UserSession userSession = (UserSession) request.getSession().getAttribute(KRADConstants.USER_SESSION_KEY);
384 
385 		String docFormKey = request.getParameter(KRADConstants.DOC_FORM_KEY);
386 		String methodToCall = request.getParameter(KRADConstants.DISPATCH_REQUEST_PARAMETER);
387 		String refreshCaller = request.getParameter(KRADConstants.REFRESH_CALLER);
388 //		String searchListRequestKey = request.getParameter(KRADConstants.SEARCH_LIST_REQUEST_KEY);
389 		String documentWebScope = request.getParameter(KRADConstants.DOCUMENT_WEB_SCOPE);
390 
391 		if (mapping.getPath().startsWith(KRADConstants.REFRESH_MAPPING_PREFIX) || KRADConstants.RETURN_METHOD_TO_CALL.equalsIgnoreCase(methodToCall) ||
392 				KRADConstants.QUESTION_REFRESH.equalsIgnoreCase(refreshCaller) || KRADConstants.TEXT_AREA_REFRESH.equalsIgnoreCase(refreshCaller) || KRADConstants
393                 .SESSION_SCOPE.equalsIgnoreCase(documentWebScope)) {
394 			ActionForm form = null;
395 			// check for search result storage and clear
396 			GlobalVariables.getUserSession().removeObjectsByPrefix(KRADConstants.SEARCH_LIST_KEY_PREFIX);
397 
398 			// We put different type of forms such as document form, lookup form
399 			// in session but we only store document form in
400 			// database.
401 			if (userSession.retrieveObject(docFormKey) != null) {
402 				LOG.debug("getDecomentForm KualiDocumentFormBase from session");
403 				form = (ActionForm) userSession.retrieveObject(docFormKey);
404 			} else if (StringUtils.isNotBlank(documentNumber)) {
405 				form = getSessionDocumentService().getDocumentForm(documentNumber, docFormKey, userSession, request.getRemoteAddr());
406 			}
407 			request.setAttribute(mapping.getAttribute(), form);
408 			if (!KRADConstants.SESSION_SCOPE.equalsIgnoreCase(documentWebScope)) {
409 				userSession.removeObject(docFormKey);
410 			}
411 			// we should check whether this is a multipart request because we
412 			// could have had a combination of query parameters and a multipart
413 			// request
414 			String contentType = request.getContentType();
415 			String method = request.getMethod();
416 			if (("POST".equalsIgnoreCase(method) && contentType != null && contentType.startsWith("multipart/form-data"))) {
417 				// this method parses the multipart request and adds new
418 				// non-file parameters into the request
419 				WebUtils.getMultipartParameters(request, null, form, mapping);
420 			}
421 			// The form can be null if the document is not a session document
422 			if (form != null) {
423 				return form;
424 			}
425 		}
426 
427 		// Rice has the ability to limit file upload sizes on a per-form basis,
428 		// so the max upload sizes may be accessed by calling methods on
429 		// PojoFormBase.
430 		// This requires that we are able know the file upload size limit (i.e.
431 		// retrieve a form instance) before we parse a mulitpart request.
432 		ActionForm form = super.processActionForm(request, response, mapping);
433 
434 		// for sessiondocument with multipart request
435 		String contentType = request.getContentType();
436 		String method = request.getMethod();
437 
438 		if ("GET".equalsIgnoreCase(method) && StringUtils.isNotBlank(methodToCall) && form instanceof PojoForm &&
439 				((PojoForm) form).getMethodToCallsToBypassSessionRetrievalForGETRequests().contains(methodToCall)) {
440 			return createNewActionForm(mapping, request);
441 		}
442 		
443 		// if we have a multipart request, parse it and return the stored form
444 		// from session if the doc form key is not blank. If it is blank, then
445 		// we just return the form
446 		// generated from the superclass processActionForm method. Either way,
447 		// we need to parse the mulitpart request now so that we may determine
448 		// what the value of the doc form key is.
449 		// This is generally against the contract of processActionForm, because
450 		// processPopulate should be responsible for parsing the mulitpart
451 		// request, but we need to parse it now
452 		// to determine the doc form key value.
453 		if (("POST".equalsIgnoreCase(method) && contentType != null && contentType.startsWith("multipart/form-data"))) {
454 			WebUtils.getMultipartParameters(request, null, form, mapping);
455 			docFormKey = request.getParameter(KRADConstants.DOC_FORM_KEY);
456 			documentWebScope = request.getParameter(KRADConstants.DOCUMENT_WEB_SCOPE);
457 
458 			documentNumber = getDocumentNumber(request);
459 
460 			if (KRADConstants.SESSION_SCOPE.equalsIgnoreCase(documentWebScope) ||
461 					(form instanceof KualiDocumentFormBase && WebUtils
462                             .isDocumentSession(((KualiDocumentFormBase) form).getDocument(),
463                                     (KualiDocumentFormBase) form))) {
464 
465 				Object userSessionObject = userSession.retrieveObject(docFormKey);
466 				if ( userSessionObject != null &&  userSessionObject instanceof ActionForm ) {
467 					LOG.debug("getDocumentForm KualiDocumentFormBase from session");
468 					form = (ActionForm) userSessionObject;
469 				} else {
470 					ActionForm tempForm = getSessionDocumentService().getDocumentForm(documentNumber, docFormKey, userSession, request.getRemoteAddr());
471 					if ( tempForm != null ) {
472 						form = tempForm;
473 					}
474 				}
475 
476 				request.setAttribute(mapping.getAttribute(), form);
477 				if (form != null) {
478 					return form;
479 				}
480 			}
481 		}
482 		return form;
483 	}
484 
485 	/**
486 	 * Hook into action perform to handle errors in the error map and catch
487 	 * exceptions.
488 	 * 
489 	 * <p>
490 	 * A transaction is started prior to the execution of the action. This
491 	 * allows for the action code to execute efficiently without the need for
492 	 * using PROPAGATION_SUPPORTS in the transaction definitions. The
493 	 * PROPAGATION_SUPPORTS propagation type does not work well with JTA.
494 	 */
495 	@Override
496 	protected ActionForward processActionPerform(final HttpServletRequest request, final HttpServletResponse response, final Action action, final ActionForm form, final ActionMapping mapping) throws IOException, ServletException {
497 		try {
498             TransactionTemplate template = new TransactionTemplate(getTransactionManager());
499 			ActionForward forward = null;
500 			try {
501 				forward = (ActionForward) template.execute(new TransactionCallback() {
502 					public Object doInTransaction(TransactionStatus status) {
503 						ActionForward actionForward = null;
504 						try {
505 							actionForward = action.execute(mapping, form, request, response);
506 						} catch (Exception e) {
507                             if (e.getMessage()!= null && e.getMessage().equals(KRADConstants.KRAD_INITIATED_DOCUMENT_VIEW_NAME)) {
508                                 ConfigurationService kualiConfigurationService = CoreApiServiceLocator
509                                         .getKualiConfigurationService();
510                                 StringBuffer sb = new StringBuffer();
511                                 sb.append(kualiConfigurationService.getPropertyValueAsString(KRADConstants.KRAD_URL_KEY));
512                                 sb.append(kualiConfigurationService.getPropertyValueAsString(KRADConstants.KRAD_INITIATED_DOCUMENT_URL_KEY));
513                                 return new ActionForward(KRADConstants.KRAD_INITIATED_DOCUMENT_VIEW_NAME, sb.toString() ,true);
514                             }
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 = KNSServiceLocator.getTransactionManager();
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 }