001    package com.mockrunner.struts;
002    
003    import java.io.File;
004    import java.io.FileInputStream;
005    import java.util.ArrayList;
006    import java.util.Iterator;
007    import java.util.List;
008    import java.util.Locale;
009    
010    import javax.servlet.ServletException;
011    import javax.sql.DataSource;
012    
013    import org.apache.commons.beanutils.BeanUtils;
014    import org.apache.commons.logging.Log;
015    import org.apache.commons.logging.LogFactory;
016    import org.apache.commons.validator.ValidatorResources;
017    import org.apache.struts.Globals;
018    import org.apache.struts.action.Action;
019    import org.apache.struts.action.ActionErrors;
020    import org.apache.struts.action.ActionForm;
021    import org.apache.struts.action.ActionForward;
022    import org.apache.struts.action.ActionMapping;
023    import org.apache.struts.action.ActionMessage;
024    import org.apache.struts.action.ActionMessages;
025    import org.apache.struts.action.DynaActionForm;
026    import org.apache.struts.action.DynaActionFormClass;
027    import org.apache.struts.config.FormBeanConfig;
028    import org.apache.struts.config.MessageResourcesConfig;
029    import org.apache.struts.taglib.html.Constants;
030    import org.apache.struts.util.MessageResources;
031    import org.apache.struts.validator.ValidatorPlugIn;
032    
033    import com.mockrunner.base.HTMLOutputModule;
034    import com.mockrunner.base.NestedApplicationException;
035    import com.mockrunner.base.VerifyFailedException;
036    import com.mockrunner.mock.web.ActionMockObjectFactory;
037    import com.mockrunner.mock.web.MockActionForward;
038    import com.mockrunner.mock.web.MockActionMapping;
039    import com.mockrunner.mock.web.MockPageContext;
040    import com.mockrunner.util.common.StreamUtil;
041    
042    /**
043     * Module for Struts action tests. Simulates Struts
044     * without reading the <i>struts-config.xml</i> file.
045     * Per default this class does everything like Struts
046     * when calling an action but you can change the behaviour
047     * (e.g. disable form population).
048     * Please note: If your action throws an exception and an 
049     * exception handler is registered (use {@link #addExceptionHandler}),
050     * the handler will be called to handle the exception.
051     * Otherwise the exception will be rethrown as {@link com.mockrunner.base.NestedApplicationException}.
052     */
053    public class ActionTestModule extends HTMLOutputModule
054    {
055        private final static Log log = LogFactory.getLog(ActionTestModule.class);
056        private ActionMockObjectFactory mockFactory;
057        private MockActionForward forward;
058        private ActionForm formObj;
059        private Action actionObj;
060        private boolean reset;
061        private boolean doPopulate;
062        private boolean recognizeInSession;
063        private String messageAttributeKey;
064        private String errorAttributeKey;
065        private List exceptionHandlers;
066        
067        public ActionTestModule(ActionMockObjectFactory mockFactory)
068        {
069            super(mockFactory);
070            this.mockFactory = mockFactory;
071            reset = true;
072            doPopulate = true;
073            recognizeInSession = true;
074            messageAttributeKey = Globals.MESSAGE_KEY;
075            errorAttributeKey = Globals.ERROR_KEY;
076            exceptionHandlers = new ArrayList();
077        }
078        
079        /**
080         * Set if the reset method should be called before
081         * populating a form with {@link #populateRequestToForm}.
082         * Default is <code>true</code> which is the standard Struts 
083         * behaviour.
084         * @param reset should reset be called
085         */
086        public void setReset(boolean reset)
087        {
088            this.reset = reset;
089        }
090        
091        /**
092         * Set if the form should be populated with the request
093         * parameters before calling the action.
094         * Default is <code>true</code> which is the standard Struts
095         * behaviour.
096         * @param doPopulate should population be performed
097         */
098        public void setDoPopulate(boolean doPopulate)
099        {
100            this.doPopulate = doPopulate;
101        }
102        
103        /**
104         * Set if messages that are saved to the session (instead of
105         * the request) should be recognized.
106         * Default is <code>true</code>.
107         * @param recognizeInSession should messages in the session be recognized
108         */
109        public void setRecognizeMessagesInSession(boolean recognizeInSession)
110        {
111            this.recognizeInSession = recognizeInSession;
112        }
113        
114        /**
115         * Name of the key under which messages are stored. Default is
116         * <code>Globals.MESSAGE_KEY</code>.
117         * @param messageAttributeKey the message key
118         */
119        public void setMessageAttributeKey(String messageAttributeKey)
120        {
121            this.messageAttributeKey = messageAttributeKey;
122        }
123        
124        /**
125         * Name of the key under which errors are stored. Default is
126         * <code>Globals.ERROR_KEY</code>.
127         * @param errorAttributeKey the message key
128         */
129        public void setErrorAttributeKey(String errorAttributeKey)
130        {
131            this.errorAttributeKey = errorAttributeKey;
132        }
133    
134        /**
135         * Convenience method for map backed properties. Creates a String
136         * <i>value(property)</i>.
137         * @param property the property
138         * @return the String in map backed propery style
139         */
140        public String addMappedPropertyRequestPrefix(String property)
141        {
142            return "value(" + property + ")";
143        }
144    
145        /**
146         * Sets the parameter by calling <code>ActionMapping.setParameter</code>
147         * on the action mapping returned by {@link #getActionMapping}.
148         * You can test your Actions with different parameter settings in the
149         * same test method.
150         * @param parameter the parameter
151         */
152        public void setParameter(String parameter)
153        {
154            getActionMapping().setParameter(parameter);
155        }
156    
157        /**
158         * Sets if form validation should be performed before calling the action.
159         * Calls <code>ActionMapping.setValidate</code> on the action mapping returned 
160         * by {@link #getActionMapping}. Default is <code>false</code>.
161         * @param validate should validation be performed
162         */
163        public void setValidate(boolean validate)
164        {
165            getActionMapping().setValidate(validate);
166        }
167        
168        /**
169         * Sets the input attribute. If form validation fails, the
170         * input attribute can be verified with {@link #verifyForward}.
171         * Calls <code>ActionMapping.setInput</code> on the action mapping returned 
172         * by {@link #getActionMapping}.
173         * @param input the input attribute
174         */
175        public void setInput(String input)
176        {
177            getActionMapping().setInput(input);
178        }
179        
180        /**
181         * Registers an exception handler. The exception handler will
182         * be called if an action throws an exception. Usually, you
183         * will pass an instance of {@link DefaultExceptionHandlerConfig}
184         * to this method. {@link DefaultExceptionHandlerConfig}
185         * relies on Struts <code>ExceptionHandler</code> classes.
186         * In special cases, you may add own implementations of
187         * {@link ExceptionHandlerConfig}, that may be independent from
188         * the Struts exception handling mechanism.
189         * If no matching handler is registered, the exception will be rethrown
190         * as {@link com.mockrunner.base.NestedApplicationException}.
191         * @param handler the exception handler
192         */
193        public void addExceptionHandler(ExceptionHandlerConfig handler)
194        {
195            if(null != handler)
196            {
197                exceptionHandlers.add(handler);
198            }
199        }
200        
201        /**
202         * Sets the specified messages resources as a request attribute
203         * using <code>Globals.MESSAGES_KEY</code> as the key. You can
204         * use this method, if your action calls 
205         * <code>Action.getResources(HttpServletRequest)</code>.
206         * The deprecated method <code>Action.getResources()</code>
207         * takes the resources from the servlet context with the same key.
208         * If your action uses this method, you have to set the resources
209         * manually to the servlet context.
210         * @param resources the messages resources
211         */
212        public void setResources(MessageResources resources)
213        {
214            mockFactory.getWrappedRequest().setAttribute(Globals.MESSAGES_KEY, resources);
215        }
216        
217        /**
218         * Sets the specified messages resources as a servlet context 
219         * attribute using the specified key and the module config prefix.
220         * You can use this method, if your action calls 
221         * <code>Action.getResources(HttpServletRequest, String)</code>.
222         * Please note that the {@link com.mockrunner.mock.web.MockModuleConfig}
223         * is set by Mockrunner as the current module. It has the name <i>testmodule</i>.
224         * This can be changed with <code>ModuleConfig.setPrefix</code>.
225         * @param key the key of the messages resources
226         * @param resources the messages resources
227         */
228        public void setResources(String key, MessageResources resources)
229        {
230            MessageResourcesConfig config = new MessageResourcesConfig();
231            config.setKey(key);
232            mockFactory.getMockModuleConfig().addMessageResourcesConfig(config);
233            key = key + mockFactory.getMockModuleConfig().getPrefix();
234            mockFactory.getMockServletContext().setAttribute(key, resources); 
235        }
236        
237        /**
238         * Sets the specified <code>DataSource</code>.
239         * You can use this method, if your action calls 
240         * <code>Action.getDataSource(HttpServletRequest)</code>.
241         * @param dataSource <code>DataSource</code>
242         */
243        public void setDataSource(DataSource dataSource)
244        {
245            setDataSource("org.apache.struts.action.DATA_SOURCE", dataSource);
246        }
247        
248        /**
249         * Sets the specified <code>DataSource</code>.
250         * You can use this method, if your action calls 
251         * <code>Action.getDataSource(HttpServletRequest, String)</code>.
252         * @param key the key of the <code>DataSource</code>
253         * @param dataSource <code>DataSource</code>
254         */
255        public void setDataSource(String key, DataSource dataSource)
256        {
257            key = key + mockFactory.getMockModuleConfig().getPrefix();
258            mockFactory.getMockServletContext().setAttribute(key, dataSource);
259        }
260        
261        /**
262         * Sets the specified locale as a session attribute
263         * using <code>Globals.LOCALE_KEY</code> as the key. You can
264         * use this method, if your action calls 
265         * <code>Action.getLocale(HttpServletRequest)</code>.
266         * @param locale the locale
267         */
268        public void setLocale(Locale locale)
269        {
270            mockFactory.getMockSession().setAttribute(Globals.LOCALE_KEY, locale);
271        }
272        
273        /**
274         * Creates a valid <code>ValidatorResources</code> object based
275         * on the specified config files. Since the parsing of the files
276         * is time consuming, it makes sense to cache the result.
277         * You can set the returned <code>ValidatorResources</code> object
278         * with {@link #setValidatorResources}. It is then used in
279         * all validations.
280         * @param resourcesFiles the array of config files
281         */
282        public ValidatorResources createValidatorResources(String[] resourcesFiles)
283        {  
284            if(resourcesFiles.length == 0) return null;
285            setUpServletContextResourcePath(resourcesFiles);
286            String resourceString = resourcesFiles[0];
287            for(int ii = 1; ii < resourcesFiles.length; ii++)
288            {
289                resourceString += "," + resourcesFiles[ii];
290            }
291            ValidatorPlugIn plugIn = new ValidatorPlugIn();
292            plugIn.setPathnames(resourceString);
293            try
294            {
295                plugIn.init(mockFactory.getMockActionServlet(), mockFactory.getMockModuleConfig());
296            }
297            catch(ServletException exc)
298            {
299                log.error("Error initializing ValidatorPlugIn", exc);
300                throw new RuntimeException("Error initializing ValidatorPlugIn: " + exc.getMessage());
301            }
302            String key = ValidatorPlugIn.VALIDATOR_KEY + mockFactory.getMockModuleConfig().getPrefix();
303            return (ValidatorResources)mockFactory.getMockServletContext().getAttribute(key);
304        }
305        
306        private void setUpServletContextResourcePath(String[] resourcesFiles)
307        {
308            for(int ii = 0; ii < resourcesFiles.length; ii++)
309            {
310                String file = resourcesFiles[ii];
311                try
312                {
313                    File streamFile = new File(file);
314                    FileInputStream stream = new FileInputStream(streamFile);
315                    byte[] fileData = StreamUtil.getStreamAsByteArray(stream);
316                    mockFactory.getMockServletContext().setResourceAsStream(file, fileData);
317                    mockFactory.getMockServletContext().setResource(file, streamFile.toURL());
318                }
319                catch(Exception exc)
320                {
321                    throw new NestedApplicationException(exc);
322                }
323            }
324        }
325        
326        /**
327         * Sets the specified <code>ValidatorResources</code>. The easiest
328         * way to create <code>ValidatorResources</code> is the method
329         * {@link #createValidatorResources}.
330         * @param validatorResources the <code>ValidatorResources</code>
331         */
332        public void setValidatorResources(ValidatorResources validatorResources)
333        {
334            String key = ValidatorPlugIn.VALIDATOR_KEY + mockFactory.getMockModuleConfig().getPrefix();
335            mockFactory.getMockServletContext().setAttribute(key, validatorResources);
336        }
337    
338        /**
339         * Verifies the forward path returned by the action.
340         * If your action uses <code>mapping.findForward("success")</code>
341         * to find the forward, you can use this method or
342         * {@link #verifyForwardName} to test the <code>success</code> forward
343         * name. If your action creates an <code>ActionForward</code> on its
344         * own you can use this method to verify the forward <code>path</code>.
345         * @param path the expected path
346         * @throws VerifyFailedException if verification fails
347         */
348        public void verifyForward(String path)
349        {
350            if(null == getActionForward())
351            {
352                throw new VerifyFailedException("ActionForward == null");
353            }
354            else if (!getActionForward().verifyPath(path))
355            {
356                throw new VerifyFailedException("expected " + path + ", received " + getActionForward().getPath());
357            }
358        }
359        
360        /**
361         * Verifies the forward name returned by the action.
362         * If your action uses <code>mapping.findForward("success")</code>
363         * to find the forward, you can use this method or
364         * {@link #verifyForward} to test the <code>success</code> forward
365         * name. If your action creates an <code>ActionForward</code> on its
366         * own you can use this method to verify the forward <code>name</code>.
367         * @param name the expected name
368         * @throws VerifyFailedException if verification fails
369         */
370        public void verifyForwardName(String name)
371        {
372            if(null == getActionForward())
373            {
374                throw new VerifyFailedException("ActionForward == null");
375            }
376            else if (!getActionForward().verifyName(name))
377            {
378                throw new VerifyFailedException("expected " + name + ", received " + getActionForward().getName());
379            }
380        }
381        
382        /**
383         * Verifies the redirect attribute.
384         * @param redirect the expected redirect attribute
385         * @throws VerifyFailedException if verification fails
386         */
387        public void verifyRedirect(boolean redirect)
388        {
389            if(null == getActionForward())
390            {
391                throw new VerifyFailedException("ActionForward == null");
392            }
393            else if(!getActionForward().verifyRedirect(redirect))
394            {
395                throw new VerifyFailedException("expected " + redirect + ", received " + getActionForward().getRedirect());
396            }
397        }
398    
399        /**
400         * Verifies that there are no action errors present.
401         * @throws VerifyFailedException if verification fails
402         */
403        public void verifyNoActionErrors()
404        {
405            verifyNoActionMessages(getActionErrors());   
406        }
407        
408        /**
409         * Verifies that there are no action messages present.
410         * @throws VerifyFailedException if verification fails
411         */
412        public void verifyNoActionMessages()
413        {
414            verifyNoActionMessages(getActionMessages());   
415        }
416        
417        private void verifyNoActionMessages(ActionMessages messages)
418        {
419            if(containsMessages(messages))
420            {
421                StringBuffer buffer = new StringBuffer();
422                buffer.append("has the following messages/errors: ");
423                Iterator iterator = messages.get();
424                while(iterator.hasNext())
425                {
426                    ActionMessage message = (ActionMessage)iterator.next();
427                    buffer.append(message.getKey() + ";");
428                }
429                throw new VerifyFailedException(buffer.toString());
430            }
431        }
432    
433        /**
434         * Verifies that there are action errors present.
435         * @throws VerifyFailedException if verification fails
436         */
437        public void verifyHasActionErrors()
438        {
439            if(!containsMessages(getActionErrors()))
440            {
441                throw new VerifyFailedException("no action errors");
442            }
443        }
444        
445        /**
446         * Verifies that there are action messages present.
447         * @throws VerifyFailedException if verification fails
448         */
449        public void verifyHasActionMessages()
450        {
451            if(!containsMessages(getActionMessages()))
452            {
453                throw new VerifyFailedException("no action messages");
454            }
455        }
456    
457        /**
458         * Verifies that an action error with the specified key
459         * is present.
460         * @param errorKey the expected error key
461         * @throws VerifyFailedException if verification fails
462         */
463        public void verifyActionErrorPresent(String errorKey)
464        {
465            verifyActionMessagePresent(errorKey, getActionErrors());
466        }
467        
468        /**
469         * Verifies that an action message with the specified key
470         * is present.
471         * @param messageKey the expected message key
472         * @throws VerifyFailedException if verification fails
473         */
474        public void verifyActionMessagePresent(String messageKey)
475        {
476            verifyActionMessagePresent(messageKey, getActionMessages());
477        }
478        
479        private void verifyActionMessagePresent(String messageKey, ActionMessages messages)
480        {
481            if(!containsMessages(messages)) throw new VerifyFailedException("no action messages/errors");
482            Iterator iterator = messages.get();
483            while (iterator.hasNext())
484            {
485                ActionMessage message = (ActionMessage) iterator.next();
486                if(message.getKey().equals(messageKey))
487                {
488                    return;
489                }
490            }
491            throw new VerifyFailedException("message/error " + messageKey + " not present");
492        }
493        
494        /**
495         * Verifies that an action error with the specified key
496         * is not present.
497         * @param errorKey the error key
498         * @throws VerifyFailedException if verification fails
499         */
500        public void verifyActionErrorNotPresent(String errorKey)
501        {
502            verifyActionMessageNotPresent(errorKey, getActionErrors());
503        }
504        
505        /**
506         * Verifies that an action message with the specified key
507         * is not present.
508         * @param messageKey the message key
509         * @throws VerifyFailedException if verification fails
510         */
511        public void verifyActionMessageNotPresent(String messageKey)
512        {
513            verifyActionMessageNotPresent(messageKey, getActionMessages());
514        }
515        
516        private void verifyActionMessageNotPresent(String messageKey, ActionMessages messages)
517        {
518            if(!containsMessages(messages)) return;
519            Iterator iterator = messages.get();
520            while(iterator.hasNext())
521            {
522                ActionMessage message = (ActionMessage) iterator.next();
523                if(message.getKey().equals(messageKey))
524                {
525                    throw new VerifyFailedException("message/error " + messageKey + " present");
526                }
527            }
528        }
529    
530        /**
531         * Verifies that the specified action errors are present.
532         * Regards number and order of action errors.
533         * @param errorKeys the array of expected error keys
534         * @throws VerifyFailedException if verification fails
535         */
536        public void verifyActionErrors(String errorKeys[])
537        {
538            verifyActionMessages(errorKeys, getActionErrors());  
539        }
540        
541        /**
542         * Verifies that the specified action messages are present.
543         * Regards number and order of action messages.
544         * @param messageKeys the array of expected message keys
545         * @throws VerifyFailedException if verification fails
546         */
547        public void verifyActionMessages(String messageKeys[])
548        {
549            verifyActionMessages(messageKeys, getActionMessages());  
550        }
551        
552        private void verifyActionMessages(String messageKeys[], ActionMessages messages)
553        {
554            if (!containsMessages(messages)) throw new VerifyFailedException("no action messages/errors");
555            if(messages.size() != messageKeys.length) throw new VerifyFailedException("expected " + messageKeys.length + " messages/errors, received " + messages.size() + " messages/errors");
556            Iterator iterator = messages.get();
557            for(int ii = 0; ii < messageKeys.length; ii++)
558            {
559                ActionMessage message = (ActionMessage) iterator.next();
560                if(!message.getKey().equals(messageKeys[ii]))
561                {
562                    throw new VerifyFailedException("mismatch at position " + ii + ", actual: " + message.getKey() + ", expected: " + messageKeys[ii]);
563                }
564            }
565        }
566        
567        /**
568         * Verifies the values of the action error with the
569         * specified key. Regards number and order of values.
570         * @param errorKey the error key
571         * @param values the exepcted values
572         * @throws VerifyFailedException if verification fails
573         */
574        public void verifyActionErrorValues(String errorKey, Object[] values)
575        {
576            ActionMessage error = getActionErrorByKey(errorKey);
577            if(null == error) throw new VerifyFailedException("action error " + errorKey + " not present");
578            verifyActionMessageValues(error, values);
579        }
580        
581        /**
582         * Verifies the values of the action message with the
583         * specified key. Regards number and order of values.
584         * @param messageKey the message key
585         * @param values the exepcted values
586         * @throws VerifyFailedException if verification fails
587         */
588        public void verifyActionMessageValues(String messageKey, Object[] values)
589        {
590            ActionMessage message = getActionMessageByKey(messageKey);
591            if(null == message) throw new VerifyFailedException("action message " + messageKey + " not present");
592            verifyActionMessageValues(message, values);
593        }
594        
595        private void verifyActionMessageValues(ActionMessage message, Object[] values)
596        {
597            Object[] actualValues = message.getValues();
598            if(null == actualValues) throw new VerifyFailedException("action message/error " + message.getKey() + " has no values");
599            if(values.length != actualValues.length) throw new VerifyFailedException("action message/error " + message.getKey() + " has " + actualValues + " values");
600            for(int ii = 0; ii < actualValues.length; ii++)
601            {
602                if(!values[ii].equals(actualValues[ii]))
603                {
604                    throw new VerifyFailedException("action message/error " + message.getKey() + ": expected value[" + ii + "]: " + values[ii] + " received value[" + ii + "]: " + actualValues[ii]);
605                }
606            }
607        }
608        
609        /**
610         * Verifies the value of the action error with the
611         * specified key. Fails if the specified value does
612         * not match the actual value or if the error has more
613         * than one value.
614         * @param errorKey the error key
615         * @param value the exepcted value
616         * @throws VerifyFailedException if verification fails
617         */
618        public void verifyActionErrorValue(String errorKey, Object value)
619        {
620            verifyActionErrorValues(errorKey, new Object[] { value });
621        }
622        
623        /**
624         * Verifies the value of the action message with the
625         * specified key. Fails if the specified value does
626         * not match the actual value or if the message has more
627         * than one value.
628         * @param messageKey the message key
629         * @param value the exepcted value
630         * @throws VerifyFailedException if verification fails
631         */
632        public void verifyActionMessageValue(String messageKey, Object value)
633        {
634            verifyActionMessageValues(messageKey, new Object[] { value });
635        }
636        
637        /**
638         * Verifies that the specified error is stored for the specified
639         * property.
640         * @param errorKey the error key
641         * @param property the exepcted value
642         * @throws VerifyFailedException if verification fails
643         */
644        public void verifyActionErrorProperty(String errorKey, String property)
645        {
646            verifyActionMessageProperty(errorKey, property, getActionErrors());
647        }
648        
649        /**
650         * Verifies that the specified message is stored for the specified
651         * property.
652         * @param messageKey the message key
653         * @param property the exepcted value
654         * @throws VerifyFailedException if verification fails
655         */
656        public void verifyActionMessageProperty(String messageKey, String property)
657        {
658            verifyActionMessageProperty(messageKey, property, getActionMessages());
659        }
660        
661        private void verifyActionMessageProperty(String messageKey, String property, ActionMessages messages)
662        {
663            verifyActionMessagePresent(messageKey, messages);
664            Iterator iterator = messages.get(property);
665            while(iterator.hasNext())
666            {
667                  ActionMessage message = (ActionMessage)iterator.next();
668                  if(message.getKey().equals(messageKey)) return;
669            }
670            throw new VerifyFailedException("action message/error " + messageKey + " not present for property " + property);
671        }
672        
673        /**
674         * Verifies the number of action errors.
675         * @param number the expected number of errors
676         * @throws VerifyFailedException if verification fails
677         */
678        public void verifyNumberActionErrors(int number)
679        {
680            verifyNumberActionMessages(number, getActionErrors());
681        }
682        
683        /**
684         * Verifies the number of action messages.
685         * @param number the expected number of messages
686         * @throws VerifyFailedException if verification fails
687         */
688        public void verifyNumberActionMessages(int number)
689        {
690            verifyNumberActionMessages(number, getActionMessages());
691        }
692       
693        private void verifyNumberActionMessages(int number, ActionMessages messages)
694        {
695            if (null != messages)
696            {
697                if (messages.size() == number) return;
698                throw new VerifyFailedException("expected " + number + " messages/errors, received " + messages.size() + " messages/errors");
699            }
700            if (number == 0) return;
701            throw new VerifyFailedException("no action messages/errors");
702        }
703    
704        /**
705         * Returns the action error with the specified key or null
706         * if such an error does not exist.
707         * @param errorKey the error key
708         * @return the action error with the specified key
709         */
710        public ActionMessage getActionErrorByKey(String errorKey)
711        {
712            return getActionMessageByKey(errorKey, getActionErrors());
713        }
714        
715        /**
716         * Returns the action message with the specified key or null
717         * if such a message does not exist.
718         * @param messageKey the message key
719         * @return the action message with the specified key
720         */
721        public ActionMessage getActionMessageByKey(String messageKey)
722        {
723            return (ActionMessage)getActionMessageByKey(messageKey, getActionMessages());
724        }
725        
726        private ActionMessage getActionMessageByKey(String messageKey, ActionMessages messages)
727        {
728            if(null == messages) return null;
729            Iterator iterator = messages.get();
730            while (iterator.hasNext())
731            {
732                ActionMessage message = (ActionMessage) iterator.next();
733                if (message.getKey().equals(messageKey))
734                {
735                    return message;
736                }
737            }
738            return null;
739        }
740        
741        /**
742         * Sets the specified <code>ActionMessages</code> object 
743         * as the currently present messages to the request.
744         * @param messages the ActionMessages object
745         */
746        public void setActionMessages(ActionMessages messages)
747        {
748            mockFactory.getWrappedRequest().setAttribute(messageAttributeKey, messages);
749        }
750        
751        /**
752         * Sets the specified <code>ActionMessages</code> object 
753         * as the currently present messages to the session.
754         * @param messages the ActionMessages object
755         */
756        public void setActionMessagesToSession(ActionMessages messages)
757        {
758            mockFactory.getMockSession().setAttribute(messageAttributeKey, messages);
759        }
760    
761        /**
762         * Get the currently present action messages. Can be called
763         * after {@link #actionPerform} to get the messages the action
764         * has set. If messages in the session are recognized 
765         * (use {@link #setRecognizeMessagesInSession}), this method
766         * returns the union of request and session messages. Otherwise,
767         * it only returns the request messages.
768         * @return the action messages
769         */
770        public ActionMessages getActionMessages()
771        {
772            ActionMessages requestMessages = getActionMessagesFromRequest();
773            ActionMessages sessionMessages = getActionMessagesFromSession();
774            if(recognizeInSession)
775            {
776                if(null == requestMessages || requestMessages.isEmpty()) return sessionMessages;
777                if(null == sessionMessages || sessionMessages.isEmpty()) return requestMessages;
778                requestMessages = new ActionMessages(requestMessages);
779                requestMessages.add(sessionMessages);
780            }
781            return requestMessages;
782        }
783        
784        /**
785         * Get the currently present action messages from the request.
786         * @return the action messages
787         */
788        public ActionMessages getActionMessagesFromRequest()
789        {
790            return (ActionMessages)mockFactory.getWrappedRequest().getAttribute(messageAttributeKey);
791        }
792        
793        /**
794         * Get the currently present action messages from the session.
795         * @return the action messages
796         */
797        public ActionMessages getActionMessagesFromSession()
798        {
799            return (ActionMessages)mockFactory.getMockSession().getAttribute(messageAttributeKey);
800        }
801        
802        /**
803         * Returns if action messages are present.
804         * @return true if messages are present, false otherwise
805         */
806        public boolean hasActionMessages()
807        {
808            ActionMessages messages = getActionMessages();
809            return containsMessages(messages);
810        }
811    
812        /**
813         * Sets the specified <code>ActionErrors</code> object 
814         * as the currently present errors to the request.
815         * @param errors the ActionErrors object
816         */
817        public void setActionErrors(ActionMessages errors)
818        {
819            mockFactory.getWrappedRequest().setAttribute(errorAttributeKey, errors);
820        }
821        
822        /**
823         * Sets the specified <code>ActionErrors</code> object 
824         * as the currently present errors to the session.
825         * @param errors the ActionErrors object
826         */
827        public void setActionErrorsToSession(ActionMessages errors)
828        {
829            mockFactory.getMockSession().setAttribute(errorAttributeKey, errors);
830        }
831    
832        /**
833         * Get the currently present action errors. Can be called
834         * after {@link #actionPerform} to get the errors the action
835         * has set. If messages in the session are recognized 
836         * (use {@link #setRecognizeMessagesInSession}), this method
837         * returns the union of request and session errors. Otherwise,
838         * it only returns the request errors.
839         * @return the action errors
840         */
841        public ActionMessages getActionErrors()
842        {
843            ActionMessages requestErrors = getActionErrorsFromRequest();
844            ActionMessages sessionErrors = getActionErrorsFromSession();
845            if(recognizeInSession)
846            {
847                if(null == requestErrors || requestErrors.isEmpty()) return sessionErrors;
848                if(null == sessionErrors || sessionErrors.isEmpty()) return requestErrors;
849                if((requestErrors instanceof ActionErrors) || (sessionErrors instanceof ActionErrors))
850                {
851                    ActionErrors tempErrors = new ActionErrors();
852                    tempErrors.add(requestErrors);
853                    requestErrors = tempErrors;
854                }
855                else
856                {
857                    requestErrors = new ActionMessages(requestErrors);
858                }
859                requestErrors.add(sessionErrors);
860            }
861            return requestErrors;
862        }
863        
864        /**
865         * Get the currently present action errors from the request.
866         * @return the action messages
867         */
868        public ActionMessages getActionErrorsFromRequest()
869        {
870            return (ActionMessages)mockFactory.getWrappedRequest().getAttribute(errorAttributeKey);
871        }
872        
873        /**
874         * Get the currently present action errors from the session.
875         * @return the action messages
876         */
877        public ActionMessages getActionErrorsFromSession()
878        {
879            return (ActionMessages)mockFactory.getMockSession().getAttribute(errorAttributeKey);
880        }
881    
882        /**
883         * Returns if action errors are present.
884         * @return true if errors are present, false otherwise
885         */
886        public boolean hasActionErrors()
887        {
888            ActionMessages errors = getActionErrors();
889            return containsMessages(errors);
890        }
891    
892        /**
893         * Delegates to {@link com.mockrunner.mock.web.ActionMockObjectFactory#getMockActionMapping}.
894         * @return the MockActionMapping
895         */
896        public MockActionMapping getMockActionMapping()
897        {
898            return mockFactory.getMockActionMapping();
899        }
900        
901        /**
902         * Delegates to {@link com.mockrunner.mock.web.ActionMockObjectFactory#getActionMapping}.
903         * @return the MockActionMapping
904         */
905        public ActionMapping getActionMapping()
906        {
907            return mockFactory.getActionMapping();
908        }
909    
910        /**
911         * Returns the <code>MockPageContext</code> object.
912         * Delegates to {@link com.mockrunner.mock.web.ActionMockObjectFactory#getMockPageContext}.
913         * @return the MockPageContext
914         */
915        public MockPageContext getMockPageContext()
916        {
917            return mockFactory.getMockPageContext();
918        }
919    
920        /**
921         * Returns the current <code>ActionForward</code>. 
922         * Can be called after {@link #actionPerform} to get 
923         * the <code>ActionForward</code> the action
924         * has returned.
925         * @return the MockActionForward
926         */
927        public MockActionForward getActionForward()
928        {
929            return forward;
930        }
931    
932        /**
933         * Returns the last tested <code>Action</code> object.
934         * @return the <code>Action</code> object
935         */
936        public Action getLastAction()
937        {
938            return actionObj;
939        }
940    
941        /**
942         * Generates a token and sets it to the session and the request.
943         */
944        public void generateValidToken()
945        {
946            String token = String.valueOf(Math.random());
947            mockFactory.getMockSession().setAttribute(Globals.TRANSACTION_TOKEN_KEY, token);
948            addRequestParameter(Constants.TOKEN_KEY, token);
949        }
950        
951        /**
952         * Returns the current <code>ActionForm</code>.
953         * @return the <code>ActionForm</code> object
954         */
955        public ActionForm getActionForm()
956        {
957            return formObj;
958        }
959    
960        /**
961         * Sets the specified <code>ActionForm</code> object as the
962         * current <code>ActionForm</code>.
963         * @param formObj the <code>ActionForm</code> object
964         */
965        public void setActionForm(ActionForm formObj)
966        {
967            this.formObj = formObj;
968        }
969    
970        /**
971         * Creates a new <code>ActionForm</code> object of the specified
972         * type and sets it as the current <code>ActionForm</code>.
973         * @param form the <code>Class</code> of the form
974         */
975        public ActionForm createActionForm(Class form)
976        {
977            try
978            {
979                if (null == form)
980                {
981                    formObj = null;
982                    return null;
983                }
984                formObj = (ActionForm)form.newInstance();
985                return formObj;
986            }
987            catch(Exception exc)
988            {
989                log.error(exc.getMessage(), exc);
990                throw new NestedApplicationException(exc);
991            }
992        }
993        
994        /**
995         * Creates a new <code>DynaActionForm</code> based on the specified
996         * form config and sets it as the current <code>ActionForm</code>.
997         * @param formConfig the <code>FormBeanConfig</code>
998         */
999        public DynaActionForm createDynaActionForm(FormBeanConfig formConfig)
1000        {
1001            try
1002            {
1003                if (null == formConfig)
1004                {
1005                    formObj = null;
1006                    return null;
1007                }
1008                DynaActionFormClass formClass = DynaActionFormClass.createDynaActionFormClass(formConfig);
1009                formObj = (DynaActionForm)formClass.newInstance();
1010                return (DynaActionForm)formObj;
1011            }
1012            catch(Exception exc)
1013            {
1014                log.error(exc.getMessage(), exc);
1015                throw new NestedApplicationException(exc);
1016            }
1017        }
1018    
1019        /**
1020         * Populates the current request parameters to the
1021         * <code>ActionForm</code>. The form will be reset
1022         * before populating if reset is enabled ({@link #setReset}.
1023         * If form validation is enabled (use {@link #setValidate}) the
1024         * form will be validated after populating it and the
1025         * appropriate <code>ActionErrors</code> will be set.
1026         */
1027        public void populateRequestToForm()
1028        {
1029            try
1030            {
1031                handleActionForm();
1032            }
1033            catch(Exception exc)
1034            {
1035                log.error(exc.getMessage(), exc);
1036                throw new NestedApplicationException(exc);
1037            }
1038        }
1039    
1040        /**
1041         * Calls the action of the specified type using
1042         * no <code>ActionForm</code>. Sets the current action
1043         * form to <code>null</code>.
1044         * @param action the <code>Class</code> of the action
1045         * @return the resulting <code>ActionForward</code>
1046         */
1047        public ActionForward actionPerform(Class action)
1048        {
1049            return actionPerform(action, (ActionForm) null);
1050        }
1051        
1052        /**
1053         * Calls the specified action using
1054         * no <code>ActionForm</code>. Sets the current <code>ActionForm</code>
1055         * to <code>null</code>.
1056         * @param action the <code>Action</code>
1057         * @return the resulting <code>ActionForward</code>
1058         */
1059        public ActionForward actionPerform(Action action)
1060        {
1061            return actionPerform(action, (ActionForm) null);
1062        }
1063    
1064        /**
1065         * Calls the action of the specified type using
1066         * the <code>ActionForm</code> of the specified type. 
1067         * Creates the appropriate <code>ActionForm</code>, sets it as the 
1068         * current <code>ActionForm</code> and populates it before calling the action 
1069         * (if populating is disabled, the form will not be populated, use 
1070         * {@link #setDoPopulate}). 
1071         * If form validation is enabled (use {@link #setValidate}) and 
1072         * fails, the action will not be called. In this case,
1073         * the returned  <code>ActionForward</code> is based on the 
1074         * input attribute. (Set it with {@link #setInput}).
1075         * @param action the <code>Class</code> of the action
1076         * @param form the <code>Class</code> of the form
1077         * @return the resulting <code>ActionForward</code>
1078         */
1079        public ActionForward actionPerform(Class action, Class form)
1080        {
1081            createActionForm(form);
1082            return actionPerform(action, formObj);
1083        }
1084        
1085        /**
1086         * Calls the specified action using
1087         * the <code>ActionForm</code> of the specified type. 
1088         * Creates the appropriate <code>ActionForm</code>, sets it as the 
1089         * current <code>ActionForm</code> and populates it before calling the action 
1090         * (if populating is disabled, the form will not be populated, use 
1091         * {@link #setDoPopulate}).
1092         * If form validation is enabled (use {@link #setValidate}) and 
1093         * fails, the action will not be called. In this case,
1094         * the returned  <code>ActionForward</code> is based on the 
1095         * input attribute. (Set it with {@link #setInput}).
1096         * @param action the <code>Action</code>
1097         * @param form the <code>Class</code> of the form
1098         * @return the resulting <code>ActionForward</code>
1099         */
1100        public ActionForward actionPerform(Action action, Class form)
1101        {
1102            createActionForm(form);
1103            return actionPerform(action, formObj);
1104        }
1105    
1106        /**
1107         * Calls the action of the specified type using
1108         * the specified <code>ActionForm</code> object. The form will
1109         * be set as the current <code>ActionForm</code> and
1110         * will be populated before the action is called (if populating is
1111         * disabled, the form will not be populated, use {@link #setDoPopulate}).
1112         * Please note that request parameters will eventually overwrite
1113         * form values. Furthermore the form will be reset
1114         * before populating it. If you do not want that, disable reset 
1115         * using {@link #setReset}. If form validation is enabled 
1116         * (use {@link #setValidate}) and fails, the action will not be 
1117         * called. In this case, the returned <code>ActionForward</code> 
1118         * is based on the input attribute. (Set it with {@link #setInput}).
1119         * @param action the <code>Class</code> of the action
1120         * @param form the <code>ActionForm</code> object
1121         * @return the resulting <code>ActionForward</code>
1122         */
1123        public ActionForward actionPerform(Class action, ActionForm form)
1124        {
1125            Action actionToCall = null;
1126            try
1127            {
1128                actionToCall = (Action)action.newInstance();
1129            }
1130            catch(Exception exc)
1131            {
1132                throw new NestedApplicationException(exc);
1133            }
1134            return actionPerform(actionToCall, form);
1135        }
1136        
1137        /**
1138         * Calls the specified action using
1139         * the specified <code>ActionForm</code> object. The form will
1140         * be set as the current <code>ActionForm</code> and
1141         * will be populated before the action is called (if populating is
1142         * disabled, the form will not be populated, use {@link #setDoPopulate}).
1143         * Please note that request parameters will eventually overwrite
1144         * form values. Furthermore the form will be reset
1145         * before populating it. If you do not want that, disable reset 
1146         * using {@link #setReset}. If form validation is enabled 
1147         * (use {@link #setValidate}) and fails, the action will not be 
1148         * called. In this case, the returned <code>ActionForward</code> 
1149         * is based on the input attribute. (Set it with {@link #setInput}).
1150         * @param action the <code>Action</code>
1151         * @param form the <code>ActionForm</code> object
1152         * @return the resulting <code>ActionForward</code>
1153         */
1154        public ActionForward actionPerform(Action action, ActionForm form)
1155        {
1156            try
1157            {
1158                actionObj = action;
1159                actionObj.setServlet(mockFactory.getMockActionServlet());
1160                formObj = form;
1161                setActionErrors(null);
1162                getActionMapping().setType(action.getClass().getName());
1163                if(null != formObj)
1164                {
1165                    handleActionForm();
1166                }
1167                if(!hasActionErrors())
1168                {
1169                    ActionForward currentForward = null;
1170                    try
1171                    {
1172                        currentForward = (ActionForward)actionObj.execute(getActionMapping(), formObj, mockFactory.getWrappedRequest(), mockFactory.getWrappedResponse());
1173                    } 
1174                    catch(Exception exc)
1175                    {
1176                        ExceptionHandlerConfig handler = findExceptionHandler(exc);
1177                        if(null == handler)
1178                        {
1179                            throw exc;
1180                        }
1181                        else
1182                        {
1183                            Object result = handler.handle(exc, getActionMapping(), formObj, mockFactory.getWrappedRequest(), mockFactory.getWrappedResponse());
1184                            if(result instanceof ActionForward)
1185                            {
1186                                currentForward = (ActionForward)result;
1187                            }
1188                        }
1189                    }
1190                    setResult(currentForward);
1191                }
1192                else
1193                {
1194                    setResult(getActionMapping().getInputForward());
1195                }
1196            }
1197            catch(Exception exc)
1198            {
1199                throw new NestedApplicationException(exc);
1200            }
1201            return getActionForward();
1202        }
1203        
1204        /**
1205         * Returns the HTML output as a string (if the action creates HTML output). 
1206         * Flushes the output before returning it.
1207         * @return the output
1208         */
1209        public String getOutput()
1210        {
1211            try
1212            {
1213                mockFactory.getMockResponse().getWriter().flush();    
1214            }
1215            catch(Exception exc)
1216            {
1217                log.error(exc.getMessage(), exc);
1218            }
1219            return mockFactory.getMockResponse().getOutputStreamContent();
1220        }
1221    
1222        private void setResult(ActionForward currentForward)
1223        {
1224            if (null == currentForward)
1225            {
1226                forward = null;
1227            }
1228            else
1229            {
1230                forward = new MockActionForward(currentForward);
1231            }
1232        }
1233        
1234        private ExceptionHandlerConfig findExceptionHandler(Exception exc)
1235        {
1236            for(int ii = 0; ii < exceptionHandlers.size(); ii++)
1237            {
1238                ExceptionHandlerConfig next = (ExceptionHandlerConfig)exceptionHandlers.get(ii);
1239                if(next.canHandle(exc)) return next;
1240            }
1241            return null;
1242        }
1243    
1244        private void handleActionForm() throws Exception
1245        {
1246            if(reset) getActionForm().reset(getActionMapping(), mockFactory.getWrappedRequest());
1247            if(doPopulate) populateMockRequest();
1248            formObj.setServlet(mockFactory.getMockActionServlet());
1249            if(getActionMapping().getValidate())
1250            {
1251                ActionMessages errors = formObj.validate(getActionMapping(), mockFactory.getWrappedRequest());
1252                if (containsMessages(errors))
1253                {
1254                    mockFactory.getWrappedRequest().setAttribute(errorAttributeKey, errors);
1255                }
1256            }
1257        }
1258    
1259        private void populateMockRequest() throws Exception
1260        {
1261            BeanUtils.populate(getActionForm(), mockFactory.getWrappedRequest().getParameterMap());
1262        }
1263       
1264        private boolean containsMessages(ActionMessages messages)
1265        {
1266            return (null != messages) && (messages.size() > 0);
1267        }
1268    }