View Javadoc

1   /***
2    * Licensed under the Artistic License; you may not use this file
3    * except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    *      http://displaytag.sourceforge.net/license.html
7    *
8    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
9    * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
10   * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
11   */
12  package org.displaytag.tags;
13  
14  import java.io.ByteArrayOutputStream;
15  import java.io.IOException;
16  import java.io.StringWriter;
17  import java.io.Writer;
18  import java.text.MessageFormat;
19  import java.util.Collection;
20  import java.util.HashMap;
21  import java.util.Hashtable;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Map;
25  
26  import javax.servlet.http.HttpServletRequest;
27  import javax.servlet.http.HttpServletResponse;
28  import javax.servlet.jsp.JspException;
29  import javax.servlet.jsp.JspTagException;
30  import javax.servlet.jsp.JspWriter;
31  import javax.servlet.jsp.PageContext;
32  
33  import org.apache.commons.beanutils.BeanUtils;
34  import org.apache.commons.collections.IteratorUtils;
35  import org.apache.commons.lang.StringUtils;
36  import org.apache.commons.lang.math.LongRange;
37  import org.apache.commons.lang.math.Range;
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  import org.displaytag.decorator.DecoratorFactory;
41  import org.displaytag.decorator.TableDecorator;
42  import org.displaytag.exception.DecoratorException;
43  import org.displaytag.exception.ExportException;
44  import org.displaytag.exception.FactoryInstantiationException;
45  import org.displaytag.exception.InvalidTagAttributeValueException;
46  import org.displaytag.exception.ObjectLookupException;
47  import org.displaytag.exception.WrappedRuntimeException;
48  import org.displaytag.export.BinaryExportView;
49  import org.displaytag.export.ExportView;
50  import org.displaytag.export.ExportViewFactory;
51  import org.displaytag.export.TextExportView;
52  import org.displaytag.model.Cell;
53  import org.displaytag.model.Column;
54  import org.displaytag.model.ColumnIterator;
55  import org.displaytag.model.HeaderCell;
56  import org.displaytag.model.Row;
57  import org.displaytag.model.RowIterator;
58  import org.displaytag.model.TableModel;
59  import org.displaytag.pagination.SmartListHelper;
60  import org.displaytag.properties.MediaTypeEnum;
61  import org.displaytag.properties.SortOrderEnum;
62  import org.displaytag.properties.TableProperties;
63  import org.displaytag.util.Anchor;
64  import org.displaytag.util.CollectionUtil;
65  import org.displaytag.util.DependencyChecker;
66  import org.displaytag.util.Href;
67  import org.displaytag.util.ParamEncoder;
68  import org.displaytag.util.RequestHelper;
69  import org.displaytag.util.RequestHelperFactory;
70  import org.displaytag.util.TagConstants;
71  
72  
73  /***
74   * This tag takes a list of objects and creates a table to display those objects. With the help of column tags, you
75   * simply provide the name of properties (get Methods) that are called against the objects in your list that gets
76   * displayed. This tag works very much like the struts iterator tag, most of the attributes have the same name and
77   * functionality as the struts tag.
78   * @author mraible
79   * @author Fabrizio Giustina
80   * @version $Revision: 1.99 $ ($Author: fgiust $)
81   */
82  public class TableTag extends HtmlTableTag
83  {
84  
85      /***
86       * name of the attribute added to page scope when exporting, containing an MediaTypeEnum this can be used in column
87       * content to detect the output type and to return different data when exporting.
88       */
89      public static final String PAGE_ATTRIBUTE_MEDIA = "mediaType"; //$NON-NLS-1$
90  
91      /***
92       * If this variable is found in the request, assume the export filter is enabled.
93       */
94      public static final String FILTER_CONTENT_OVERRIDE_BODY = //
95      "org.displaytag.filter.ResponseOverrideFilter.CONTENT_OVERRIDE_BODY"; //$NON-NLS-1$
96  
97      /***
98       * D1597A17A6.
99       */
100     private static final long serialVersionUID = 899149338534L;
101 
102     /***
103      * logger.
104      */
105     private static Log log = LogFactory.getLog(TableTag.class);
106 
107     /***
108      * RequestHelperFactory instance used for link generation.
109      */
110     private static RequestHelperFactory rhf;
111 
112     /***
113      * Object (collection, list) on which the table is based. This is not set directly using a tag attribute and can be
114      * cleaned.
115      */
116     protected Object list;
117 
118     // -- start tag attributes --
119 
120     /***
121      * Object (collection, list) on which the table is based. Set directly using the "list" attribute or evaluated from
122      * expression.
123      */
124     protected Object listAttribute;
125 
126     /***
127      * actual row number, updated during iteration.
128      */
129     private int rowNumber = 1;
130 
131     /***
132      * name of the object to use for iteration. Can contain expressions.
133      */
134     private String name;
135 
136     /***
137      * property to get into the bean defined by "name".
138      * @deprecated Use expressions in "name" attribute
139      */
140     private String property;
141 
142     /***
143      * scope of the bean defined by "name". Use expressions in name instead
144      * @deprecated
145      */
146     private String scope;
147 
148     /***
149      * length of list to display.
150      */
151     private int length;
152 
153     /***
154      * table decorator class name.
155      */
156     private String decoratorName;
157 
158     /***
159      * page size.
160      */
161     private int pagesize;
162 
163     /***
164      * add export links.
165      */
166     private boolean export;
167 
168     /***
169      * list offset.
170      */
171     private int offset;
172 
173     /***
174      * sort the full list?
175      */
176     private Boolean sortFullTable;
177 
178     /***
179      * Request uri.
180      */
181     private String requestUri;
182 
183     /***
184      * Prepend application context to generated links.
185      */
186     private boolean dontAppendContext;
187 
188     /***
189      * the index of the column sorted by default.
190      */
191     private int defaultSortedColumn = -1;
192 
193     /***
194      * the sorting order for the sorted column.
195      */
196     private SortOrderEnum defaultSortOrder;
197 
198     /***
199      * Name of parameter which should not be forwarded during sorting or pagination.
200      */
201     private String excludedParams;
202 
203     /***
204      * Unique table id.
205      */
206     private String uid;
207 
208     // -- end tag attributes --
209 
210     /***
211      * Map which contains previous row values. Needed for grouping
212      */
213     private Map previousRow;
214 
215     /***
216      * table model - initialized in doStartTag().
217      */
218     private TableModel tableModel;
219 
220     /***
221      * current row.
222      */
223     private Row currentRow;
224 
225     /***
226      * next row.
227      */
228     private Map nextRow;
229 
230     /***
231      * Used by various functions when the person wants to do paging - cleaned in doEndTag().
232      */
233     private SmartListHelper listHelper;
234 
235     /***
236      * base href used for links - set in initParameters().
237      */
238     private Href baseHref;
239 
240     /***
241      * table properties - set in doStartTag().
242      */
243     private TableProperties properties;
244 
245     /***
246      * page number - set in initParameters().
247      */
248     private int pageNumber = 1;
249 
250     /***
251      * Iterator on collection.
252      */
253     private Iterator tableIterator;
254 
255     /***
256      * export type - set in initParameters().
257      */
258     private MediaTypeEnum currentMediaType;
259 
260     /***
261      * daAfterBody() has been executed at least once?
262      */
263     private boolean doAfterBodyExecuted;
264 
265     /***
266      * The param encoder used to generate unique parameter names. Initialized at the first use of encodeParameter().
267      */
268     private ParamEncoder paramEncoder;
269 
270     /***
271      * static footer added using the footer tag.
272      */
273     private String footer;
274 
275     /***
276      * static caption added using the footer tag.
277      */
278     private String caption;
279 
280     /***
281      * Included row range. If no rows can be skipped the range is from 0 to Long.MAX_VALUE. Range check should be always
282      * done using containsLong(). This is an instance of org.apache.commons.lang.math.Range, but it's declared as Object
283      * to avoid runtime errors while Jasper tries to compile the page and commons lang 2.0 is not available. Commons
284      * lang version will be checked in the doStartTag() method in order to provide a more user friendly message.
285      */
286     private Object filteredRows;
287 
288     /***
289      * Sets the list of parameter which should not be forwarded during sorting or pagination.
290      * @param value whitespace separated list of parameters which should not be included (* matches all parameters)
291      */
292     public void setExcludedParams(String value)
293     {
294         this.excludedParams = value;
295     }
296 
297     /***
298      * Sets the content of the footer. Called by a nested footer tag.
299      * @param string footer content
300      */
301     public void setFooter(String string)
302     {
303         this.footer = string;
304     }
305 
306     /***
307      * Sets the content of the caption. Called by a nested caption tag.
308      * @param string caption content
309      */
310     public void setCaption(String string)
311     {
312         this.caption = string;
313     }
314 
315     /***
316      * Is the current row empty?
317      * @return true if the current row is empty
318      */
319     protected boolean isEmpty()
320     {
321         return this.currentRow == null;
322     }
323 
324     /***
325      * setter for the "sort" attribute.
326      * @param value "page" (sort a single page) or "list" (sort the full list)
327      * @throws InvalidTagAttributeValueException if value is not "page" or "list"
328      */
329     public void setSort(String value) throws InvalidTagAttributeValueException
330     {
331         if (TableTagParameters.SORT_AMOUNT_PAGE.equals(value))
332         {
333             this.sortFullTable = Boolean.FALSE;
334         }
335         else if (TableTagParameters.SORT_AMOUNT_LIST.equals(value))
336         {
337             this.sortFullTable = Boolean.TRUE;
338         }
339         else
340         {
341             throw new InvalidTagAttributeValueException(getClass(), "sort", value); //$NON-NLS-1$
342         }
343     }
344 
345     /***
346      * setter for the "requestURI" attribute. Context path is automatically added to path starting with "/".
347      * @param value base URI for creating links
348      */
349     public void setRequestURI(String value)
350     {
351         this.requestUri = value;
352     }
353 
354     /***
355      * Setter for the "requestURIcontext" attribute.
356      * @param value base URI for creating links
357      */
358     public void setRequestURIcontext(boolean value)
359     {
360         this.dontAppendContext = !value;
361     }
362 
363     /***
364      * Used to directly set a list (or any object you can iterate on).
365      * @param value Object
366      * @deprecated use setName() to get the object from the page or request scope instead of setting it directly here
367      */
368     public void setList(Object value)
369     {
370         this.listAttribute = value;
371     }
372 
373     /***
374      * Sets the name of the object to use for iteration.
375      * @param value name of the object to use for iteration (can contain expression). It also supports direct setting of
376      * a list, for jsp 2.0 containers where users can set up a data source here using EL expressions.
377      */
378     public void setName(Object value)
379     {
380         if (value instanceof String)
381         {
382             // ok, assuming this is the name of the object
383             this.name = (String) value;
384         }
385         else
386         {
387             // is this the list?
388             this.list = value;
389         }
390     }
391 
392     /***
393      * Sets the name of the object to use for iteration. This setter is needed for jsp 1.1 container which doesn't
394      * support the String - Object conversion. The bean info class will swith to this setter.
395      * @param value name of the object
396      */
397     public void setNameString(String value)
398     {
399         this.name = value;
400     }
401 
402     /***
403      * Sets the property to get into the bean defined by "name".
404      * @param value property name
405      * @deprecated Use expressions in "name" attribute
406      */
407     public void setProperty(String value)
408     {
409         this.property = value;
410     }
411 
412     /***
413      * sets the sorting order for the sorted column.
414      * @param value "ascending" or "descending"
415      * @throws InvalidTagAttributeValueException if value is not one of "ascending" or "descending"
416      */
417     public void setDefaultorder(String value) throws InvalidTagAttributeValueException
418     {
419         this.defaultSortOrder = SortOrderEnum.fromName(value);
420         if (this.defaultSortOrder == null)
421         {
422             throw new InvalidTagAttributeValueException(getClass(), "defaultorder", value); //$NON-NLS-1$
423         }
424     }
425 
426     /***
427      * Setter for object scope.
428      * @param value String
429      * @deprecated Use expressions in "name" attribute
430      */
431     public void setScope(String value)
432     {
433         this.scope = value;
434     }
435 
436     /***
437      * Setter for the decorator class name.
438      * @param decorator fully qualified name of the table decorator to use
439      */
440     public void setDecorator(String decorator)
441     {
442         this.decoratorName = decorator;
443     }
444 
445     /***
446      * Is export enabled?
447      * @param value <code>true</code> if export should be enabled
448      */
449     public void setExport(boolean value)
450     {
451         this.export = value;
452     }
453 
454     /***
455      * sets the number of items to be displayed in the page.
456      * @param value number of items to display in a page
457      */
458     public void setLength(int value)
459     {
460         this.length = value;
461     }
462 
463     /***
464      * sets the index of the default sorted column.
465      * @param value index of the column to sort
466      */
467     public void setDefaultsort(int value)
468     {
469         // subtract one (internal index is 0 based)
470         this.defaultSortedColumn = value - 1;
471     }
472 
473     /***
474      * sets the number of items that should be displayed for a single page.
475      * @param value number of items that should be displayed for a single page
476      */
477     public void setPagesize(int value)
478     {
479         this.pagesize = value;
480     }
481 
482     /***
483      * Setter for the list offset attribute.
484      * @param value String
485      */
486     public void setOffset(int value)
487     {
488         if (value < 1)
489         {
490             // negative values has no meaning, simply treat them as 0
491             this.offset = 0;
492         }
493         else
494         {
495             this.offset = value - 1;
496         }
497     }
498 
499     /***
500      * Sets the unique id used to identify for this table.
501      * @param value String
502      */
503     public void setUid(String value)
504     {
505         if (getHtmlId() == null)
506         {
507             setHtmlId(value); // by default id is actually used for the html id attribute, if no htmlId is added
508         }
509 
510         this.uid = value;
511     }
512 
513     /***
514      * Returns the unique id used to identify for this table.
515      * @return id for this table
516      */
517     public String getUid()
518     {
519         return this.uid;
520     }
521 
522     /***
523      * It's a getter.
524      * @return the this.pageContext
525      */
526     public PageContext getPageContext()
527     {
528         return this.pageContext;
529     }
530 
531     /***
532      * Returns the properties.
533      * @return TableProperties
534      */
535     protected TableProperties getProperties()
536     {
537         return this.properties;
538     }
539 
540     /***
541      * Returns the base href with parameters. This is the instance used for links, need to be cloned before being
542      * modified.
543      * @return base Href with parameters
544      */
545     protected Href getBaseHref()
546     {
547         return this.baseHref;
548     }
549 
550     /***
551      * Called by interior column tags to help this tag figure out how it is supposed to display the information in the
552      * List it is supposed to display.
553      * @param column an internal tag describing a column in this tableview
554      */
555     public void addColumn(HeaderCell column)
556     {
557         if (log.isDebugEnabled())
558         {
559             log.debug("[" + getUid() + "] addColumn " + column);
560         }
561         this.tableModel.addColumnHeader(column);
562     }
563 
564     /***
565      * Adds a cell to the current row. This method is usually called by a contained ColumnTag
566      * @param cell Cell to add to the current row
567      */
568     public void addCell(Cell cell)
569     {
570         // check if null: could be null if list is empty, we don't need to fill rows
571         if (this.currentRow != null)
572         {
573             this.currentRow.addCell(cell);
574         }
575     }
576 
577     /***
578      * Is this the first iteration?
579      * @return boolean <code>true</code> if this is the first iteration
580      */
581     protected boolean isFirstIteration()
582     {
583         if (log.isDebugEnabled())
584         {
585             log.debug("["
586                 + getUid()
587                 + "] first iteration="
588                 + (this.rowNumber == 1)
589                 + " (row number="
590                 + this.rowNumber
591                 + ")");
592         }
593         // in first iteration this.rowNumber is 1
594         // (this.rowNumber is incremented in doAfterBody)
595         return this.rowNumber == 1;
596     }
597 
598     /***
599      * When the tag starts, we just initialize some of our variables, and do a little bit of error checking to make sure
600      * that the user is not trying to give us parameters that we don't expect.
601      * @return int
602      * @throws JspException generic exception
603      * @see javax.servlet.jsp.tagext.Tag#doStartTag()
604      */
605     public int doStartTag() throws JspException
606     {
607         DependencyChecker.check();
608 
609         // needed before column processing, elsewhere registered views will not be added
610         ExportViewFactory.getInstance();
611 
612         if (log.isDebugEnabled())
613         {
614             log.debug("[" + getUid() + "] doStartTag called");
615         }
616 
617         this.properties = TableProperties.getInstance((HttpServletRequest) pageContext.getRequest());
618         this.tableModel = new TableModel(this.properties, pageContext.getResponse().getCharacterEncoding());
619 
620         // copying id to the table model for logging
621         this.tableModel.setId(getUid());
622 
623         initParameters();
624 
625         Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA);
626         // set the PAGE_ATTRIBUTE_MEDIA attribute in the page scope
627         if (this.currentMediaType != null
628             && (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType)))
629         {
630             if (log.isDebugEnabled())
631             {
632                 log.debug("[" + getUid() + "] setting media [" + this.currentMediaType + "] in this.pageContext");
633             }
634             this.pageContext.setAttribute(PAGE_ATTRIBUTE_MEDIA, this.currentMediaType);
635         }
636 
637         doIteration();
638 
639         // always return EVAL_BODY_TAG to get column headers also if the table is empty
640         // using int to avoid deprecation error in compilation using j2ee 1.3
641         return 2;
642     }
643 
644     /***
645      * @see javax.servlet.jsp.tagext.BodyTag#doAfterBody()
646      */
647     public int doAfterBody()
648     {
649         // doAfterBody() has been called, body is not empty
650         this.doAfterBodyExecuted = true;
651 
652         if (log.isDebugEnabled())
653         {
654             log.debug("[" + getUid() + "] doAfterBody called - iterating on row " + this.rowNumber);
655         }
656 
657         // increment this.rowNumber
658         this.rowNumber++;
659 
660         // Call doIteration() to do the common work
661         return doIteration();
662     }
663 
664     /***
665      * Utility method that is used by both doStartTag() and doAfterBody() to perform an iteration.
666      * @return <code>int</code> either EVAL_BODY_TAG or SKIP_BODY depending on whether another iteration is desired.
667      */
668     protected int doIteration()
669     {
670 
671         if (log.isDebugEnabled())
672         {
673             log.debug("[" + getUid() + "] doIteration called");
674         }
675 
676         // Row already filled?
677         if (this.currentRow != null)
678         {
679             // if yes add to table model and remove
680             this.tableModel.addRow(this.currentRow);
681             this.currentRow = null;
682         }
683 
684         if (this.tableIterator.hasNext())
685         {
686 
687             Object iteratedObject = this.tableIterator.next();
688             if (getUid() != null)
689             {
690                 if ((iteratedObject != null))
691                 {
692                     // set object into this.pageContext
693                     if (log.isDebugEnabled())
694                     {
695                         log.debug("[" + getUid() + "] setting attribute \"" + getUid() + "\" in pageContext");
696                     }
697                     this.pageContext.setAttribute(getUid(), iteratedObject);
698 
699                 }
700                 else
701                 {
702                     // if row is null remove previous object
703                     this.pageContext.removeAttribute(getUid());
704                 }
705                 // set the current row number into this.pageContext
706                 this.pageContext.setAttribute(getUid() + TableTagExtraInfo.ROWNUM_SUFFIX, new Integer(this.rowNumber));
707             }
708 
709             // Row object for Cell values
710             this.currentRow = new Row(iteratedObject, this.rowNumber);
711 
712             // new iteration
713             // using int to avoid deprecation error in compilation using j2ee 1.3
714             return 2;
715         }
716 
717         if (log.isDebugEnabled())
718         {
719             log.debug("[" + getUid() + "] doIteration() - iterator ended after " + (this.rowNumber - 1) + " rows");
720         }
721 
722         // end iteration
723         return SKIP_BODY;
724     }
725 
726     /***
727      * Reads parameters from the request and initialize all the needed table model attributes.
728      * @throws ObjectLookupException for problems in evaluating the expression in the "name" attribute
729      * @throws FactoryInstantiationException for problems in instantiating a RequestHelperFactory
730      */
731     private void initParameters() throws ObjectLookupException, FactoryInstantiationException
732     {
733         if (rhf == null)
734         {
735             // first time initialization
736             rhf = this.properties.getRequestHelperFactoryInstance();
737         }
738 
739         RequestHelper requestHelper = rhf.getRequestHelperInstance(this.pageContext);
740 
741         initHref(requestHelper);
742 
743         Integer pageNumberParameter = requestHelper.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_PAGE));
744         this.pageNumber = (pageNumberParameter == null) ? 1 : pageNumberParameter.intValue();
745 
746         Integer sortColumnParameter = requestHelper.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_SORT));
747         int sortColumn = (sortColumnParameter == null) ? this.defaultSortedColumn : sortColumnParameter.intValue();
748         this.tableModel.setSortedColumnNumber(sortColumn);
749 
750         // default value
751         boolean finalSortFull = this.properties.getSortFullList();
752 
753         // user value for this single table
754         if (this.sortFullTable != null)
755         {
756             finalSortFull = this.sortFullTable.booleanValue();
757         }
758 
759         this.tableModel.setSortFullTable(finalSortFull);
760 
761         SortOrderEnum paramOrder = SortOrderEnum.fromCode(requestHelper
762             .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_ORDER)));
763 
764         // if no order parameter is set use default
765         if (paramOrder == null)
766         {
767             paramOrder = this.defaultSortOrder;
768         }
769 
770         boolean order = SortOrderEnum.DESCENDING != paramOrder;
771         this.tableModel.setSortOrderAscending(order);
772 
773         Integer exportTypeParameter = requestHelper
774             .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_EXPORTTYPE));
775         this.currentMediaType = MediaTypeEnum.fromCode(exportTypeParameter);
776         if (this.currentMediaType == null)
777         {
778             this.currentMediaType = MediaTypeEnum.HTML;
779         }
780 
781         String fullName = getFullObjectName();
782 
783         // only evaluate if needed, else use list attribute
784         if (fullName != null)
785         {
786             this.list = evaluateExpression(fullName);
787         }
788         else if (this.list == null)
789         {
790             // needed to allow removing the collection of objects if not set directly
791             this.list = this.listAttribute;
792         }
793 
794         // do we really need to skip any row?
795         boolean wishOptimizedIteration = (this.pagesize > 0 // we are paging
796             || this.offset > 0 // or we are skipping some records using offset
797         || this.length > 0 // or we are limiting the records using length
798         );
799 
800         // can we actually skip any row?
801         if (wishOptimizedIteration && (this.list instanceof Collection) // we need to know the size
802             && ((sortColumn == -1 // and we are not sorting
803             || !finalSortFull // or we are sorting with the "page" behaviour
804             ) && (this.currentMediaType == MediaTypeEnum.HTML // and we are not exporting
805             || !this.properties.getExportFullList()) // or we are exporting a single page
806             ))
807         {
808             int start = 0;
809             int end = 0;
810             if (this.offset > 0)
811             {
812                 start = this.offset;
813             }
814             if (length > 0)
815             {
816                 end = start + this.length;
817             }
818 
819             if (this.pagesize > 0)
820             {
821                 int fullSize = ((Collection) this.list).size();
822                 start = (this.pageNumber - 1) * this.pagesize;
823 
824                 // invalid page requested, go back to page one
825                 if (start > fullSize)
826                 {
827                     start = 0;
828                 }
829 
830                 end = start + this.pagesize;
831             }
832 
833             // rowNumber starts from 1
834             filteredRows = new LongRange(start + 1, end);
835         }
836         else
837         {
838             filteredRows = new LongRange(1, Long.MAX_VALUE);
839         }
840 
841         this.tableIterator = IteratorUtils.getIterator(this.list);
842     }
843 
844     /***
845      * Is the current row included in the "to-be-evaluated" range? Called by nested ColumnTags. If <code>false</code>
846      * column body is skipped.
847      * @return <code>true</code> if the current row must be evaluated because is included in output or because is
848      * included in sorting.
849      */
850     protected boolean isIncludedRow()
851     {
852         return ((Range) filteredRows).containsLong(this.rowNumber);
853     }
854 
855     /***
856      * Create a complete string for compatibility with previous version before expression evaluation. This approach is
857      * optimized for new expressions, not for previous property/scope parameters.
858      * @return Expression composed by scope + name + property
859      */
860     private String getFullObjectName()
861     {
862         // only evaluate if needed, else preserve original list
863         if (this.name == null)
864         {
865             return null;
866         }
867 
868         StringBuffer fullName = new StringBuffer(30);
869 
870         // append scope
871         if (StringUtils.isNotBlank(this.scope))
872         {
873             fullName.append(this.scope).append("Scope."); //$NON-NLS-1$
874         }
875 
876         // base bean name
877         fullName.append(this.name);
878 
879         // append property
880         if (StringUtils.isNotBlank(this.property))
881         {
882             fullName.append('.').append(this.property);
883         }
884 
885         return fullName.toString();
886     }
887 
888     /***
889      * init the href object used to generate all the links for pagination, sorting, exporting.
890      * @param requestHelper request helper used to extract the base Href
891      */
892     protected void initHref(RequestHelper requestHelper)
893     {
894         // get the href for this request
895         Href normalHref = requestHelper.getHref();
896 
897         if (this.excludedParams != null)
898         {
899             String[] splittedExcludedParams = StringUtils.split(this.excludedParams);
900 
901             // handle * keyword
902             if (splittedExcludedParams.length == 1 && "*".equals(splittedExcludedParams[0]))
903             {
904                 // @todo cleanup: paramEncoder initialization should not be done here
905                 if (this.paramEncoder == null)
906                 {
907                     this.paramEncoder = new ParamEncoder(getUid());
908                 }
909 
910                 Iterator paramsIterator = normalHref.getParameterMap().keySet().iterator();
911                 while (paramsIterator.hasNext())
912                 {
913                     String key = (String) paramsIterator.next();
914 
915                     // don't remove parameters added by the table tag
916                     if (!this.paramEncoder.isParameterEncoded(key))
917                     {
918                         normalHref.removeParameter(key);
919                     }
920                 }
921             }
922             else
923             {
924                 for (int j = 0; j < splittedExcludedParams.length; j++)
925                 {
926                     normalHref.removeParameter(splittedExcludedParams[j]);
927                 }
928             }
929         }
930 
931         if (this.requestUri != null)
932         {
933             // if user has added a requestURI create a new href
934             String fullURI = requestUri;
935             if (!this.dontAppendContext)
936             {
937                 String contextPath = ((HttpServletRequest) this.pageContext.getRequest()).getContextPath();
938 
939                 // prepend the context path if any.
940                 // actually checks if context path is already there for people which manually add it
941                 if (!StringUtils.isEmpty(contextPath)
942                     && requestUri != null
943                     && requestUri.startsWith("/")
944                     && !requestUri.startsWith(contextPath))
945                 {
946                     fullURI = contextPath + this.requestUri;
947                 }
948             }
949 
950             // call encodeURL to preserve session id when cookies are disabled
951             fullURI = ((HttpServletResponse) this.pageContext.getResponse()).encodeURL(fullURI);
952             this.baseHref = new Href(fullURI);
953 
954             // ... and copy parameters from the current request
955             Map parameterMap = normalHref.getParameterMap();
956             this.baseHref.addParameterMap(parameterMap);
957         }
958         else
959         {
960             // simply copy href
961             this.baseHref = normalHref;
962         }
963     }
964 
965     /***
966      * Draw the table. This is where everything happens, we figure out what values we are supposed to be showing, we
967      * figure out how we are supposed to be showing them, then we draw them.
968      * @return int
969      * @throws JspException generic exception
970      * @see javax.servlet.jsp.tagext.Tag#doEndTag()
971      */
972     public int doEndTag() throws JspException
973     {
974 
975         if (log.isDebugEnabled())
976         {
977             log.debug("[" + getUid() + "] doEndTag called");
978         }
979 
980         if (!this.doAfterBodyExecuted)
981         {
982             if (log.isDebugEnabled())
983             {
984                 log.debug("[" + getUid() + "] tag body is empty.");
985             }
986 
987             // first row (created in doStartTag)
988             if (this.currentRow != null)
989             {
990                 // if yes add to table model and remove
991                 this.tableModel.addRow(this.currentRow);
992             }
993 
994             // other rows
995             while (this.tableIterator.hasNext())
996             {
997                 Object iteratedObject = this.tableIterator.next();
998                 this.rowNumber++;
999 
1000                 // Row object for Cell values
1001                 this.currentRow = new Row(iteratedObject, this.rowNumber);
1002 
1003                 this.tableModel.addRow(this.currentRow);
1004             }
1005         }
1006 
1007         // if no rows are defined automatically get all properties from bean
1008         if (this.tableModel.isEmpty())
1009         {
1010             describeEmptyTable();
1011         }
1012 
1013         TableDecorator tableDecorator = DecoratorFactory.loadTableDecorator(this.decoratorName);
1014 
1015         if (tableDecorator != null)
1016         {
1017             tableDecorator.init(this.pageContext, this.list);
1018             this.tableModel.setTableDecorator(tableDecorator);
1019         }
1020 
1021         setupViewableData();
1022 
1023         // Figure out how we should sort this data, typically we just sort
1024         // the data being shown, but the programmer can override this behavior
1025 
1026         if (!this.tableModel.isSortFullTable())
1027         {
1028             this.tableModel.sortPageList();
1029         }
1030 
1031         // Get the data back in the representation that the user is after, do they want HTML/XML/CSV/EXCEL/etc...
1032         int returnValue = EVAL_PAGE;
1033 
1034         // check for nested tables
1035         Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA);
1036         if (MediaTypeEnum.HTML.equals(this.currentMediaType)
1037             && (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType)))
1038         {
1039             writeHTMLData();
1040         }
1041         else if (!MediaTypeEnum.HTML.equals(this.currentMediaType))
1042         {
1043             if (log.isDebugEnabled())
1044             {
1045                 log.debug("[" + getUid() + "] doEndTag - exporting");
1046             }
1047 
1048             returnValue = doExport();
1049         }
1050 
1051         // do not remove media attribute! if the table is nested in other tables this is still needed
1052         // this.pageContext.removeAttribute(PAGE_ATTRIBUTE_MEDIA);
1053 
1054         if (log.isDebugEnabled())
1055         {
1056             log.debug("[" + getUid() + "] doEndTag - end");
1057         }
1058 
1059         cleanUp();
1060         return returnValue;
1061     }
1062 
1063     /***
1064      * clean up instance variables, but not the ones representing tag attributes.
1065      */
1066     private void cleanUp()
1067     {
1068         // reset instance variables (non attributes)
1069         this.currentMediaType = null;
1070         this.baseHref = null;
1071         this.caption = null;
1072         this.currentRow = null;
1073         this.doAfterBodyExecuted = false;
1074         this.footer = null;
1075         this.listHelper = null;
1076         this.nextRow = null;
1077         this.pageNumber = 0;
1078         this.paramEncoder = null;
1079         this.previousRow = null;
1080         this.properties = null;
1081         this.rowNumber = 1;
1082         this.tableIterator = null;
1083         this.tableModel = null;
1084         this.list = null;
1085     }
1086 
1087     /***
1088      * If no columns are provided, automatically add them from bean properties. Get the first object in the list and get
1089      * all the properties (except the "class" property which is automatically skipped). Of course this isn't possible
1090      * for empty lists.
1091      */
1092     private void describeEmptyTable()
1093     {
1094         this.tableIterator = IteratorUtils.getIterator(this.list);
1095 
1096         if (this.tableIterator.hasNext())
1097         {
1098             Object iteratedObject = this.tableIterator.next();
1099             Map objectProperties = new HashMap();
1100 
1101             // if it's a String don't add the "Bytes" column
1102             if (iteratedObject instanceof String)
1103             {
1104                 return;
1105             }
1106             // if it's a map already use key names for column headers
1107             if (iteratedObject instanceof Map)
1108             {
1109                 objectProperties = (Map) iteratedObject;
1110             }
1111             else
1112             {
1113                 try
1114                 {
1115                     objectProperties = BeanUtils.describe(iteratedObject);
1116                 }
1117                 catch (Exception e)
1118                 {
1119                     log.warn("Unable to automatically add columns: " + e.getMessage(), e);
1120                 }
1121             }
1122 
1123             // iterator on properties names
1124             Iterator propertiesIterator = objectProperties.keySet().iterator();
1125 
1126             while (propertiesIterator.hasNext())
1127             {
1128                 // get the property name
1129                 String propertyName = (String) propertiesIterator.next();
1130 
1131                 // dont't want to add the standard "class" property
1132                 if (!"class".equals(propertyName)) //$NON-NLS-1$
1133                 {
1134                     // creates a new header and add to the table model
1135                     HeaderCell headerCell = new HeaderCell();
1136                     headerCell.setBeanPropertyName(propertyName);
1137 
1138                     // handle title i18n
1139                     headerCell.setTitle(this.properties.geResourceProvider().getResource(
1140                         null,
1141                         propertyName,
1142                         this,
1143                         this.pageContext));
1144 
1145                     this.tableModel.addColumnHeader(headerCell);
1146                 }
1147             }
1148         }
1149     }
1150 
1151     /***
1152      * Called when data are not displayed in a html page but should be exported.
1153      * @return int SKIP_PAGE
1154      * @throws JspException generic exception
1155      */
1156     protected int doExport() throws JspException
1157     {
1158 
1159         boolean exportFullList = this.properties.getExportFullList();
1160 
1161         if (log.isDebugEnabled())
1162         {
1163             log.debug("[" + getUid() + "] currentMediaType=" + this.currentMediaType);
1164         }
1165 
1166         boolean exportHeader = this.properties.getExportHeader(this.currentMediaType);
1167         boolean exportDecorated = this.properties.getExportDecorated();
1168 
1169         ExportView exportView = ExportViewFactory.getInstance().getView(
1170             this.currentMediaType,
1171             this.tableModel,
1172             exportFullList,
1173             exportHeader,
1174             exportDecorated);
1175 
1176         try
1177         {
1178             writeExport(exportView);
1179         }
1180         catch (IOException e)
1181         {
1182             throw new WrappedRuntimeException(getClass(), e);
1183         }
1184 
1185         return SKIP_PAGE;
1186     }
1187 
1188     /***
1189      * Will write the export. The default behavior is to write directly to the response. If the ResponseOverrideFilter
1190      * is configured for this request, will instead write the exported content to a map in the Request object.
1191      * @param exportView export view
1192      * @throws JspException for problem in clearing the response or for invalid export views
1193      * @throws IOException exception thrown when writing content to the response
1194      */
1195     protected void writeExport(ExportView exportView) throws IOException, JspException
1196     {
1197         String filename = properties.getExportFileName(this.currentMediaType);
1198 
1199         HttpServletResponse response = (HttpServletResponse) this.pageContext.getResponse();
1200         HttpServletRequest request = (HttpServletRequest) this.pageContext.getRequest();
1201 
1202         Map bean = (Map) request.getAttribute(FILTER_CONTENT_OVERRIDE_BODY);
1203         boolean usingFilter = bean != null;
1204 
1205         String mimeType = exportView.getMimeType();
1206         // original encoding, be sure to add it back after reset()
1207         String characterEncoding = response.getCharacterEncoding();
1208 
1209         if (usingFilter)
1210         {
1211             if (!bean.containsKey(TableTagParameters.BEAN_BUFFER))
1212             {
1213                 // We are running under the export filter, call it
1214                 log.debug("Exportfilter enabled in unbuffered mode, setting headers");
1215                 response.addHeader(TableTagParameters.PARAMETER_EXPORTING, TagConstants.EMPTY_STRING);
1216             }
1217             else
1218             {
1219                 // We are running under the export filter in buffered mode
1220                 bean.put(TableTagParameters.BEAN_CONTENTTYPE, mimeType);
1221                 bean.put(TableTagParameters.BEAN_FILENAME, filename);
1222 
1223                 if (exportView instanceof TextExportView)
1224                 {
1225                     StringWriter writer = new StringWriter();
1226                     ((TextExportView) exportView).doExport(writer);
1227                     bean.put(TableTagParameters.BEAN_BODY, writer.toString());
1228                 }
1229                 else if (exportView instanceof BinaryExportView)
1230                 {
1231                     ByteArrayOutputStream stream = new ByteArrayOutputStream();
1232                     ((BinaryExportView) exportView).doExport(stream);
1233                     bean.put(TableTagParameters.BEAN_BODY, stream.toByteArray());
1234 
1235                 }
1236                 else
1237                 {
1238                     throw new JspTagException("Export view "
1239                         + exportView.getClass().getName()
1240                         + " must implement TextExportView or BinaryExportView");
1241                 }
1242 
1243                 return;
1244             }
1245         }
1246         else
1247         {
1248             log.debug("Exportfilter NOT enabled");
1249             // response can't be already committed at this time
1250             if (response.isCommitted())
1251             {
1252                 throw new ExportException(getClass());
1253             }
1254 
1255             try
1256             {
1257                 response.reset();
1258                 pageContext.getOut().clearBuffer();
1259             }
1260             catch (Exception e)
1261             {
1262                 throw new ExportException(getClass());
1263             }
1264         }
1265 
1266         if (!usingFilter && characterEncoding != null && mimeType.indexOf("charset") == -1) //$NON-NLS-1$
1267         {
1268             mimeType += "; charset=" + characterEncoding; //$NON-NLS-1$
1269         }
1270 
1271         response.setContentType(mimeType);
1272 
1273         if (StringUtils.isNotEmpty(filename))
1274         {
1275             response.setHeader("Content-Disposition", //$NON-NLS-1$
1276                 "attachment; filename=\"" + filename + "\""); //$NON-NLS-1$ //$NON-NLS-2$
1277         }
1278 
1279         if (exportView instanceof TextExportView)
1280         {
1281             Writer writer;
1282             if (usingFilter)
1283             {
1284                 writer = response.getWriter();
1285             }
1286             else
1287             {
1288                 writer = pageContext.getOut();
1289             }
1290 
1291             ((TextExportView) exportView).doExport(writer);
1292         }
1293         else if (exportView instanceof BinaryExportView)
1294         {
1295             // dealing with binary content
1296             // note that this is not assured to work on any application server if the filter is not enabled. According
1297             // to the jsp specs response.getOutputStream() should no be called in jsps.
1298             ((BinaryExportView) exportView).doExport(response.getOutputStream());
1299         }
1300         else
1301         {
1302             throw new JspTagException("Export view "
1303                 + exportView.getClass().getName()
1304                 + " must implement TextExportView or BinaryExportView");
1305         }
1306 
1307         log.debug("Export completed");
1308 
1309     }
1310 
1311     /***
1312      * This sets the list of all of the data that will be displayed on the page via the table tag. This might include
1313      * just a subset of the total data in the list due to to paging being active, or the user asking us to just show a
1314      * subset, etc...
1315      */
1316     protected void setupViewableData()
1317     {
1318 
1319         // If the user has changed the way our default behavior works, then we need to look for it now, and resort
1320         // things if needed before we ask for the viewable part. (this is a bad place for this, this should be
1321         // refactored and moved somewhere else).
1322 
1323         if (this.tableModel.isSortFullTable())
1324         {
1325             // Sort the total list...
1326             this.tableModel.sortFullList();
1327         }
1328 
1329         Object originalData = this.tableModel.getRowListFull();
1330 
1331         // If they have asked for a subset of the list via the length
1332         // attribute, then only fetch those items out of the master list.
1333         List fullList = CollectionUtil.getListFromObject(originalData, this.offset, this.length);
1334 
1335         int pageOffset = this.offset;
1336         // If they have asked for just a page of the data, then use the
1337         // SmartListHelper to figure out what page they are after, etc...
1338         if (this.pagesize > 0)
1339         {
1340             this.listHelper = new SmartListHelper(fullList, fullList.size(), this.pagesize, this.properties);
1341             this.listHelper.setCurrentPage(this.pageNumber);
1342             pageOffset = this.listHelper.getFirstIndexForCurrentPage();
1343             fullList = this.listHelper.getListForCurrentPage();
1344         }
1345 
1346         this.tableModel.setRowListPage(fullList);
1347         this.tableModel.setPageOffset(pageOffset);
1348     }
1349 
1350     /***
1351      * called when data have to be displayed in a html page.
1352      * @throws JspException generic exception
1353      */
1354     private void writeHTMLData() throws JspException
1355     {
1356         JspWriter out = this.pageContext.getOut();
1357 
1358         if (log.isDebugEnabled())
1359         {
1360             log.debug("[" + getUid() + "] getHTMLData called for table [" + getUid() + "]");
1361         }
1362 
1363         boolean noItems = this.tableModel.getRowListPage().size() == 0;
1364 
1365         if (noItems && !this.properties.getEmptyListShowTable())
1366         {
1367             write(this.properties.getEmptyListMessage(), out);
1368             return;
1369         }
1370 
1371         // variables to hold the previous row columns values.
1372         this.previousRow = new Hashtable(10);
1373 
1374         // variables to hold next row column values.
1375         this.nextRow = new Hashtable(10);
1376 
1377         // Put the page stuff there if it needs to be there...
1378         if (this.properties.getAddPagingBannerTop())
1379         {
1380             // search result and navigation bar
1381             writeSearchResultAndNavigation();
1382         }
1383 
1384         String css = this.properties.getCssTable();
1385         if (StringUtils.isNotBlank(css))
1386         {
1387             this.addClass(css);
1388         }
1389 
1390         // open table
1391         write(getOpenTag(), out);
1392 
1393         // caption
1394         if (this.caption != null)
1395         {
1396             write(this.caption, out);
1397         }
1398 
1399         // thead
1400         if (this.properties.getShowHeader())
1401         {
1402             writeTableHeader();
1403         }
1404 
1405         if (this.footer != null)
1406         {
1407             write(TagConstants.TAG_TFOOTER_OPEN, out);
1408             write(this.footer, out);
1409             write(TagConstants.TAG_TFOOTER_CLOSE, out);
1410             // reset footer
1411             this.footer = null;
1412         }
1413 
1414         // open table body
1415         write(TagConstants.TAG_TBODY_OPEN, out);
1416 
1417         // write table body
1418         writeTableBody();
1419 
1420         // close table body
1421         write(TagConstants.TAG_TBODY_CLOSE, out);
1422 
1423         // close table
1424         write(getCloseTag(), out);
1425 
1426         writeTableFooter();
1427 
1428         if (this.tableModel.getTableDecorator() != null)
1429         {
1430             this.tableModel.getTableDecorator().finish();
1431         }
1432 
1433         if (log.isDebugEnabled())
1434         {
1435             log.debug("[" + getUid() + "] getHTMLData end");
1436         }
1437     }
1438 
1439     /***
1440      * Generates the table header, including the first row of the table which displays the titles of the columns.
1441      */
1442     private void writeTableHeader()
1443     {
1444         JspWriter out = this.pageContext.getOut();
1445 
1446         if (log.isDebugEnabled())
1447         {
1448             log.debug("[" + getUid() + "] getTableHeader called");
1449         }
1450 
1451         // open thead
1452         write(TagConstants.TAG_THEAD_OPEN, out);
1453 
1454         // open tr
1455         write(TagConstants.TAG_TR_OPEN, out);
1456 
1457         // no columns?
1458         if (this.tableModel.isEmpty())
1459         {
1460             write(TagConstants.TAG_TH_OPEN, out);
1461             write(TagConstants.TAG_TH_CLOSE, out);
1462         }
1463 
1464         // iterator on columns for header
1465         Iterator iterator = this.tableModel.getHeaderCellList().iterator();
1466 
1467         while (iterator.hasNext())
1468         {
1469             // get the header cell
1470             HeaderCell headerCell = (HeaderCell) iterator.next();
1471 
1472             if (headerCell.getSortable())
1473             {
1474                 String cssSortable = this.properties.getCssSortable();
1475                 headerCell.addHeaderClass(cssSortable);
1476             }
1477 
1478             // if sorted add styles
1479             if (headerCell.isAlreadySorted())
1480             {
1481                 // sorted css class
1482                 headerCell.addHeaderClass(this.properties.getCssSorted());
1483 
1484                 // sort order css class
1485                 headerCell.addHeaderClass(this.properties.getCssOrder(this.tableModel.isSortOrderAscending()));
1486             }
1487 
1488             // append th with html attributes
1489             write(headerCell.getHeaderOpenTag(), out);
1490 
1491             // title
1492             String header = headerCell.getTitle();
1493 
1494             // column is sortable, create link
1495             if (headerCell.getSortable())
1496             {
1497                 // creates the link for sorting
1498                 Anchor anchor = new Anchor(getSortingHref(headerCell), header);
1499 
1500                 // append to buffer
1501                 header = anchor.toString();
1502             }
1503 
1504             write(header, out);
1505             write(headerCell.getHeaderCloseTag(), out);
1506         }
1507 
1508         // close tr
1509         write(TagConstants.TAG_TR_CLOSE, out);
1510 
1511         // close thead
1512         write(TagConstants.TAG_THEAD_CLOSE, out);
1513 
1514         if (log.isDebugEnabled())
1515         {
1516             log.debug("[" + getUid() + "] getTableHeader end");
1517         }
1518     }
1519 
1520     /***
1521      * Generates the link to be added to a column header for sorting.
1522      * @param headerCell header cell the link should be added to
1523      * @return Href for sorting
1524      */
1525     private Href getSortingHref(HeaderCell headerCell)
1526     {
1527         // costruct Href from base href, preserving parameters
1528         Href href = new Href(this.baseHref);
1529 
1530         // add column number as link parameter
1531         href.addParameter(encodeParameter(TableTagParameters.PARAMETER_SORT), headerCell.getColumnNumber());
1532 
1533         boolean nowOrderAscending = !(headerCell.isAlreadySorted() && this.tableModel.isSortOrderAscending());
1534 
1535         int sortOrderParam = nowOrderAscending ? SortOrderEnum.ASCENDING.getCode() : SortOrderEnum.DESCENDING.getCode();
1536         href.addParameter(encodeParameter(TableTagParameters.PARAMETER_ORDER), sortOrderParam);
1537 
1538         // If user want to sort the full table I need to reset the page number.
1539         if (this.tableModel.isSortFullTable())
1540         {
1541             href.addParameter(encodeParameter(TableTagParameters.PARAMETER_PAGE), 1);
1542         }
1543 
1544         return href;
1545     }
1546 
1547     /***
1548      * This takes a column value and grouping index as the argument. It then groups the column and returns the
1549      * appropriate string back to the caller.
1550      * @param value String
1551      * @param group int
1552      * @return String
1553      */
1554     private String groupColumns(String value, int group)
1555     {
1556 
1557         if ((group == 1) && this.nextRow.size() > 0)
1558         {
1559             // we are at the begining of the next row so copy the contents from nextRow to the previousRow.
1560             this.previousRow.clear();
1561             this.previousRow.putAll(this.nextRow);
1562             this.nextRow.clear();
1563         }
1564 
1565         if (!this.nextRow.containsKey(new Integer(group)))
1566         {
1567             // Key not found in the nextRow so adding this key now...
1568             // remember all the old values.
1569             this.nextRow.put(new Integer(group), value);
1570         }
1571 
1572         // Start comparing the value we received, along with the grouping index.
1573         // if no matching value is found in the previous row then return the value.
1574         // if a matching value is found then this value should not get printed out
1575         // so return an empty String
1576         if (this.previousRow.containsKey(new Integer(group)))
1577         {
1578             for (int j = 1; j <= group; j++)
1579             {
1580 
1581                 if (!((String) this.previousRow.get(new Integer(j))).equals((this.nextRow.get(new Integer(j)))))
1582                 {
1583                     // no match found so return this value back to the caller.
1584                     return value;
1585                 }
1586             }
1587         }
1588 
1589         // This is used, for when there is no data in the previous row,
1590         // It gets used only the first time.
1591         if (this.previousRow.size() == 0)
1592         {
1593             return value;
1594         }
1595 
1596         // There is corresponding value in the previous row
1597         // this value doesn't need to be printed, return an empty String
1598         return TagConstants.EMPTY_STRING;
1599     }
1600 
1601     /***
1602      * Writes the table body content.
1603      * @throws ObjectLookupException for errors in looking up properties in objects
1604      * @throws DecoratorException for errors returned by decorators
1605      */
1606     private void writeTableBody() throws ObjectLookupException, DecoratorException
1607     {
1608         JspWriter out = this.pageContext.getOut();
1609 
1610         // Ok, start bouncing through our list (only the visible part)
1611         RowIterator rowIterator = this.tableModel.getRowIterator(false);
1612 
1613         // iterator on rows
1614         while (rowIterator.hasNext())
1615         {
1616             Row row = rowIterator.next();
1617             if (log.isDebugEnabled())
1618             {
1619                 log.debug("[" + getUid() + "] rowIterator.next()=" + row);
1620             }
1621             if (this.tableModel.getTableDecorator() != null)
1622             {
1623                 String stringStartRow = this.tableModel.getTableDecorator().startRow();
1624                 if (stringStartRow != null)
1625                 {
1626                     write(stringStartRow, out);
1627                 }
1628             }
1629 
1630             // open tr
1631             write(row.getOpenTag(), out);
1632 
1633             // iterator on columns
1634             if (log.isDebugEnabled())
1635             {
1636                 log.debug("[" + getUid() + "] creating ColumnIterator on " + this.tableModel.getHeaderCellList());
1637             }
1638             ColumnIterator columnIterator = row.getColumnIterator(this.tableModel.getHeaderCellList());
1639 
1640             while (columnIterator.hasNext())
1641             {
1642                 Column column = columnIterator.nextColumn();
1643 
1644                 // Get the value to be displayed for the column
1645                 write(column.getOpenTag(), out);
1646                 String value = column.getChoppedAndLinkedValue();
1647 
1648                 // check if column is grouped
1649                 if (column.getGroup() != -1)
1650                 {
1651                     value = this.groupColumns(value, column.getGroup());
1652                 }
1653 
1654                 // add column value
1655                 write(value, out);
1656                 write(column.getCloseTag(), out);
1657             }
1658 
1659             // no columns?
1660             if (this.tableModel.isEmpty())
1661             {
1662                 if (log.isDebugEnabled())
1663                 {
1664                     log.debug("[" + getUid() + "] table has no columns");
1665                 }
1666                 write(TagConstants.TAG_TD_OPEN, out);
1667                 write(row.getObject().toString(), out);
1668                 write(TagConstants.TAG_TD_CLOSE, out);
1669             }
1670 
1671             // close tr
1672             write(row.getCloseTag(), out);
1673 
1674             if (this.tableModel.getTableDecorator() != null)
1675             {
1676                 String endRow = this.tableModel.getTableDecorator().finishRow();
1677                 if (endRow != null)
1678                 {
1679                     write(endRow, out);
1680                 }
1681             }
1682         }
1683 
1684         if (this.tableModel.getRowListPage().size() == 0)
1685         {
1686             write(MessageFormat.format(properties.getEmptyListRowMessage(), new Object[]{new Integer(this.tableModel
1687                 .getNumberOfColumns())}), out);
1688         }
1689     }
1690 
1691     /***
1692      * Generates table footer with links for export commands.
1693      */
1694     private void writeTableFooter()
1695     {
1696         // Put the page stuff there if it needs to be there...
1697         if (this.properties.getAddPagingBannerBottom())
1698         {
1699             writeSearchResultAndNavigation();
1700         }
1701 
1702         // add export links (only if the table is not empty)
1703         if (this.export && this.tableModel.getRowListPage().size() != 0)
1704         {
1705             writeExportLinks();
1706         }
1707     }
1708 
1709     /***
1710      * generates the search result and navigation bar.
1711      */
1712     private void writeSearchResultAndNavigation()
1713     {
1714         if (this.pagesize != 0 && this.listHelper != null)
1715         {
1716             // create a new href
1717             Href navigationHref = new Href(this.baseHref);
1718 
1719             write(this.listHelper.getSearchResultsSummary());
1720             write(this.listHelper.getPageNavigationBar(
1721                 navigationHref,
1722                 encodeParameter(TableTagParameters.PARAMETER_PAGE)));
1723         }
1724     }
1725 
1726     /***
1727      * Writes the formatted export links section.
1728      */
1729     private void writeExportLinks()
1730     {
1731         // Figure out what formats they want to export, make up a little string
1732         Href exportHref = new Href(this.baseHref);
1733 
1734         StringBuffer buffer = new StringBuffer(200);
1735         Iterator iterator = MediaTypeEnum.iterator();
1736 
1737         while (iterator.hasNext())
1738         {
1739             MediaTypeEnum currentExportType = (MediaTypeEnum) iterator.next();
1740 
1741             if (this.properties.getAddExport(currentExportType))
1742             {
1743 
1744                 if (buffer.length() > 0)
1745                 {
1746                     buffer.append(this.properties.getExportBannerSeparator());
1747                 }
1748 
1749                 exportHref.addParameter(encodeParameter(TableTagParameters.PARAMETER_EXPORTTYPE), currentExportType
1750                     .getCode());
1751 
1752                 // export marker
1753                 exportHref.addParameter(TableTagParameters.PARAMETER_EXPORTING, "1");
1754 
1755                 Anchor anchor = new Anchor(exportHref, this.properties.getExportLabel(currentExportType));
1756                 buffer.append(anchor.toString());
1757             }
1758         }
1759 
1760         String[] exportOptions = {buffer.toString()};
1761         write(MessageFormat.format(this.properties.getExportBanner(), exportOptions));
1762     }
1763 
1764     /***
1765      * Called by the setProperty tag to override some default behavior or text String.
1766      * @param propertyName String property name
1767      * @param propertyValue String property value
1768      */
1769     public void setProperty(String propertyName, String propertyValue)
1770     {
1771         this.properties.setProperty(propertyName, propertyValue);
1772     }
1773 
1774     /***
1775      * @see javax.servlet.jsp.tagext.Tag#release()
1776      */
1777     public void release()
1778     {
1779         super.release();
1780 
1781         // tag attributes
1782         this.decoratorName = null;
1783         this.defaultSortedColumn = -1;
1784         this.defaultSortOrder = null;
1785         this.export = false;
1786         this.length = 0;
1787         this.listAttribute = null;
1788         this.name = null;
1789         this.offset = 0;
1790         this.pagesize = 0;
1791         this.property = null;
1792         this.requestUri = null;
1793         this.dontAppendContext = false;
1794         this.scope = null;
1795         this.sortFullTable = null;
1796         this.excludedParams = null;
1797         this.filteredRows = null;
1798         this.uid = null;
1799     }
1800 
1801     /***
1802      * Returns the name.
1803      * @return String
1804      */
1805     public String getName()
1806     {
1807         return this.name;
1808     }
1809 
1810     /***
1811      * encode a parameter name to be unique in the page using ParamEncoder.
1812      * @param parameterName parameter name to encode
1813      * @return String encoded parameter name
1814      */
1815     private String encodeParameter(String parameterName)
1816     {
1817         // paramEncoder has been already instantiated?
1818         if (this.paramEncoder == null)
1819         {
1820             // use the id attribute to get the unique identifier
1821             this.paramEncoder = new ParamEncoder(getUid());
1822         }
1823 
1824         return this.paramEncoder.encodeParameterName(parameterName);
1825     }
1826 
1827 }