1 /*
2 * ========================================================================
3 *
4 * Licensed to the Apache Software Foundation (ASF) under one or more
5 * contributor license agreements. See the NOTICE file distributed with
6 * this work for additional information regarding copyright ownership.
7 * The ASF licenses this file to You under the Apache License, Version 2.0
8 * (the "License"); you may not use this file except in compliance with
9 * the License. You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * ========================================================================
20 */
21 package org.apache.cactus.extension.jsp;
22
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.Iterator;
26 import java.util.List;
27
28 import junit.framework.Assert;
29
30 import javax.servlet.jsp.JspException;
31 import javax.servlet.jsp.PageContext;
32 import javax.servlet.jsp.tagext.BodyContent;
33 import javax.servlet.jsp.tagext.BodyTag;
34 import javax.servlet.jsp.tagext.IterationTag;
35 import javax.servlet.jsp.tagext.Tag;
36 import javax.servlet.jsp.tagext.TryCatchFinally;
37
38 import org.apache.commons.logging.Log;
39 import org.apache.commons.logging.LogFactory;
40
41 /**
42 * Convenience class that supports the testing of JSP tag by managing the tag's
43 * lifecycle as required by the JSP specification.
44 *
45 * <p>
46 * This class is basically a stub implementation of the tag management
47 * facilities that an actual JSP container would provide. The implementation
48 * attempts to follow the specification as closely as possible, but the tag
49 * handling functionality of real JSP implementations may vary in some
50 * details.
51 * </p>
52 *
53 * <p>
54 * Although this class works quite well when used in the test methods of a
55 * {@link org.apache.cactus.JspTestCase JspTestCase}, it can also safely be
56 * used outside of the Cactus testing framework, for example when following
57 * a mock objects approach.
58 * </p>
59 *
60 * <h4>Testing Simple Tags</h4>
61 * <p>
62 * This is how you would use this class when testing the
63 * <code><c:set></code>-tag of the JSTL reference implementation:
64 * <blockquote><pre>
65 SetTag tag = new SetTag();
66 JspTagLifecycle lifecycle = new JspTagLifecycle(pageContext, tag);
67 tag.setVar("name");
68 tag.setValue("value");
69 lifecycle.invoke();
70 assertEquals("value", pageContext.findAttribute("name"));</pre>
71 * </blockquote>
72 * The order is important:
73 * <ol>
74 * <li>
75 * Instantiation of the tag under test
76 * </li>
77 * <li>
78 * Instantiation of the lifecycle helper, passing in the page context and
79 * the tag instance
80 * </li>
81 * <li>
82 * Set the tag's attributes
83 * </li>
84 * <li>
85 * Start the tag's lifecycle by calling
86 * {@link #invoke() JspTagLifecycle.invoke()}
87 * </li>
88 * <li>
89 * Make assertions
90 * </li>
91 * </ol>
92 * </p>
93 *
94 * <h4>Adding Expectations to the Lifecycle</h4>
95 * <p>
96 * <code>JspTagLifecycle</code> features a couple of methods that let you
97 * easily add expectations about the tag's lifecycle to the test. For example,
98 * the method {@link #expectBodySkipped expectBodySkipped()} can be used to
99 * verify that tag's body is not evaluated under the conditions set up by the
100 * test:
101 * <pre>
102 * IfTag tag = new IfTag();
103 * JspTagLifecycle lifecycle = new JspTagLifecycle(pageContext, tag);
104 * tag.setTest("false");
105 * lifecycle.expectBodySkipped();
106 * lifecycle.invoke();</pre>
107 * </p>
108 * <p>
109 * An example of a more sophisticated expectationion is the
110 * {@link #expectScopedVariableExposed(String, Object[])}
111 * method, which can verify that a specific scoped variable gets exposed in
112 * the body of the tag, and that the exposed variable has a specific value in
113 * each iteration step:
114 * <pre>
115 * ForEachTag tag = new ForEachTag();
116 * JspTagLifecycle lifecycle = new JspTagLifecycle(pageContext, tag);
117 * tag.setVar("item");
118 * tag.setItems("One,Two,Three");
119 * lifecycle.expectBodyEvaluated(3);
120 * lifecycle.expectScopedVariableExposed(
121 * "item", new Object[] {"One", "Two", "Three"});
122 * lifecycle.invoke();</pre>
123 * </p>
124 *
125 * <h4>Custom Expectations</h4>
126 * <p>
127 * In some cases, using the expectations offered by
128 * <code>JspTagLifecycle</code> does not suffice. In such cases, you need to
129 * use custom expectations. You can add custom expectations by creating a
130 * concrete subclass of the {@link JspTagLifecycle.Interceptor Interceptor}
131 * class, and adding it to the list of the tag lifecycles interceptors through
132 * {@link JspTagLifecycle#addInterceptor addInterceptor()}:
133 * <pre>
134 * ForEachTag tag = new ForEachTag();
135 * JspTagLifecycle lifecycle = new JspTagLifecycle(pageContext, tag);
136 * tag.setVarStatus("status");
137 * tag.setBegin("0");
138 * tag.setEnd("2");
139 * lifecycle.addInterceptor(new JspTagLifecycle.Interceptor() {
140 * public void evalBody(int theIteration, BodyContent theBody) {
141 * LoopTagStatus status = (LoopTagStatus)
142 * pageContext.findAttribute("status");
143 * assertNotNull(status);
144 * if (theIteration == 0) {
145 * assertTrue(status.isFirst());
146 * assertFalse(status.isLast());
147 * }
148 * else if (theIteration == 1) {
149 * assertFalse(status.isFirst());
150 * assertFalse(status.isLast());
151 * }
152 * else if (theIteration == 2) {
153 * assertFalse(status.isFirst());
154 * assertTrue(status.isLast());
155 * }
156 * }
157 * });
158 * lifecycle.invoke();</pre>
159 * </p>
160 *
161 * <h4>Specifying Nested Content</h4>
162 * <p>
163 * <code>JspTagLifecycle</code> let's you add nested tempate text as well as
164 * nested tags to the tag under test. The most important use of this feature
165 * is testing of collaboration between tags, but it also allows you to easily
166 * check whether a tag correctly handles its body content.
167 * </p>
168 * <p>
169 * The following example demonstrates how to add nested template text to the
170 * tag, and how to assert that the body was written to the HTTP response on
171 * the client side:
172 * <pre>
173 * public void testOutTagDefaultBody() throws JspException, IOException {
174 * OutTag tag = new OutTag();
175 * JspTagLifecycle lifecycle = new JspTagLifecycle(pageContext, tag);
176 * tag.setValue(null);
177 * lifecycle.addNestedText("Default");
178 * lifecycle.expectBodyEvaluated();
179 * lifecycle.invoke();
180 * }
181 * public void endOutTagDefaultBody(WebResponse theResponse) {
182 * String output = theResponse.getText();
183 * assertEquals("Default", output);
184 * }</pre>
185 * </p>
186 * <p>
187 * In sophisticated tag libraries, there will be many cases where tags need
188 * to collaborate with each other in some way. This is usually done by nesting
189 * such tags within eachother. <code>JspTagLifecycle</code> supports such
190 * scenarios by allowing you to add nested tags to an existing tag lifecycle.
191 * The nested tags can than be decorated with expectations themselves, as you
192 * can see in the following example:
193 * <pre>
194 * ChooseTag chooseTag = new ChooseTag();
195 * JspTagLifecycle chooseLifecycle =
196 * new JspTagLifecycle(pageContext, chooseTag);
197 * WhenTag whenTag = new WhenTag();
198 * JspTagLifecycle whenLifecycle =
199 * chooseLifecycle.addNestedTag(whenTag);
200 * whenTag.setTest("false");
201 * whenLifecycle.expectBodySkipped();
202 * OtherwiseTag otherwiseTag = new OtherwiseTag();
203 * JspTagLifecycle otherwiseLifecycle =
204 * chooseLifecycle.addNestedTag(otherwiseTag);
205 * otherwiseLifecycle.expectBodyEvaluated();
206 * chooseLifecycle.invoke();</pre>
207 * The code above creates a constellation of tags equivalent to the following
208 * JSP fragment:
209 * <pre>
210 * <c:choose>
211 * <c:when test='false'>
212 * <%-- body content not significant for the test --%>
213 * </c:when>
214 * <c:otherwise>
215 * <%-- body content not significant for the test --%>
216 * </c:otherwise>
217 * </c:choose></pre>
218 * </p>
219 *
220 * @since Cactus 1.5
221 *
222 * @version $Id: JspTagLifecycle.java 238991 2004-05-22 11:34:50Z vmassol $
223 * @see org.apache.cactus.JspTestCase
224 */
225 public final class JspTagLifecycle
226 {
227 // Inner Classes -----------------------------------------------------------
228
229 /**
230 * Abstract class for intercepting the tag lifecycle. You can override any
231 * of the methods to insert expectations about the tag's behaviour while it
232 * is being executed.
233 *
234 * @since Cactus 1.5
235 */
236 public abstract static class Interceptor
237 {
238
239 /**
240 * Method called when the body of the tag would be evaluated. Can be
241 * used in specific test cases to perform assertions.
242 *
243 * Please note that if you're testing a <code>BodyTag</code>, you
244 * should not write content to the
245 * {@link org.apache.cactus.JspTestCase#out} instance variable while
246 * the body is being evaluated. This is because the actual implicit
247 * object <code>out</code> in JSP pages gets replaced by the current
248 * nested <code>BodyContent</code>, whereas in <code>JspTestCase</code>
249 * the <code>out</code> variable always refers to the top level
250 * <code>JspWriter</code>. Instead, simply use the
251 * <code>BodyContent</code> parameter passed into the
252 * {@link JspTagLifecycle.Interceptor#evalBody evalBody()} method or
253 * the <code>JspWriter</code> retrieved by a call to
254 * {javax.servlet.jsp.PageContext#getOut pageContext.getOut()}.
255 *
256 * @param theIteration The number of times the body has been evaluated
257 * @param theBody The body content, or <tt>null</tt> if the tag isn't a
258 * <tt>BodyTag</tt>
259 * @throws JspException If thrown by a nested tag
260 * @throws IOException If an error occurs when reading or writing the
261 * body content
262 */
263 public void evalBody(int theIteration, BodyContent theBody)
264 throws JspException, IOException
265 {
266 // default implementation does nothing
267 }
268
269 /**
270 * Method called when the body of the tag would be skipped. Can be used
271 * in specific test cases to perform assertions.
272 */
273 public void skipBody()
274 {
275 // default implementation does nothing
276 }
277
278 }
279
280 /**
281 * A specialized interceptor that verifies that the tag's body is evaluated
282 * at least once.
283 *
284 * @since Cactus 1.5
285 */
286 private static class ExpectBodyEvaluatedInterceptor
287 extends Interceptor
288 {
289 /**
290 * The actual number of times the tag's body has been evaluated.
291 */
292 private int actualNumIterations;
293
294 /**
295 * The number of times the tag's body is expected to be evaluated.
296 */
297 private int expectedNumIterations;
298
299 /**
300 * Constructor.
301 *
302 * @param theNumIterations The number of iterations expected
303 */
304 public ExpectBodyEvaluatedInterceptor(int theNumIterations)
305 {
306 this.expectedNumIterations = theNumIterations;
307 }
308
309 /**
310 * Overridden to assert that the body doesn't get evaluated more often
311 * than expected.
312 *
313 * {@inheritDoc}
314 * @see JspTagLifecycle.Interceptor#evalBody(int,BodyContent)
315 */
316 public void evalBody(int theIteration, BodyContent theBody)
317 {
318 actualNumIterations++;
319 if (actualNumIterations > expectedNumIterations)
320 {
321 Assert.fail("Expected " + expectedNumIterations
322 + " iterations, but was " + actualNumIterations);
323 }
324 }
325
326 /**
327 * Overridden to assert that the body got evaluated as often as
328 * expected.
329 */
330 public void skipBody()
331 {
332 if (actualNumIterations < expectedNumIterations)
333 {
334 Assert.fail("Expected " + expectedNumIterations
335 + " iterations, but was " + actualNumIterations);
336 }
337 }
338 }
339
340 /**
341 * A specialized interceptor that asserts that the tag's body is skipped.
342 *
343 * @since Cactus 1.5
344 */
345 private static class ExpectBodySkippedInterceptor
346 extends Interceptor
347 {
348 /**
349 * Overridden to assert that the body doesn't get evaluated.
350 *
351 * {@inheritDoc}
352 * @see JspTagLifecycle.Interceptor#evalBody(int,BodyContent)
353 */
354 public void evalBody(int theIteration, BodyContent theBody)
355 {
356 Assert.fail("Tag body should have been skipped");
357 }
358 }
359
360 /**
361 * A specialized interceptor that checks whether a specific scoped variable
362 * is exposed in the body of the tag with a specific value.
363 *
364 * @since Cactus 1.5
365 */
366 private class ExpectScopedVariableExposedInterceptor
367 extends Interceptor
368 {
369 /**
370 * The name of the scoped variable.
371 */
372 private String name;
373
374 /**
375 * The list of expected values of the variable.
376 */
377 private Object[] expectedValues;
378
379 /**
380 * The scope in which the variable is stored.
381 */
382 private int scope;
383
384 /**
385 * Constructor.
386 *
387 * @param theName The name of the scoped variable to check for
388 * @param theExpectedValues An array containing the expected values,
389 * one item for every iteration step
390 * @param theScope The scope to search for the scoped variable
391 */
392 public ExpectScopedVariableExposedInterceptor(String theName,
393 Object[] theExpectedValues, int theScope)
394 {
395 this.name = theName;
396 this.expectedValues = theExpectedValues;
397 this.scope = theScope;
398 }
399
400 /**
401 * Overridden to assert that the scoped variable is exposed as expected.
402 *
403 * {@inheritDoc}
404 * @see JspTagLifecycle.Interceptor#evalBody(int,BodyContent)
405 */
406 public void evalBody(int theIteration, BodyContent theBody)
407 {
408 Assert.assertEquals(expectedValues[theIteration],
409 pageContext.getAttribute(name, scope));
410 }
411 }
412
413 /**
414 * A specialized interceptor that invokes the lifecycle of a nested tag.
415 *
416 * @since Cactus 1.5
417 */
418 private class NestedTagInterceptor
419 extends Interceptor
420 {
421 /**
422 * The lifecycle object of the nested tag.
423 */
424 private JspTagLifecycle lifecycle;
425
426 /**
427 * Constructor.
428 *
429 * @param theLifecycle The lifecycle instance associated with the nested
430 * tag
431 */
432 public NestedTagInterceptor(JspTagLifecycle theLifecycle)
433 {
434 this.lifecycle = theLifecycle;
435 }
436
437 /**
438 * Overridden to invoke the lifecycle of the nested tag.
439 *
440 * {@inheritDoc}
441 * @see JspTagLifecycle.Interceptor#evalBody(int,BodyContent)
442 */
443 public void evalBody(int theIteration, BodyContent theBody)
444 throws JspException, IOException
445 {
446 lifecycle.invoke();
447 }
448 }
449
450 /**
451 * A specialized interceptor that prints nested template text when the tag's
452 * body is evaluated.
453 *
454 * @since Cactus 1.5
455 */
456 private class NestedTextInterceptor
457 extends Interceptor
458 {
459 /**
460 * The nested text.
461 */
462 private String text;
463
464 /**
465 * Constructor.
466 *
467 * @param theText The nested text
468 */
469 public NestedTextInterceptor(String theText)
470 {
471 this.text = theText;
472 }
473
474 /**
475 * Overridden to write the nested text to the current writer.
476 *
477 * {@inheritDoc}
478 * @see JspTagLifecycle.Interceptor#evalBody(int,BodyContent)
479 */
480 public void evalBody(int theIteration, BodyContent theBody)
481 throws IOException
482 {
483 if (theBody != null)
484 {
485 theBody.print(text);
486 }
487 else
488 {
489 pageContext.getOut().print(text);
490 }
491 }
492 }
493
494 // Class Variables ---------------------------------------------------------
495
496 /**
497 * The log target.
498 */
499 private static Log log = LogFactory.getLog(JspTagLifecycle.class);
500
501 // Instance Variables ------------------------------------------------------
502
503 /**
504 * The JSP page context.
505 */
506 protected PageContext pageContext;
507
508 /**
509 * The JSP tag handler.
510 */
511 private Tag tag;
512
513 /**
514 * The interceptor chain.
515 */
516 private List interceptors;
517
518 // Constructors ------------------------------------------------------------
519
520 /**
521 * Constructor.
522 *
523 * @param thePageContext The JSP page context
524 * @param theTag The JSP tag
525 */
526 public JspTagLifecycle(PageContext thePageContext, Tag theTag)
527 {
528 if ((thePageContext == null) || (theTag == null))
529 {
530 throw new NullPointerException();
531 }
532 this.tag = theTag;
533 this.pageContext = thePageContext;
534 this.tag.setPageContext(this.pageContext);
535 }
536
537 // Public Methods ----------------------------------------------------------
538
539 /**
540 * Adds an interceptor to the interceptor chain.
541 *
542 * @param theInterceptor The interceptor to add
543 */
544 public void addInterceptor(Interceptor theInterceptor)
545 {
546 if (theInterceptor == null)
547 {
548 throw new NullPointerException();
549 }
550 if (this.interceptors == null)
551 {
552 this.interceptors = new ArrayList();
553 }
554 this.interceptors.add(theInterceptor);
555 }
556
557 /**
558 * Adds a nested tag. The tag will be invoked when the body content of the
559 * enclosing tag is evaluated.
560 *
561 * @return The lifecycle wrapper for the nested tag, can be used to add
562 * expectations for the nested tag
563 * @param theNestedTag The tag to be nested
564 */
565 public JspTagLifecycle addNestedTag(Tag theNestedTag)
566 {
567 if (theNestedTag == null)
568 {
569 throw new NullPointerException();
570 }
571 JspTagLifecycle lifecycle =
572 new JspTagLifecycle(this.pageContext, theNestedTag);
573 theNestedTag.setParent(this.tag);
574 addInterceptor(new NestedTagInterceptor(lifecycle));
575 return lifecycle;
576 }
577
578 /**
579 * Adds template text to nest inside the tag. The text will be printed to
580 * the body content when it is evaluated.
581 *
582 * @param theNestedText The string containing the template text
583 */
584 public void addNestedText(String theNestedText)
585 {
586 if (theNestedText == null)
587 {
588 throw new NullPointerException();
589 }
590 addInterceptor(new NestedTextInterceptor(theNestedText));
591 }
592
593 /**
594 * Adds the expectation that the tag body must be evaluated once in the
595 * course of the tags lifecycle.
596 */
597 public void expectBodyEvaluated()
598 {
599 addInterceptor(new ExpectBodyEvaluatedInterceptor(1));
600 }
601
602 /**
603 * Adds the expectation that the tag body must be evaluated a specific
604 * number of times in the course of the tags lifecycle.
605 *
606 * @param theNumIterations The number of times the body is expected to get
607 * evaluated
608 */
609 public void expectBodyEvaluated(int theNumIterations)
610 {
611 addInterceptor(new ExpectBodyEvaluatedInterceptor(theNumIterations));
612 }
613
614 /**
615 * Adds the expectation that the tag body must be skipped. Essentially, this
616 * expectation verifies that the tag returns <code>SKIP_BODY</code> from
617 * <code>doStartTag()</code>.
618 */
619 public void expectBodySkipped()
620 {
621 addInterceptor(new ExpectBodySkippedInterceptor());
622 }
623
624 /**
625 * Adds a special expectation that verifies that a specific scoped variable
626 * is exposed in the body of the tag.
627 *
628 * @param theName The name of the variable
629 * @param theExpectedValues An ordered list containing the expected values
630 * values of the scoped variable, one for each expected iteration
631 * step
632 */
633 public void expectScopedVariableExposed(String theName,
634 Object[] theExpectedValues)
635 {
636 expectScopedVariableExposed(theName, theExpectedValues,
637 PageContext.PAGE_SCOPE);
638 }
639
640 /**
641 * Adds a special expectation that verifies that a specific scoped variable
642 * is exposed in the body of the tag.
643 *
644 * @param theName The name of the variable
645 * @param theExpectedValues An ordered list containing the expected values
646 * values of the scoped variable, one for each expected iteration
647 * step
648 * @param theScope The scope under which the variable is stored
649 */
650 public void expectScopedVariableExposed(String theName,
651 Object[] theExpectedValues, int theScope)
652 {
653 if ((theName == null) || (theExpectedValues == null))
654 {
655 throw new NullPointerException();
656 }
657 if (theExpectedValues.length == 0)
658 {
659 throw new IllegalArgumentException();
660 }
661 if ((theScope != PageContext.PAGE_SCOPE)
662 && (theScope != PageContext.REQUEST_SCOPE)
663 && (theScope != PageContext.SESSION_SCOPE)
664 && (theScope != PageContext.APPLICATION_SCOPE))
665 {
666 throw new IllegalArgumentException();
667 }
668 addInterceptor(
669 new ExpectScopedVariableExposedInterceptor(theName,
670 theExpectedValues, theScope));
671 }
672
673 /**
674 * Invokes the tag with the provided interceptor. The tag should have been
675 * populated with its properties before calling this method. The tag is not
676 * released after the tag's lifecycle is over.
677 *
678 * @throws JspException If the tag throws an exception
679 * @throws IOException If an error occurs when reading or writing the body
680 * content
681 */
682 public void invoke() throws JspException, IOException
683 {
684 if (this.tag instanceof TryCatchFinally)
685 {
686 TryCatchFinally tryCatchFinally = (TryCatchFinally) this.tag;
687 try
688 {
689 invokeInternal();
690 }
691 catch (Throwable t1)
692 {
693 try
694 {
695 tryCatchFinally.doCatch(t1);
696 }
697 catch (Throwable t2)
698 {
699 throw new JspException(t2.getMessage());
700 }
701 }
702 finally
703 {
704 tryCatchFinally.doFinally();
705 }
706 }
707 else
708 {
709 invokeInternal();
710 }
711 }
712
713 // Private Methods ---------------------------------------------------------
714
715 /**
716 * Notify all interceptors about a body evaluation.
717 *
718 * @param theIteration The iteration
719 * @param theBody The body content
720 * @throws JspException If thrown by a nested tag
721 * @throws IOException If an error occurs when reading or writing the body
722 * content
723 */
724 private void fireEvalBody(int theIteration, BodyContent theBody)
725 throws JspException, IOException
726 {
727 if (this.interceptors != null)
728 {
729 for (Iterator i = this.interceptors.iterator(); i.hasNext();)
730 {
731 ((Interceptor) i.next()).evalBody(theIteration, theBody);
732 }
733 }
734 }
735
736 /**
737 * Notify all interceptors that the body has been skipped.
738 */
739 private void fireSkipBody()
740 {
741 if (this.interceptors != null)
742 {
743 for (Iterator i = this.interceptors.iterator(); i.hasNext();)
744 {
745 ((Interceptor) i.next()).skipBody();
746 }
747 }
748 }
749
750 /**
751 * Internal method to invoke a tag without doing exception handling.
752 *
753 * @throws JspException If the tag throws an exception
754 * @throws IOException If an error occurs when reading or writing the body
755 * content
756 */
757 private void invokeInternal()
758 throws JspException, IOException
759 {
760 int status = this.tag.doStartTag();
761 if (this.tag instanceof IterationTag)
762 {
763 if (status != Tag.SKIP_BODY)
764 {
765 BodyContent body = null;
766 try
767 {
768 IterationTag iterationTag = (IterationTag) this.tag;
769 if ((status == BodyTag.EVAL_BODY_BUFFERED)
770 && (this.tag instanceof BodyTag))
771 {
772 BodyTag bodyTag = (BodyTag) this.tag;
773 body = pageContext.pushBody();
774 if (log.isDebugEnabled())
775 {
776 log.debug("Pushed body content ["
777 + body.getString() + "]");
778 }
779 bodyTag.setBodyContent(body);
780 bodyTag.doInitBody();
781 }
782 int iteration = 0;
783 do
784 {
785 fireEvalBody(iteration, body);
786 if (log.isDebugEnabled())
787 {
788 log.debug("Body evaluated for the ["
789 + iteration + "] time");
790 }
791 status = iterationTag.doAfterBody();
792 iteration++;
793 } while (status == IterationTag.EVAL_BODY_AGAIN);
794 if (log.isDebugEnabled())
795 {
796 log.debug("Body skipped");
797 }
798 fireSkipBody();
799 }
800 finally
801 {
802 if (body != null)
803 {
804 if (log.isDebugEnabled())
805 {
806 log.debug("Popping body content ["
807 + body.getString() + "]");
808 }
809 pageContext.popBody();
810 body = null;
811 }
812 }
813 }
814 else
815 {
816 if (log.isDebugEnabled())
817 {
818 log.debug("Body skipped");
819 }
820 fireSkipBody();
821 }
822 }
823 status = tag.doEndTag();
824 }
825
826 }