001    package com.mockrunner.tag;
002    
003    import java.io.IOException;
004    import java.util.HashMap;
005    import java.util.Map;
006    
007    import javax.servlet.jsp.JspException;
008    import javax.servlet.jsp.tagext.JspTag;
009    import javax.servlet.jsp.tagext.Tag;
010    import javax.servlet.jsp.tagext.TagSupport;
011    
012    import org.apache.commons.logging.Log;
013    import org.apache.commons.logging.LogFactory;
014    
015    import com.mockrunner.base.HTMLOutputModule;
016    import com.mockrunner.base.NestedApplicationException;
017    import com.mockrunner.mock.web.MockJspWriter;
018    import com.mockrunner.mock.web.MockPageContext;
019    import com.mockrunner.mock.web.WebMockObjectFactory;
020    
021    /**
022     * Module for custom tag tests. Simulates the container by
023     * performing the tag lifecycle.
024     */
025    public class TagTestModule extends HTMLOutputModule
026    {
027        private final static Log log = LogFactory.getLog(TagTestModule.class);
028        private WebMockObjectFactory mockFactory;
029        private NestedTag tag;
030    
031        public TagTestModule(WebMockObjectFactory mockFactory)
032        {
033            super(mockFactory);
034            this.mockFactory = mockFactory;
035        }
036        
037        /**
038         * Creates a tag. Internally a {@link NestedTag}
039         * is created but the wrapped tag is returned. If you
040         * simply want to test the output of the tag without 
041         * nesting other tags, you do not have to care about the
042         * {@link NestedTag}, just use the returned instance.
043         * An empty attribute <code>Map</code> will be used for
044         * the tag.
045         * @param tagClass the class of the tag
046         * @return instance of <code>TagSupport</code> or <code>BodyTagSupport</code>
047         * @throws <code>RuntimeException</code>, if the created tag
048         *         is not an instance of <code>TagSupport</code>
049         */
050        public TagSupport createTag(Class tagClass)
051        {
052            if(!TagSupport.class.isAssignableFrom(tagClass))
053            {
054                throw new IllegalArgumentException("specified class is not an instance of TagSupport. Please use createWrappedTag");
055            }
056            return createTag(tagClass, new HashMap());
057        }
058        
059        /**
060         * Creates a tag. Internally a {@link NestedTag}
061         * is created but the wrapped tag is returned. If you
062         * simply want to test the output of the tag without 
063         * nesting other tags, you do not have to care about the
064         * {@link NestedTag}, just use the returned instance.
065         * The attributes <code>Map</code> contains the attributes
066         * of this tag (<i>propertyname</i> maps to <i>propertyvalue</i>).
067         * The attributes are populated (i.e. the tags setters are called)
068         * during the lifecycle or with an explicit call of
069         * {@link #populateAttributes}.
070         * @param tagClass the class of the tag
071         * @param attributes the attribute map
072         * @return instance of <code>TagSupport</code> or <code>BodyTagSupport</code>
073         * @throws <code>RuntimeException</code>, if the created tag
074         *         is not an instance of <code>TagSupport</code>
075         */
076        public TagSupport createTag(Class tagClass, Map attributes)
077        {
078            if(!TagSupport.class.isAssignableFrom(tagClass))
079            {
080                throw new IllegalArgumentException("specified class is not an instance of TagSupport. Please use createWrappedTag");
081            }
082            return createNestedTag(tagClass, attributes).getTag();
083        }
084        
085        /**
086         * Creates a tag. Internally a {@link NestedTag}
087         * is created but the wrapped tag is returned. If you
088         * simply want to test the output of the tag without 
089         * nesting other tags, you do not have to care about the
090         * {@link NestedTag}, just use the returned instance.
091         * An empty attribute <code>Map</code> will be used for
092         * the tag.
093         * This method can be used for all kind of tags. The tag
094         * class does not need to be a subclass of <code>TagSupport</code>.
095         * @param tagClass the class of the tag
096         * @return instance of <code>JspTag</code>
097         */
098        public JspTag createWrappedTag(Class tagClass)
099        {
100            return createWrappedTag(tagClass, new HashMap());
101        }
102        
103        /**
104         * Creates a tag. Internally a {@link NestedTag}
105         * is created but the wrapped tag is returned. If you
106         * simply want to test the output of the tag without 
107         * nesting other tags, you do not have to care about the
108         * {@link NestedTag}, just use the returned instance.
109         * The attributes <code>Map</code> contains the attributes
110         * of this tag (<i>propertyname</i> maps to <i>propertyvalue</i>).
111         * The attributes are populated (i.e. the tags setters are called)
112         * during the lifecycle or with an explicit call of
113         * {@link #populateAttributes}.
114         * This method can be used for all kind of tags. The tag
115         * class does not need to be a subclass of <code>TagSupport</code>.
116         * @param tagClass the class of the tag
117         * @param attributes the attribute map
118         * @return instance of <code>JspTag</code>
119         */
120        public JspTag createWrappedTag(Class tagClass, Map attributes)
121        {
122            return createNestedTag(tagClass, attributes).getWrappedTag();
123        }
124        
125        /**
126         * Creates a {@link NestedTag} and returns it. You can
127         * add child tags or body blocks to the {@link NestedTag}.
128         * Use {@link #getTag} to get the wrapped tag.
129         * An empty attribute <code>Map</code> will be used for
130         * the tag.
131         * @param tagClass the class of the tag
132         * @return instance of {@link NestedStandardTag}, {@link NestedBodyTag} or 
133         *                     {@link NestedSimpleTag}
134         */
135        public NestedTag createNestedTag(Class tagClass)
136        {
137            return createNestedTag(tagClass, new HashMap());
138        }
139        
140        /**
141         * Creates a {@link NestedTag} and returns it. You can
142         * add child tags or body blocks to the {@link NestedTag}.
143         * Use {@link #getTag} to get the wrapped tag.
144         * The attributes <code>Map</code> contains the attributes
145         * of this tag (<i>propertyname</i> maps to <i>propertyvalue</i>).
146         * The attributes are populated (i.e. the tags setters are called)
147         * during the lifecycle or with an explicit call of
148         * {@link #populateAttributes}.
149         * @param tagClass the class of the tag
150         * @param attributes the attribute map
151         * @return instance of {@link NestedStandardTag}, {@link NestedBodyTag} or 
152         *                     {@link NestedSimpleTag}
153         */
154        public NestedTag createNestedTag(Class tagClass, Map attributes)
155        {
156            try
157            {
158                this.tag = (NestedTag)TagUtil.createNestedTagInstance(tagClass, getMockPageContext(), attributes);
159                return this.tag;
160            }
161            catch(IllegalArgumentException exc)
162            {
163                throw exc;
164            }
165            catch(Exception exc)
166            {
167                log.error(exc.getMessage(), exc);
168                throw new NestedApplicationException(exc);
169            }
170        }
171        
172        /**
173         * Creates a {@link NestedTag} and returns it. You can
174         * add child tags or body blocks to the {@link NestedTag}.
175         * Use {@link #getTag} to get the wrapped tag.
176         * An empty attribute <code>Map</code> will be used for
177         * the tag.
178         * @param tag the tag
179         * @return instance of {@link NestedStandardTag} or {@link NestedBodyTag}
180         */
181        public NestedTag setTag(TagSupport tag)
182        {
183            return setTag(tag, new HashMap());
184        }
185        
186        /**
187         * Creates a {@link NestedTag} and returns it. You can
188         * add child tags or body blocks to the {@link NestedTag}.
189         * Use {@link #getTag} to get the wrapped tag.
190         * The attributes <code>Map</code> contains the attributes
191         * of this tag (<i>propertyname</i> maps to <i>propertyvalue</i>).
192         * The attributes are populated (i.e. the tags setters are called)
193         * during the lifecycle or with an explicit call of
194         * {@link #populateAttributes}.
195         * @param tag the tag
196         * @param attributes the attribute map
197         * @return instance of {@link NestedStandardTag} or {@link NestedBodyTag}
198         */
199        public NestedTag setTag(TagSupport tag, Map attributes)
200        {
201            try
202            {
203                this.tag = (NestedTag)TagUtil.createNestedTagInstance(tag, getMockPageContext(), attributes);
204                return this.tag;
205            }
206            catch(IllegalArgumentException exc)
207            {
208                throw exc;
209            }
210            catch(Exception exc)
211            {
212                log.error(exc.getMessage(), exc);
213                throw new NestedApplicationException(exc);
214            }
215        }
216        
217        /**
218         * Creates a {@link NestedTag} and returns it. You can
219         * add child tags or body blocks to the {@link NestedTag}.
220         * Use {@link #getTag} to get the wrapped tag.
221         * An empty attribute <code>Map</code> will be used for
222         * the tag.
223         * This method can be used for all kind of tags. The tag
224         * class does not need to be a subclass of <code>TagSupport</code>.
225         * @param tag the tag
226         * @return instance of {@link NestedStandardTag}, {@link NestedBodyTag} or 
227         *                     {@link NestedSimpleTag}
228         */
229        public NestedTag setTag(JspTag tag)
230        {
231            return setTag(tag, new HashMap());
232        }
233        
234        /**
235         * Creates a {@link NestedTag} and returns it. You can
236         * add child tags or body blocks to the {@link NestedTag}.
237         * Use {@link #getTag} to get the wrapped tag.
238         * The attributes <code>Map</code> contains the attributes
239         * of this tag (<i>propertyname</i> maps to <i>propertyvalue</i>).
240         * The attributes are populated (i.e. the tags setters are called)
241         * during the lifecycle or with an explicit call of
242         * {@link #populateAttributes}.
243         * This method can be used for all kind of tags. The tag
244         * class does not need to be a subclass of <code>TagSupport</code>.
245         * @param tag the tag
246         * @param attributes the attribute map
247         * @return instance of {@link NestedStandardTag}, {@link NestedBodyTag} or 
248         *                     {@link NestedSimpleTag}
249         */
250        public NestedTag setTag(JspTag tag, Map attributes)
251        {
252            try
253            {
254                this.tag = (NestedTag)TagUtil.createNestedTagInstance(tag, getMockPageContext(), attributes);
255                return this.tag;
256            }
257            catch(IllegalArgumentException exc)
258            {
259                throw exc;
260            }
261            catch(Exception exc)
262            {
263                log.error(exc.getMessage(), exc);
264                throw new NestedApplicationException(exc);
265            }
266        }
267        
268        /**
269         * Specify if the <code>release</code> method should be called
270         * after processing the tag lifecycle. Delegates to {@link NestedTag#setDoRelease}
271         * Defaults to <code>false</code>. It's the container behaviour to call 
272         * <code>release</code>, but it's usually not necessary in the tests, 
273         * because the tag instances are not reused during a test run.
274         * @param doRelease should release be called
275         */
276        public void setDoRelease(boolean doRelease)
277        {
278            if(null == tag)
279            {
280                throw new RuntimeException("Not current tag set");
281            }
282            tag.setDoRelease(doRelease);
283        }
284        
285        /**
286         * Specify if the <code>release</code> method should be called
287         * after processing the tag lifecycle. Delegates to {@link NestedTag#setDoReleaseRecursive}
288         * Defaults to <code>false</code>. It's the container behaviour to call 
289         * <code>release</code>, but it's usually not necessary in the tests, 
290         * because the tag instances are not reused during a test run.
291         * @param doRelease should release be called
292         */
293        public void setDoReleaseRecursive(boolean doRelease)
294        {
295            if(null == tag)
296            {
297                throw new RuntimeException("Not current tag set");
298            }
299            tag.setDoReleaseRecursive(doRelease);
300        }
301        
302        /**
303         * Populates the attributes of the underlying tag by
304         * calling {@link NestedTag#populateAttributes}. The setters
305         * of the tag are called. Please note that child tags are not
306         * populated. This is done during the lifecycle.
307         */
308        public void populateAttributes()
309        {
310            if(null == tag)
311            {
312                throw new RuntimeException("Not current tag set");
313            }
314            tag.populateAttributes();
315        }
316        
317        /**
318         * Sets the body of the tag as a static string. Please
319         * note that all childs of the underlying {@link NestedTag}
320         * are deleted and the static content is set. If you want
321         * to use nested tags, please use the method {@link NestedTag#addTextChild}
322         * to set static content.
323         * @param body the static body content
324         */
325        public void setBody(String body)
326        {
327            if(null == tag)
328            {
329                throw new RuntimeException("Not current tag set");
330            }
331            tag.removeChilds();
332            tag.addTextChild(body);
333        }
334        
335        /**
336         * Returns the current wrapped tag.
337         * @return instance of <code>TagSupport</code> or <code>BodyTagSupport</code>
338         * @throws <code>RuntimeException</code>, if the wrapped tag
339         *         is not an instance of <code>TagSupport</code>
340         */
341        public TagSupport getTag()
342        {
343            if(null == tag) return null;
344            return tag.getTag();
345        }
346        
347        /**
348         * Returns the current wrapped tag.
349         * This method can be used for all kind of tags. The tag
350         * class does not need to be a subclass of <code>TagSupport</code>.
351         * @return instance of <code>JspTag</code>
352         */
353        public JspTag getWrappedTag()
354        {
355            if(null == tag) return null;
356            return tag.getWrappedTag();
357        }
358        
359        /**
360         * Returns the current nested tag. You can
361         * add child tags or body blocks to the {@link NestedTag}.
362         * Use {@link #getTag} to get the wrapped tag.
363         * @return instance of {@link NestedStandardTag} or {@link NestedBodyTag}
364         */
365        public NestedTag getNestedTag()
366        {
367            return tag;
368        }
369        
370        /**
371         * Returns the <code>MockPageContext</code> object.
372         * Delegates to {@link com.mockrunner.mock.web.WebMockObjectFactory#getMockPageContext}.
373         * @return the MockPageContext
374         */
375        public MockPageContext getMockPageContext()
376        {
377            return mockFactory.getMockPageContext();
378        }
379        
380        /**
381         * Calls the <code>doStartTag</code> method of the current tag.
382         * @throws <code>RuntimeException</code>, if the tag
383         *         is not a simple tag
384         */
385        public void doTag()
386        {
387            if(null == tag)
388            {
389                throw new RuntimeException("No current tag set");
390            }
391            if(!isSimpleTag()) 
392            {
393                throw new RuntimeException("Tag is no simple tag");
394            }
395            try
396            {
397                ((NestedSimpleTag)tag).doTag();
398            }
399            catch(Exception exc)
400            {
401                log.error(exc.getMessage(), exc);
402                throw new NestedApplicationException(exc);
403            }
404        }
405        
406        /**
407         * Calls the <code>doStartTag</code> method of the current tag.
408         * @return the result of <code>doStartTag</code>
409         * @throws <code>RuntimeException</code>, if the tag
410         *         is a simple tag
411         */
412        public int doStartTag()
413        {
414            if(null == tag)
415            {
416                throw new RuntimeException("No current tag set");
417            }
418            if(isSimpleTag()) 
419            {
420                throw new RuntimeException("Cannot call doStartTag() on simple tags");
421            }
422            try
423            {
424                return ((Tag)tag).doStartTag();
425            }
426            catch(JspException exc)
427            {
428                log.error(exc.getMessage(), exc);
429                throw new NestedApplicationException(exc);
430            }
431        }
432        
433        /**
434         * Calls the <code>doEndTag</code> method of the current tag.
435         * @return the result of <code>doEndTag</code>
436         * @throws <code>RuntimeException</code>, if the tag
437         *         is a simple tag
438         */
439        public int doEndTag()
440        {
441            if(null == tag)
442            {
443                throw new RuntimeException("No current tag set");
444            }
445            if(isSimpleTag()) 
446            {
447                throw new RuntimeException("Cannot call doEndTag() on simple tags");
448            }
449            try
450            {
451                return ((Tag)tag).doEndTag();
452            }
453            catch(JspException exc)
454            {
455                log.error(exc.getMessage(), exc);
456                throw new NestedApplicationException(exc);
457            }
458        }
459        
460        /**
461         * Calls the <code>doInitBody</code> method of the current tag.
462         * @throws RuntimeException if the current tag is no body tag
463         * @throws <code>RuntimeException</code>, if the tag
464         *         is a simple tag
465         */
466        public void doInitBody()
467        {
468            if(null == tag)
469            {
470                throw new RuntimeException("No current tag set");
471            }
472            if(!isBodyTag()) 
473            {
474                throw new RuntimeException("Tag is no body tag");
475            }
476            try
477            {
478                NestedBodyTag bodyTag = (NestedBodyTag)tag;
479                bodyTag.doInitBody();
480            }
481            catch(JspException exc)
482            {
483                log.error(exc.getMessage(), exc);
484                throw new NestedApplicationException(exc);
485            }
486        }
487    
488        /**
489         * Calls the <code>doAfterBody</code> method of the current tag.
490         * @return the result of <code>doAfterBody</code>
491         * @throws <code>RuntimeException</code>, if the tag
492         *         is a simple tag
493         */
494        public int doAfterBody()
495        {
496            if(null == tag)
497            {
498                throw new RuntimeException("No current tag set");
499            }
500            if(isSimpleTag()) 
501            {
502                throw new RuntimeException("Cannot call doAfterBody() on simple tags");
503            }
504            try
505            {
506                return ((TagSupport)tag).doAfterBody();
507            }
508            catch(JspException exc)
509            {
510                log.error(exc.getMessage(), exc);
511                throw new NestedApplicationException(exc);
512            }
513        }
514    
515        /**
516         * Calls the <code>release</code> method of the current tag.
517         * @throws <code>RuntimeException</code>, if the tag
518         *         is a simple tag
519         */
520        public void release()
521        {
522            if(isSimpleTag())
523            {
524                throw new RuntimeException("Cannot call release() on simple tags");
525            }
526            ((Tag)tag).release();
527        }
528        
529        /**
530         * Performs the tags lifecycle by calling {@link NestedTag#doLifecycle}.
531         * All <code>doBody</code> and <code>doTag</code> methods are called as 
532         * in the real web container. The evaluation of the body is simulated 
533         * by performing the lifecycle recursively for all childs of the 
534         * {@link NestedTag}.
535         * @return the result of the final <code>doEndTag</code> call or -1 in
536         *         the case of a simple tag
537         */
538        public int processTagLifecycle()
539        {
540            if(null == tag)
541            {
542                throw new RuntimeException("No current tag set");
543            }
544            try
545            {
546                return ((NestedTag)tag).doLifecycle();
547            }
548            catch(JspException exc)
549            {
550                log.error(exc.getMessage(), exc);
551                throw new NestedApplicationException(exc);
552            }
553        }
554        
555        /**
556         * Resets the output buffer.
557         */
558        public void clearOutput()
559        {
560            MockJspWriter writer = (MockJspWriter)mockFactory.getMockPageContext().getOut();
561            try
562            {
563                writer.clearBuffer();
564            }
565            catch(IOException exc)
566            {
567                log.error(exc.getMessage(), exc);
568                throw new NestedApplicationException(exc);
569            }
570        }
571        
572        /**
573         * Gets the output data the current tag has rendered. Makes only sense
574         * after calling at least {@link #doStartTag} or {@link #processTagLifecycle}
575         * @return the output data
576         */
577        public String getOutput()
578        {
579            MockJspWriter writer = (MockJspWriter)mockFactory.getMockPageContext().getOut();
580            return writer.getOutputAsString();
581        }
582    
583        private boolean isBodyTag()
584        {
585            return (tag instanceof NestedBodyTag);
586        }
587        
588        private boolean isSimpleTag()
589        {
590            return (tag instanceof NestedSimpleTag);
591        }
592    }