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