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.util.Collection;
19  import java.util.HashMap;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.Map;
23  
24  import javax.servlet.http.HttpServletRequest;
25  import javax.servlet.http.HttpServletResponse;
26  import javax.servlet.jsp.JspException;
27  import javax.servlet.jsp.JspTagException;
28  import javax.servlet.jsp.JspWriter;
29  
30  import org.apache.commons.beanutils.BeanUtils;
31  import org.apache.commons.collections.IteratorUtils;
32  import org.apache.commons.lang.ObjectUtils;
33  import org.apache.commons.lang.StringUtils;
34  import org.apache.commons.lang.math.LongRange;
35  import org.apache.commons.lang.math.NumberUtils;
36  import org.apache.commons.lang.math.Range;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  import org.displaytag.Messages;
40  import org.displaytag.decorator.TableDecorator;
41  import org.displaytag.exception.ExportException;
42  import org.displaytag.exception.FactoryInstantiationException;
43  import org.displaytag.exception.InvalidTagAttributeValueException;
44  import org.displaytag.exception.WrappedRuntimeException;
45  import org.displaytag.export.BinaryExportView;
46  import org.displaytag.export.ExportView;
47  import org.displaytag.export.ExportViewFactory;
48  import org.displaytag.export.TextExportView;
49  import org.displaytag.model.Cell;
50  import org.displaytag.model.Column;
51  import org.displaytag.model.HeaderCell;
52  import org.displaytag.model.Row;
53  import org.displaytag.model.TableModel;
54  import org.displaytag.pagination.PaginatedList;
55  import org.displaytag.pagination.PaginatedListSmartListHelper;
56  import org.displaytag.pagination.SmartListHelper;
57  import org.displaytag.properties.MediaTypeEnum;
58  import org.displaytag.properties.SortOrderEnum;
59  import org.displaytag.properties.TableProperties;
60  import org.displaytag.render.HtmlTableWriter;
61  import org.displaytag.util.CollectionUtil;
62  import org.displaytag.util.DependencyChecker;
63  import org.displaytag.util.Href;
64  import org.displaytag.util.ParamEncoder;
65  import org.displaytag.util.RequestHelper;
66  import org.displaytag.util.RequestHelperFactory;
67  import org.displaytag.util.TagConstants;
68  
69  
70  /**
71   * This tag takes a list of objects and creates a table to display those objects. With the help of column tags, you
72   * simply provide the name of properties (get Methods) that are called against the objects in your list that gets
73   * displayed. This tag works very much like the struts iterator tag, most of the attributes have the same name and
74   * functionality as the struts tag.
75   * @author mraible
76   * @author Fabrizio Giustina
77   * @version $Revision: 1081 $ ($Author: fgiust $)
78   */
79  public class TableTag extends HtmlTableTag
80  {
81  
82      /**
83       * name of the attribute added to page scope when exporting, containing an MediaTypeEnum this can be used in column
84       * content to detect the output type and to return different data when exporting.
85       */
86      public static final String PAGE_ATTRIBUTE_MEDIA = "mediaType"; //$NON-NLS-1$
87  
88      /**
89       * If this variable is found in the request, assume the export filter is enabled.
90       */
91      public static final String FILTER_CONTENT_OVERRIDE_BODY = //
92      "org.displaytag.filter.ResponseOverrideFilter.CONTENT_OVERRIDE_BODY"; //$NON-NLS-1$
93  
94      /**
95       * D1597A17A6.
96       */
97      private static final long serialVersionUID = 899149338534L;
98  
99      /**
100      * logger.
101      */
102     private static Log log = LogFactory.getLog(TableTag.class);
103 
104     /**
105      * RequestHelperFactory instance used for link generation.
106      */
107     private static RequestHelperFactory rhf;
108 
109     /**
110      * Object (collection, list) on which the table is based. This is not set directly using a tag attribute and can be
111      * cleaned.
112      */
113     protected Object list;
114 
115     // -- start tag attributes --
116 
117     /**
118      * Object (collection, list) on which the table is based. Set directly using the "list" attribute or evaluated from
119      * expression.
120      */
121     protected Object listAttribute;
122 
123     /**
124      * actual row number, updated during iteration.
125      */
126     private int rowNumber = 1;
127 
128     /**
129      * name of the object to use for iteration. Can contain expressions.
130      */
131     private String name;
132 
133     /**
134      * length of list to display.
135      */
136     private int length;
137 
138     /**
139      * table decorator class name.
140      */
141     private String decoratorName;
142 
143     /**
144      * page size.
145      */
146     private int pagesize;
147 
148     /**
149      * list contains only viewable data.
150      */
151     private boolean partialList;
152 
153     /**
154      * add export links.
155      */
156     private boolean export;
157 
158     /**
159      * list offset.
160      */
161     private int offset;
162 
163     /**
164      * Integer containing total size of the data displaytag is paginating
165      */
166     private Object size;
167 
168     /**
169      * Name of the Integer in some scope containing the size of the data displaytag is paginating
170      */
171     private String sizeObjectName;
172 
173     /**
174      * sort the full list?
175      */
176     private Boolean sortFullTable;
177 
178     /**
179      * are we doing any local sorting? (defaults to True)
180      */
181     private boolean localSort = true;
182 
183     /**
184      * Request uri.
185      */
186     private String requestUri;
187 
188     /**
189      * Prepend application context to generated links.
190      */
191     private boolean dontAppendContext;
192 
193     /**
194      * the index of the column sorted by default.
195      */
196     private int defaultSortedColumn = -1;
197 
198     /**
199      * the sorting order for the sorted column.
200      */
201     private SortOrderEnum defaultSortOrder;
202 
203     /**
204      * Name of parameter which should not be forwarded during sorting or pagination.
205      */
206     private String excludedParams;
207 
208     /**
209      * Unique table id.
210      */
211     private String uid;
212 
213     /**
214      * The variable name to store totals in.
215      */
216     private String varTotals;
217 
218     // -- end tag attributes --
219 
220     /**
221      * table model - initialized in doStartTag().
222      */
223     private TableModel tableModel;
224 
225     /**
226      * current row.
227      */
228     private Row currentRow;
229 
230     /**
231      * next row.
232      */
233 
234     /**
235      * Used by various functions when the person wants to do paging - cleaned in doEndTag().
236      */
237     private SmartListHelper listHelper;
238 
239     /**
240      * base href used for links - set in initParameters().
241      */
242     private Href baseHref;
243 
244     /**
245      * table properties - set in doStartTag().
246      */
247     private TableProperties properties;
248 
249     /**
250      * page number - set in initParameters().
251      */
252     private int pageNumber = 1;
253 
254     /**
255      * Iterator on collection.
256      */
257     private Iterator tableIterator;
258 
259     /**
260      * export type - set in initParameters().
261      */
262     private MediaTypeEnum currentMediaType;
263 
264     /**
265      * daAfterBody() has been executed at least once?
266      */
267     private boolean doAfterBodyExecuted;
268 
269     /**
270      * The param encoder used to generate unique parameter names. Initialized at the first use of encodeParameter().
271      */
272     private ParamEncoder paramEncoder;
273 
274     /**
275      * Static footer added using the footer tag.
276      */
277     private String footer;
278 
279     /**
280      * Is this the last iteration we will be performing? We only output the footer on the last iteration.
281      */
282     private boolean lastIteration;
283 
284     /**
285      * Static caption added using the footer tag.
286      */
287     private String caption;
288 
289     /**
290      * Child caption tag.
291      */
292     private CaptionTag captionTag;
293 
294     /**
295      * Included row range. If no rows can be skipped the range is from 0 to Long.MAX_VALUE. Range check should be always
296      * done using containsLong(). This is an instance of org.apache.commons.lang.math.Range, but it's declared as Object
297      * to avoid runtime errors while Jasper tries to compile the page and commons lang 2.0 is not available. Commons
298      * lang version will be checked in the doStartTag() method in order to provide a more user friendly message.
299      */
300     private Object filteredRows;
301 
302     /**
303      * The paginated list containing the external pagination and sort parameters The presence of this paginated list is
304      * what determines if external pagination and sorting is used or not.
305      */
306     private PaginatedList paginatedList;
307 
308     /**
309      * Is this the last iteration?
310      * @return boolean <code>true</code> if this is the last iteration
311      */
312     protected boolean isLastIteration()
313     {
314         return this.lastIteration;
315     }
316 
317     /**
318      * Sets the list of parameter which should not be forwarded during sorting or pagination.
319      * @param value whitespace separated list of parameters which should not be included (* matches all parameters)
320      */
321     public void setExcludedParams(String value)
322     {
323         this.excludedParams = value;
324     }
325 
326     /**
327      * Sets the content of the footer. Called by a nested footer tag.
328      * @param string footer content
329      */
330     public void setFooter(String string)
331     {
332         this.footer = string;
333         this.tableModel.setFooter(this.footer);
334     }
335 
336     /**
337      * Sets the content of the caption. Called by a nested caption tag.
338      * @param string caption content
339      */
340     public void setCaption(String string)
341     {
342         this.caption = string;
343         this.tableModel.setCaption(this.caption);
344     }
345 
346     /**
347      * Set the child caption tag.
348      * @param captionTag Child caption tag
349      */
350     public void setCaptionTag(CaptionTag captionTag)
351     {
352         this.captionTag = captionTag;
353     }
354 
355     /**
356      * Obtain the child caption tag.
357      * @return The child caption tag
358      */
359     public CaptionTag getCaptionTag()
360     {
361         return this.captionTag;
362     }
363 
364     /**
365      * Is the current row empty?
366      * @return true if the current row is empty
367      */
368     protected boolean isEmpty()
369     {
370         return this.currentRow == null;
371     }
372 
373     /**
374      * set the Integer containing the total size of the data displaytag is paginating
375      * @param size Integer containing the total size of the data
376      */
377     public void setSize(Object size)
378     {
379         if (size instanceof String)
380         {
381             this.sizeObjectName = (String) size;
382         }
383         else
384         {
385             this.size = size;
386         }
387     }
388 
389     /**
390      * set the name of the Integer in some scope containing the total size of the data to be paginated
391      * @param sizeObjectName name of the Integer containing the total size of the data to be paginated
392      */
393     public void setSizeObjectName(String sizeObjectName)
394     {
395         this.sizeObjectName = sizeObjectName;
396     }
397 
398     /**
399      * setter for the "sort" attribute.
400      * @param value "page" (sort a single page) or "list" (sort the full list)
401      * @throws InvalidTagAttributeValueException if value is not "page" or "list"
402      */
403     public void setSort(String value) throws InvalidTagAttributeValueException
404     {
405         if (TableTagParameters.SORT_AMOUNT_PAGE.equals(value))
406         {
407             this.sortFullTable = Boolean.FALSE;
408         }
409         else if (TableTagParameters.SORT_AMOUNT_LIST.equals(value))
410         {
411             this.sortFullTable = Boolean.TRUE;
412         }
413         else if (TableTagParameters.SORT_AMOUNT_EXTERNAL.equals(value))
414         {
415             this.localSort = false;
416         }
417         else
418         {
419             throw new InvalidTagAttributeValueException(getClass(), "sort", value); //$NON-NLS-1$
420         }
421     }
422 
423     /**
424      * setter for the "requestURI" attribute. Context path is automatically added to path starting with "/".
425      * @param value base URI for creating links
426      */
427     public void setRequestURI(String value)
428     {
429         this.requestUri = value;
430     }
431 
432     /**
433      * Setter for the "requestURIcontext" attribute.
434      * @param value base URI for creating links
435      */
436     public void setRequestURIcontext(boolean value)
437     {
438         this.dontAppendContext = !value;
439     }
440 
441     /**
442      * Used to directly set a list (or any object you can iterate on).
443      * @param value Object
444      * @deprecated use setName() to get the object from the page or request scope instead of setting it directly here
445      */
446     public void setList(Object value)
447     {
448         this.listAttribute = value;
449     }
450 
451     /**
452      * Sets the name of the object to use for iteration.
453      * @param value name of the object to use for iteration (can contain expression). It also supports direct setting of
454      * a list, for jsp 2.0 containers where users can set up a data source here using EL expressions.
455      */
456     public void setName(Object value)
457     {
458         if (value instanceof String)
459         {
460             // ok, assuming this is the name of the object
461             this.name = (String) value;
462         }
463         else
464         {
465             // is this the list?
466             this.list = value;
467         }
468     }
469 
470     /**
471      * Sets the name of the object to use for iteration. This setter is needed for jsp 1.1 container which doesn't
472      * support the String - Object conversion. The bean info class will swith to this setter.
473      * @param value name of the object
474      */
475     public void setNameString(String value)
476     {
477         this.name = value;
478     }
479 
480     /**
481      * sets the sorting order for the sorted column.
482      * @param value "ascending" or "descending"
483      * @throws InvalidTagAttributeValueException if value is not one of "ascending" or "descending"
484      */
485     public void setDefaultorder(String value) throws InvalidTagAttributeValueException
486     {
487         this.defaultSortOrder = SortOrderEnum.fromName(value);
488         if (this.defaultSortOrder == null)
489         {
490             throw new InvalidTagAttributeValueException(getClass(), "defaultorder", value); //$NON-NLS-1$
491         }
492     }
493 
494     /**
495      * Setter for the decorator class name.
496      * @param decorator fully qualified name of the table decorator to use
497      */
498     public void setDecorator(String decorator)
499     {
500         this.decoratorName = decorator;
501     }
502 
503     /**
504      * Is export enabled?
505      * @param value <code>true</code> if export should be enabled
506      */
507     public void setExport(boolean value)
508     {
509         this.export = value;
510     }
511 
512     /**
513      * The variable name in which the totals map is stored.
514      * @param varTotalsName the value
515      */
516     public void setVarTotals(String varTotalsName)
517     {
518         this.varTotals = varTotalsName;
519     }
520 
521     /**
522      * Get the name that the totals should be stored under.
523      * @return the var name in pageContext
524      */
525     public String getVarTotals()
526     {
527         return this.varTotals;
528     }
529 
530     /**
531      * sets the number of items to be displayed in the page.
532      * @param value number of items to display in a page
533      */
534     public void setLength(int value)
535     {
536         this.length = value;
537     }
538 
539     /**
540      * sets the index of the default sorted column.
541      * @param value index of the column to sort
542      */
543     public void setDefaultsort(int value)
544     {
545         // subtract one (internal index is 0 based)
546         this.defaultSortedColumn = value - 1;
547     }
548 
549     /**
550      * sets the number of items that should be displayed for a single page.
551      * @param value number of items that should be displayed for a single page
552      */
553     public void setPagesize(int value)
554     {
555         this.pagesize = value;
556     }
557 
558     /**
559      * tells display tag that the values contained in the list are the viewable data only, there may be more results not
560      * given to displaytag
561      * @param partialList boolean value telling us there may be more data not given to displaytag
562      */
563     public void setPartialList(boolean partialList)
564     {
565         this.partialList = partialList;
566     }
567 
568     /**
569      * Setter for the list offset attribute.
570      * @param value String
571      */
572     public void setOffset(int value)
573     {
574         if (value < 1)
575         {
576             // negative values has no meaning, simply treat them as 0
577             this.offset = 0;
578         }
579         else
580         {
581             this.offset = value - 1;
582         }
583     }
584 
585     /**
586      * Sets the unique id used to identify for this table.
587      * @param value String
588      */
589     public void setUid(String value)
590     {
591         this.uid = value;
592     }
593 
594     /**
595      * Returns the unique id used to identify for this table.
596      * @return id for this table
597      */
598     public String getUid()
599     {
600         return this.uid;
601     }
602 
603     /**
604      * Returns the properties.
605      * @return TableProperties
606      */
607     protected TableProperties getProperties()
608     {
609         return this.properties;
610     }
611 
612     /**
613      * Returns the base href with parameters. This is the instance used for links, need to be cloned before being
614      * modified.
615      * @return base Href with parameters
616      */
617     protected Href getBaseHref()
618     {
619         return this.baseHref;
620     }
621 
622     /**
623      * Called by interior column tags to help this tag figure out how it is supposed to display the information in the
624      * List it is supposed to display.
625      * @param column an internal tag describing a column in this tableview
626      */
627     public void addColumn(HeaderCell column)
628     {
629         if (log.isDebugEnabled())
630         {
631             log.debug("[" + getUid() + "] addColumn " + column);
632         }
633 
634         if ((this.paginatedList != null) && (column.getSortable()))
635         {
636             String sortCriterion = paginatedList.getSortCriterion();
637 
638             String sortProperty = column.getSortProperty();
639             if (sortProperty == null)
640             {
641                 sortProperty = column.getBeanPropertyName();
642             }
643 
644             if ((sortCriterion != null) && sortCriterion.equals(sortProperty))
645             {
646                 this.tableModel.setSortedColumnNumber(this.tableModel.getNumberOfColumns());
647                 column.setAlreadySorted();
648             }
649         }
650 
651         this.tableModel.addColumnHeader(column);
652     }
653 
654     /**
655      * Adds a cell to the current row. This method is usually called by a contained ColumnTag
656      * @param cell Cell to add to the current row
657      */
658     public void addCell(Cell cell)
659     {
660         // check if null: could be null if list is empty, we don't need to fill rows
661         if (this.currentRow != null)
662         {
663             int columnNumber = this.currentRow.getCellList().size();
664             this.currentRow.addCell(cell);
665 
666             // just be sure that the number of columns has not been altered by conditionally including column tags in
667             // different rows. This is not supported, but better avoid IndexOutOfBounds...
668             if (columnNumber < tableModel.getHeaderCellList().size())
669             {
670                 HeaderCell header = (HeaderCell) tableModel.getHeaderCellList().get(columnNumber);
671                 header.addCell(new Column(header, cell, currentRow));
672             }
673         }
674     }
675 
676     /**
677      * Is this the first iteration?
678      * @return boolean <code>true</code> if this is the first iteration
679      */
680     protected boolean isFirstIteration()
681     {
682         if (log.isDebugEnabled())
683         {
684             log.debug("["
685                 + getUid()
686                 + "] first iteration="
687                 + (this.rowNumber == 1)
688                 + " (row number="
689                 + this.rowNumber
690                 + ")");
691         }
692         // in first iteration this.rowNumber is 1
693         // (this.rowNumber is incremented in doAfterBody)
694         return this.rowNumber == 1;
695     }
696 
697     /**
698      * When the tag starts, we just initialize some of our variables, and do a little bit of error checking to make sure
699      * that the user is not trying to give us parameters that we don't expect.
700      * @return int
701      * @throws JspException generic exception
702      * @see javax.servlet.jsp.tagext.Tag#doStartTag()
703      */
704     public int doStartTag() throws JspException
705     {
706         DependencyChecker.check();
707 
708         // needed before column processing, elsewhere registered views will not be added
709         ExportViewFactory.getInstance();
710 
711         if (log.isDebugEnabled())
712         {
713             log.debug("[" + getUid() + "] doStartTag called");
714         }
715 
716         this.properties = TableProperties.getInstance((HttpServletRequest) pageContext.getRequest());
717         this.tableModel = new TableModel(this.properties, pageContext.getResponse().getCharacterEncoding(), pageContext);
718 
719         // copying id to the table model for logging
720         this.tableModel.setId(getUid());
721 
722         initParameters();
723 
724         this.tableModel.setMedia(this.currentMediaType);
725 
726         Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA);
727         // set the PAGE_ATTRIBUTE_MEDIA attribute in the page scope
728         if (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType))
729         {
730             if (log.isDebugEnabled())
731             {
732                 log.debug("[" + getUid() + "] setting media [" + this.currentMediaType + "] in this.pageContext");
733             }
734             this.pageContext.setAttribute(PAGE_ATTRIBUTE_MEDIA, this.currentMediaType);
735         }
736 
737         doIteration();
738 
739         // always return EVAL_BODY_TAG to get column headers also if the table is empty
740         // using int to avoid deprecation error in compilation using j2ee 1.3
741         return 2;
742     }
743 
744     /**
745      * @see javax.servlet.jsp.tagext.BodyTag#doAfterBody()
746      */
747     public int doAfterBody()
748     {
749         // doAfterBody() has been called, body is not empty
750         this.doAfterBodyExecuted = true;
751 
752         if (log.isDebugEnabled())
753         {
754             log.debug("[" + getUid() + "] doAfterBody called - iterating on row " + this.rowNumber);
755         }
756 
757         // increment this.rowNumber
758         this.rowNumber++;
759 
760         // Call doIteration() to do the common work
761         return doIteration();
762     }
763 
764     /**
765      * Utility method that is used by both doStartTag() and doAfterBody() to perform an iteration.
766      * @return <code>int</code> either EVAL_BODY_TAG or SKIP_BODY depending on whether another iteration is desired.
767      */
768     protected int doIteration()
769     {
770 
771         if (log.isDebugEnabled())
772         {
773             log.debug("[" + getUid() + "] doIteration called");
774         }
775 
776         // Row already filled?
777         if (this.currentRow != null)
778         {
779             // if yes add to table model and remove
780             this.tableModel.addRow(this.currentRow);
781             this.currentRow = null;
782         }
783 
784         if (this.tableIterator.hasNext())
785         {
786 
787             Object iteratedObject = this.tableIterator.next();
788             if (getUid() != null)
789             {
790                 if ((iteratedObject != null))
791                 {
792                     // set object into this.pageContext
793                     if (log.isDebugEnabled())
794                     {
795                         log.debug("[" + getUid() + "] setting attribute \"" + getUid() + "\" in pageContext");
796                     }
797                     this.pageContext.setAttribute(getUid(), iteratedObject);
798 
799                 }
800                 else
801                 {
802                     // if row is null remove previous object
803                     this.pageContext.removeAttribute(getUid());
804                 }
805                 // set the current row number into this.pageContext
806                 this.pageContext.setAttribute(getUid() + TableTagExtraInfo.ROWNUM_SUFFIX, new Integer(this.rowNumber));
807             }
808 
809             // Row object for Cell values
810             this.currentRow = new Row(iteratedObject, this.rowNumber);
811 
812             this.lastIteration = !this.tableIterator.hasNext();
813 
814             // new iteration
815             // using int to avoid deprecation error in compilation using j2ee 1.3
816             return 2;
817         }
818         this.lastIteration = true;
819 
820         if (log.isDebugEnabled())
821         {
822             log.debug("[" + getUid() + "] doIteration() - iterator ended after " + (this.rowNumber - 1) + " rows");
823         }
824 
825         // end iteration
826         return SKIP_BODY;
827     }
828 
829     /**
830      * Reads parameters from the request and initialize all the needed table model attributes.
831      * @throws FactoryInstantiationException for problems in instantiating a RequestHelperFactory
832      */
833     private void initParameters() throws JspTagException, FactoryInstantiationException
834     {
835 
836         if (rhf == null)
837         {
838             // first time initialization
839             rhf = this.properties.getRequestHelperFactoryInstance();
840         }
841 
842         String fullName = getFullObjectName();
843 
844         // only evaluate if needed, else use list attribute
845         if (fullName != null)
846         {
847             this.list = evaluateExpression(fullName);
848         }
849         else if (this.list == null)
850         {
851             // needed to allow removing the collection of objects if not set directly
852             this.list = this.listAttribute;
853         }
854 
855         if (this.list instanceof PaginatedList)
856         {
857             this.paginatedList = (PaginatedList) this.list;
858             this.list = this.paginatedList.getList();
859         }
860 
861         // set the table model to perform in memory local sorting
862         this.tableModel.setLocalSort(this.localSort && (this.paginatedList == null));
863 
864         RequestHelper requestHelper = rhf.getRequestHelperInstance(this.pageContext);
865 
866         initHref(requestHelper);
867 
868         Integer pageNumberParameter = requestHelper.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_PAGE));
869         this.pageNumber = (pageNumberParameter == null) ? 1 : pageNumberParameter.intValue();
870 
871         int sortColumn = -1;
872         if (!this.tableModel.isLocalSort())
873         {
874             // our sort column parameter may be a string, check that first
875             String sortColumnName = requestHelper.getParameter(encodeParameter(TableTagParameters.PARAMETER_SORT));
876 
877             // if usename is not null, sortColumnName is the name, if not is the column index
878             String usename = requestHelper.getParameter(encodeParameter(TableTagParameters.PARAMETER_SORTUSINGNAME));
879 
880             if (sortColumnName == null)
881             {
882                 this.tableModel.setSortedColumnNumber(this.defaultSortedColumn);
883             }
884             else
885             {
886                 if (usename != null)
887                 {
888 
889                     this.tableModel.setSortedColumnName(sortColumnName); // its a string, set as string
890                 }
891                 else if (NumberUtils.isNumber(sortColumnName))
892                 {
893                     sortColumn = Integer.parseInt(sortColumnName);
894                     this.tableModel.setSortedColumnNumber(sortColumn); // its an int set as normal
895                 }
896             }
897         }
898         else if (this.paginatedList == null)
899         {
900             Integer sortColumnParameter = requestHelper
901                 .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_SORT));
902             sortColumn = (sortColumnParameter == null) ? this.defaultSortedColumn : sortColumnParameter.intValue();
903             this.tableModel.setSortedColumnNumber(sortColumn);
904         }
905         else
906         {
907             sortColumn = defaultSortedColumn;
908         }
909 
910         // default value
911         boolean finalSortFull = this.properties.getSortFullList();
912 
913         // user value for this single table
914         if (this.sortFullTable != null)
915         {
916             finalSortFull = this.sortFullTable.booleanValue();
917         }
918 
919         // if a partial list is used and sort="list" is specified, assume the partial list is already sorted
920         if (!this.partialList || !finalSortFull)
921         {
922             this.tableModel.setSortFullTable(finalSortFull);
923         }
924 
925         if (this.paginatedList == null)
926         {
927             SortOrderEnum paramOrder = SortOrderEnum.fromCode(requestHelper
928                 .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_ORDER)));
929 
930             // if no order parameter is set use default
931             if (paramOrder == null)
932             {
933                 paramOrder = this.defaultSortOrder;
934             }
935 
936             boolean order = SortOrderEnum.DESCENDING != paramOrder;
937             this.tableModel.setSortOrderAscending(order);
938         }
939         else
940         {
941             SortOrderEnum direction = paginatedList.getSortDirection();
942             this.tableModel.setSortOrderAscending(direction == SortOrderEnum.ASCENDING);
943         }
944 
945         Integer exportTypeParameter = requestHelper
946             .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_EXPORTTYPE));
947 
948         this.currentMediaType = (MediaTypeEnum) ObjectUtils.defaultIfNull(
949             MediaTypeEnum.fromCode(exportTypeParameter),
950             MediaTypeEnum.HTML);
951 
952         // if we are doing partialLists then ensure we have our size object
953         if (this.partialList)
954         {
955             if ((this.sizeObjectName == null) && (this.size == null))
956             {
957                 // ?
958             }
959             if (this.sizeObjectName != null)
960             {
961                 // retrieve the object from scope
962                 this.size = evaluateExpression(this.sizeObjectName);
963             }
964             if (size == null)
965             {
966                 throw new JspTagException(Messages.getString("MissingAttributeException.msg", new Object[]{"size"}));
967             }
968             else if (!(size instanceof Integer))
969             {
970                 throw new JspTagException(Messages.getString(
971                     "InvalidTypeException.msg",
972                     new Object[]{"size", "Integer"}));
973             }
974         }
975 
976         // do we really need to skip any row?
977         boolean wishOptimizedIteration = ((this.pagesize > 0 // we are paging
978             || this.offset > 0 // or we are skipping some records using offset
979         || this.length > 0 // or we are limiting the records using length
980         ) && !partialList); // only optimize if we have the full list
981 
982         // can we actually skip any row?
983         if (wishOptimizedIteration && (this.list instanceof Collection) // we need to know the size
984             && ((sortColumn == -1 // and we are not sorting
985             || !finalSortFull // or we are sorting with the "page" behaviour
986             ) && (this.currentMediaType == MediaTypeEnum.HTML // and we are not exporting
987             || !this.properties.getExportFullList()) // or we are exporting a single page
988             ))
989         {
990             int start = 0;
991             int end = 0;
992             if (this.offset > 0)
993             {
994                 start = this.offset;
995             }
996             if (length > 0)
997             {
998                 end = start + this.length;
999             }
1000 
1001             if (this.pagesize > 0)
1002             {
1003                 int fullSize = ((Collection) this.list).size();
1004                 start = (this.pageNumber - 1) * this.pagesize;
1005 
1006                 // invalid page requested, go back to last page
1007                 if (start > fullSize)
1008                 {
1009                     int div = fullSize / this.pagesize;
1010                     start = (fullSize % this.pagesize == 0) ? div : div + 1;
1011                 }
1012 
1013                 end = start + this.pagesize;
1014             }
1015 
1016             // rowNumber starts from 1
1017             filteredRows = new LongRange(start + 1, end);
1018         }
1019         else
1020         {
1021             filteredRows = new LongRange(1, Long.MAX_VALUE);
1022         }
1023 
1024         this.tableIterator = IteratorUtils.getIterator(this.list);
1025     }
1026 
1027     /**
1028      * Is the current row included in the "to-be-evaluated" range? Called by nested ColumnTags. If <code>false</code>
1029      * column body is skipped.
1030      * @return <code>true</code> if the current row must be evaluated because is included in output or because is
1031      * included in sorting.
1032      */
1033     protected boolean isIncludedRow()
1034     {
1035         return ((Range) filteredRows).containsLong(this.rowNumber);
1036     }
1037 
1038     /**
1039      * Create a complete string for compatibility with previous version before expression evaluation. This approach is
1040      * optimized for new expressions, not for previous property/scope parameters.
1041      * @return Expression composed by scope + name + property
1042      */
1043     private String getFullObjectName()
1044     {
1045         // only evaluate if needed, else preserve original list
1046         if (this.name == null)
1047         {
1048             return null;
1049         }
1050 
1051         return this.name;
1052     }
1053 
1054     /**
1055      * init the href object used to generate all the links for pagination, sorting, exporting.
1056      * @param requestHelper request helper used to extract the base Href
1057      */
1058     protected void initHref(RequestHelper requestHelper)
1059     {
1060         // get the href for this request
1061         this.baseHref = requestHelper.getHref();
1062 
1063         if (this.excludedParams != null)
1064         {
1065             String[] splittedExcludedParams = StringUtils.split(this.excludedParams);
1066 
1067             // handle * keyword
1068             if (splittedExcludedParams.length == 1 && "*".equals(splittedExcludedParams[0]))
1069             {
1070                 // @todo cleanup: paramEncoder initialization should not be done here
1071                 if (this.paramEncoder == null)
1072                 {
1073                     this.paramEncoder = new ParamEncoder(getUid());
1074                 }
1075 
1076                 Iterator paramsIterator = baseHref.getParameterMap().keySet().iterator();
1077                 while (paramsIterator.hasNext())
1078                 {
1079                     String key = (String) paramsIterator.next();
1080 
1081                     // don't remove parameters added by the table tag
1082                     if (!this.paramEncoder.isParameterEncoded(key))
1083                     {
1084                         baseHref.removeParameter(key);
1085                     }
1086                 }
1087             }
1088             else
1089             {
1090                 for (int j = 0; j < splittedExcludedParams.length; j++)
1091                 {
1092                     baseHref.removeParameter(splittedExcludedParams[j]);
1093                 }
1094             }
1095         }
1096 
1097         if (this.requestUri != null)
1098         {
1099             // if user has added a requestURI create a new href
1100             String fullURI = requestUri;
1101             if (!this.dontAppendContext)
1102             {
1103                 String contextPath = ((HttpServletRequest) this.pageContext.getRequest()).getContextPath();
1104 
1105                 // prepend the context path if any.
1106                 // actually checks if context path is already there for people which manually add it
1107                 if (!StringUtils.isEmpty(contextPath)
1108                     && requestUri != null
1109                     && requestUri.startsWith("/")
1110                     && !requestUri.startsWith(contextPath))
1111                 {
1112                     fullURI = contextPath + this.requestUri;
1113                 }
1114             }
1115 
1116             // call encodeURL to preserve session id when cookies are disabled
1117             fullURI = ((HttpServletResponse) this.pageContext.getResponse()).encodeURL(fullURI);
1118 
1119             baseHref.setFullUrl(fullURI);
1120 
1121             // // ... and copy parameters from the current request
1122             // Map parameterMap = normalHref.getParameterMap();
1123             // this.baseHref.addParameterMap(parameterMap);
1124         }
1125 
1126     }
1127 
1128     /**
1129      * Draw the table. This is where everything happens, we figure out what values we are supposed to be showing, we
1130      * figure out how we are supposed to be showing them, then we draw them.
1131      * @return int
1132      * @throws JspException generic exception
1133      * @see javax.servlet.jsp.tagext.Tag#doEndTag()
1134      */
1135     public int doEndTag() throws JspException
1136     {
1137 
1138         if (log.isDebugEnabled())
1139         {
1140             log.debug("[" + getUid() + "] doEndTag called");
1141         }
1142 
1143         if (!this.doAfterBodyExecuted)
1144         {
1145             if (log.isDebugEnabled())
1146             {
1147                 log.debug("[" + getUid() + "] tag body is empty.");
1148             }
1149 
1150             // first row (created in doStartTag)
1151             if (this.currentRow != null)
1152             {
1153                 // if yes add to table model and remove
1154                 this.tableModel.addRow(this.currentRow);
1155             }
1156 
1157             // other rows
1158             while (this.tableIterator.hasNext())
1159             {
1160                 Object iteratedObject = this.tableIterator.next();
1161                 this.rowNumber++;
1162 
1163                 // Row object for Cell values
1164                 this.currentRow = new Row(iteratedObject, this.rowNumber);
1165 
1166                 this.tableModel.addRow(this.currentRow);
1167             }
1168         }
1169 
1170         // if no rows are defined automatically get all properties from bean
1171         if (this.tableModel.isEmpty())
1172         {
1173             describeEmptyTable();
1174         }
1175 
1176         TableDecorator tableDecorator = this.properties.getDecoratorFactoryInstance().
1177                                         loadTableDecorator(this.pageContext, getConfiguredDecoratorName());
1178 
1179         if (tableDecorator != null)
1180         {
1181             tableDecorator.init(this.pageContext, this.list, this.tableModel);
1182             this.tableModel.setTableDecorator(tableDecorator);
1183         }
1184 
1185         setupViewableData();
1186 
1187         // Figure out how we should sort this data, typically we just sort
1188         // the data being shown, but the programmer can override this behavior
1189         if (this.paginatedList == null && this.tableModel.isLocalSort())
1190         {
1191             if (!this.tableModel.isSortFullTable())
1192             {
1193                 this.tableModel.sortPageList();
1194             }
1195         }
1196 
1197         // Get the data back in the representation that the user is after, do they want HTML/XML/CSV/EXCEL/etc...
1198         int returnValue = EVAL_PAGE;
1199 
1200         // check for nested tables
1201         // Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA);
1202         Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA);
1203         if (MediaTypeEnum.HTML.equals(this.currentMediaType)
1204             && (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType)))
1205         {
1206             writeHTMLData();
1207         }
1208         else if (!MediaTypeEnum.HTML.equals(this.currentMediaType))
1209         {
1210             if (log.isDebugEnabled())
1211             {
1212                 log.debug("[" + getUid() + "] doEndTag - exporting");
1213             }
1214 
1215             returnValue = doExport();
1216         }
1217 
1218         // do not remove media attribute! if the table is nested in other tables this is still needed
1219         // this.pageContext.removeAttribute(PAGE_ATTRIBUTE_MEDIA);
1220 
1221         if (log.isDebugEnabled())
1222         {
1223             log.debug("[" + getUid() + "] doEndTag - end");
1224         }
1225 
1226         cleanUp();
1227         return returnValue;
1228     }
1229 
1230     /**
1231      * Returns the name of the table decorator that should be applied to this table,
1232      * which is either the decorator configured in the property "decorator", or if
1233      * none is configured in said property, a decorator configured with the
1234      * "decorator.media.[media type]" property, or null if none is configured.  
1235      * 
1236      * @return Name of the table decorator that should be applied to this table.
1237      */
1238 	private String getConfiguredDecoratorName()
1239     {
1240         String
1241         tableDecoratorName = (this.decoratorName == null) ?
1242                                    this.properties.getMediaTypeDecoratorName(this.currentMediaType) :
1243                                    this.decoratorName;
1244         tableDecoratorName = (tableDecoratorName == null) ?
1245                                    this.properties.getExportDecoratorName(this.currentMediaType) :
1246                                    tableDecoratorName;
1247         return tableDecoratorName;
1248 	}
1249 
1250     /**
1251      * clean up instance variables, but not the ones representing tag attributes.
1252      */
1253     private void cleanUp()
1254     {
1255         // reset instance variables (non attributes)
1256         this.currentMediaType = null;
1257         this.baseHref = null;
1258         this.caption = null;
1259         this.captionTag = null;
1260         this.currentRow = null;
1261         this.doAfterBodyExecuted = false;
1262         this.footer = null;
1263         this.listHelper = null;
1264         this.pageNumber = 0;
1265         this.paramEncoder = null;
1266         this.properties = null;
1267         this.rowNumber = 1;
1268         this.tableIterator = null;
1269         this.tableModel = null;
1270         this.list = null;
1271     }
1272 
1273     /**
1274      * If no columns are provided, automatically add them from bean properties. Get the first object in the list and get
1275      * all the properties (except the "class" property which is automatically skipped). Of course this isn't possible
1276      * for empty lists.
1277      */
1278     private void describeEmptyTable()
1279     {
1280         this.tableIterator = IteratorUtils.getIterator(this.list);
1281 
1282         if (this.tableIterator.hasNext())
1283         {
1284             Object iteratedObject = this.tableIterator.next();
1285             Map objectProperties = new HashMap();
1286 
1287             // if it's a String don't add the "Bytes" column
1288             if (iteratedObject instanceof String)
1289             {
1290                 return;
1291             }
1292             // if it's a map already use key names for column headers
1293             if (iteratedObject instanceof Map)
1294             {
1295                 objectProperties = (Map) iteratedObject;
1296             }
1297             else
1298             {
1299                 try
1300                 {
1301                     objectProperties = BeanUtils.describe(iteratedObject);
1302                 }
1303                 catch (Exception e)
1304                 {
1305                     log.warn("Unable to automatically add columns: " + e.getMessage(), e);
1306                 }
1307             }
1308 
1309             // iterator on properties names
1310             Iterator propertiesIterator = objectProperties.keySet().iterator();
1311 
1312             while (propertiesIterator.hasNext())
1313             {
1314                 // get the property name
1315                 String propertyName = (String) propertiesIterator.next();
1316 
1317                 // dont't want to add the standard "class" property
1318                 if (!"class".equals(propertyName)) //$NON-NLS-1$
1319                 {
1320                     // creates a new header and add to the table model
1321                     HeaderCell headerCell = new HeaderCell();
1322                     headerCell.setBeanPropertyName(propertyName);
1323 
1324                     // handle title i18n
1325                     headerCell.setTitle(this.properties.geResourceProvider().getResource(
1326                         null,
1327                         propertyName,
1328                         this,
1329