/** * Licensed under the Artistic License; you may not use this file * except in compliance with the License. * You may obtain a copy of the License at * * http://displaytag.sourceforge.net/license.html * * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. */ package org.displaytag.tags; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspTagException; import javax.servlet.jsp.JspWriter; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.collections.IteratorUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.math.LongRange; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang.math.Range; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.displaytag.Messages; import org.displaytag.decorator.TableDecorator; import org.displaytag.exception.ExportException; import org.displaytag.exception.FactoryInstantiationException; import org.displaytag.exception.InvalidTagAttributeValueException; import org.displaytag.exception.WrappedRuntimeException; import org.displaytag.export.BinaryExportView; import org.displaytag.export.ExportView; import org.displaytag.export.ExportViewFactory; import org.displaytag.export.TextExportView; import org.displaytag.model.Cell; import org.displaytag.model.Column; import org.displaytag.model.HeaderCell; import org.displaytag.model.Row; import org.displaytag.model.TableModel; import org.displaytag.pagination.PaginatedList; import org.displaytag.pagination.PaginatedListSmartListHelper; import org.displaytag.pagination.SmartListHelper; import org.displaytag.properties.MediaTypeEnum; import org.displaytag.properties.SortOrderEnum; import org.displaytag.properties.TableProperties; import org.displaytag.render.HtmlTableWriter; import org.displaytag.util.CollectionUtil; import org.displaytag.util.DependencyChecker; import org.displaytag.util.Href; import org.displaytag.util.ParamEncoder; import org.displaytag.util.RequestHelper; import org.displaytag.util.RequestHelperFactory; import org.displaytag.util.TagConstants; /** * This tag takes a list of objects and creates a table to display those objects. With the help of column tags, you * simply provide the name of properties (get Methods) that are called against the objects in your list that gets * displayed. This tag works very much like the struts iterator tag, most of the attributes have the same name and * functionality as the struts tag. * @author mraible * @author Fabrizio Giustina * @version $Revision: 8904 $ ($Author: charles $) */ public class TableTag extends HtmlTableTag { /** * name of the attribute added to page scope when exporting, containing an MediaTypeEnum this can be used in column * content to detect the output type and to return different data when exporting. */ public static final String PAGE_ATTRIBUTE_MEDIA = "mediaType"; //$NON-NLS-1$ /** * If this variable is found in the request, assume the export filter is enabled. */ public static final String FILTER_CONTENT_OVERRIDE_BODY = // "org.displaytag.filter.ResponseOverrideFilter.CONTENT_OVERRIDE_BODY"; //$NON-NLS-1$ /** * D1597A17A6. */ private static final long serialVersionUID = 899149338534L; /** * logger. */ private static Log log = LogFactory.getLog(TableTag.class); /** * RequestHelperFactory instance used for link generation. */ private static RequestHelperFactory rhf; /** * Object (collection, list) on which the table is based. This is not set directly using a tag attribute and can be * cleaned. */ protected Object list; // -- start tag attributes -- /** * Object (collection, list) on which the table is based. Set directly using the "list" attribute or evaluated from * expression. */ protected Object listAttribute; /** * actual row number, updated during iteration. */ private int rowNumber = 1; /** * name of the object to use for iteration. Can contain expressions. */ private String name; /** * length of list to display. */ private int length; /** * table decorator class name. */ private String decoratorName; /** * page size. */ private int pagesize; /** * list contains only viewable data. */ private boolean partialList; /** * add export links. */ private boolean export; /** * list offset. */ private int offset; /** * Integer containing total size of the data displaytag is paginating */ private Object size; /** * Name of the Integer in some scope containing the size of the data displaytag is paginating */ private String sizeObjectName; /** * sort the full list? */ private Boolean sortFullTable; /** * are we doing any local sorting? (defaults to True) */ private boolean localSort = true; /** * Request uri. */ private String requestUri; /** * Prepend application context to generated links. */ private boolean dontAppendContext; /** * the index of the column sorted by default. */ private int defaultSortedColumn = -1; /** * the sorting order for the sorted column. */ private SortOrderEnum defaultSortOrder; /** * Name of parameter which should not be forwarded during sorting or pagination. */ private String excludedParams; /** * Unique table id. */ private String uid; /** * The variable name to store totals in. */ private String varTotals; // -- end tag attributes -- /** * table model - initialized in doStartTag(). */ private TableModel tableModel; /** * current row. */ private Row currentRow; /** * next row. */ /** * Used by various functions when the person wants to do paging - cleaned in doEndTag(). */ private SmartListHelper listHelper; /** * base href used for links - set in initParameters(). */ private Href baseHref; /** * table properties - set in doStartTag(). */ private TableProperties properties; /** * page number - set in initParameters(). */ private int pageNumber = 1; /** * Iterator on collection. */ private Iterator tableIterator; /** * export type - set in initParameters(). */ private MediaTypeEnum currentMediaType; /** * daAfterBody() has been executed at least once? */ private boolean doAfterBodyExecuted; /** * The param encoder used to generate unique parameter names. Initialized at the first use of encodeParameter(). */ private ParamEncoder paramEncoder; /** * Static footer added using the footer tag. */ private String footer; /** * Is this the last iteration we will be performing? We only output the footer on the last iteration. */ private boolean lastIteration; /** * Static caption added using the footer tag. */ private String caption; /** * Child caption tag. */ private CaptionTag captionTag; /** * Included row range. If no rows can be skipped the range is from 0 to Long.MAX_VALUE. Range check should be always * done using containsLong(). This is an instance of org.apache.commons.lang.math.Range, but it's declared as Object * to avoid runtime errors while Jasper tries to compile the page and commons lang 2.0 is not available. Commons * lang version will be checked in the doStartTag() method in order to provide a more user friendly message. */ private Object filteredRows; /** * The paginated list containing the external pagination and sort parameters The presence of this paginated list is * what determines if external pagination and sorting is used or not. */ private PaginatedList paginatedList; /** * Is this the last iteration? * @return boolean <code>true</code> if this is the last iteration */ protected boolean isLastIteration() { return this.lastIteration; } /** * Sets the list of parameter which should not be forwarded during sorting or pagination. * @param value whitespace separated list of parameters which should not be included (* matches all parameters) */ public void setExcludedParams(String value) { this.excludedParams = value; } /** * Sets the content of the footer. Called by a nested footer tag. * @param string footer content */ public void setFooter(String string) { this.footer = string; this.tableModel.setFooter(this.footer); } /** * Sets the content of the caption. Called by a nested caption tag. * @param string caption content */ public void setCaption(String string) { this.caption = string; this.tableModel.setCaption(this.caption); } /** * Set the child caption tag. * @param captionTag Child caption tag */ public void setCaptionTag(CaptionTag captionTag) { this.captionTag = captionTag; } /** * Obtain the child caption tag. * @return The child caption tag */ public CaptionTag getCaptionTag() { return this.captionTag; } /** * Is the current row empty? * @return true if the current row is empty */ protected boolean isEmpty() { return this.currentRow == null; } /** * set the Integer containing the total size of the data displaytag is paginating * @param size Integer containing the total size of the data */ public void setSize(Object size) { if (size instanceof String) { this.sizeObjectName = (String) size; } else { this.size = size; } } /** * set the name of the Integer in some scope containing the total size of the data to be paginated * @param sizeObjectName name of the Integer containing the total size of the data to be paginated */ public void setSizeObjectName(String sizeObjectName) { this.sizeObjectName = sizeObjectName; } /** * setter for the "sort" attribute. * @param value "page" (sort a single page) or "list" (sort the full list) * @throws InvalidTagAttributeValueException if value is not "page" or "list" */ public void setSort(String value) throws InvalidTagAttributeValueException { if (TableTagParameters.SORT_AMOUNT_PAGE.equals(value)) { this.sortFullTable = Boolean.FALSE; } else if (TableTagParameters.SORT_AMOUNT_LIST.equals(value)) { this.sortFullTable = Boolean.TRUE; } else if (TableTagParameters.SORT_AMOUNT_EXTERNAL.equals(value)) { this.localSort = false; } else { throw new InvalidTagAttributeValueException(getClass(), "sort", value); //$NON-NLS-1$ } } /** * setter for the "requestURI" attribute. Context path is automatically added to path starting with "/". * @param value base URI for creating links */ public void setRequestURI(String value) { this.requestUri = value; } /** * Setter for the "requestURIcontext" attribute. * @param value base URI for creating links */ public void setRequestURIcontext(boolean value) { this.dontAppendContext = !value; } /** * Used to directly set a list (or any object you can iterate on). * @param value Object * @deprecated use setName() to get the object from the page or request scope instead of setting it directly here */ public void setList(Object value) { this.listAttribute = value; } /** * Sets the name of the object to use for iteration. * @param value name of the object to use for iteration (can contain expression). It also supports direct setting of * a list, for jsp 2.0 containers where users can set up a data source here using EL expressions. */ public void setName(Object value) { if (value instanceof String) { // ok, assuming this is the name of the object this.name = (String) value; } else { // is this the list? this.list = value; } } /** * Sets the name of the object to use for iteration. This setter is needed for jsp 1.1 container which doesn't * support the String - Object conversion. The bean info class will swith to this setter. * @param value name of the object */ public void setNameString(String value) { this.name = value; } /** * sets the sorting order for the sorted column. * @param value "ascending" or "descending" * @throws InvalidTagAttributeValueException if value is not one of "ascending" or "descending" */ public void setDefaultorder(String value) throws InvalidTagAttributeValueException { this.defaultSortOrder = SortOrderEnum.fromName(value); if (this.defaultSortOrder == null) { throw new InvalidTagAttributeValueException(getClass(), "defaultorder", value); //$NON-NLS-1$ } } /** * Setter for the decorator class name. * @param decorator fully qualified name of the table decorator to use */ public void setDecorator(String decorator) { this.decoratorName = decorator; } /** * Is export enabled? * @param value <code>true</code> if export should be enabled */ public void setExport(boolean value) { this.export = value; } /** * The variable name in which the totals map is stored. * @param varTotalsName the value */ public void setVarTotals(String varTotalsName) { this.varTotals = varTotalsName; } /** * Get the name that the totals should be stored under. * @return the var name in pageContext */ public String getVarTotals() { return this.varTotals; } /** * sets the number of items to be displayed in the page. * @param value number of items to display in a page */ public void setLength(int value) { this.length = value; } /** * sets the index of the default sorted column. * @param value index of the column to sort */ public void setDefaultsort(int value) { // subtract one (internal index is 0 based) this.defaultSortedColumn = value - 1; } /** * sets the number of items that should be displayed for a single page. * @param value number of items that should be displayed for a single page */ public void setPagesize(int value) { this.pagesize = value; } /** * tells display tag that the values contained in the list are the viewable data only, there may be more results not * given to displaytag * @param partialList boolean value telling us there may be more data not given to displaytag */ public void setPartialList(boolean partialList) { this.partialList = partialList; } /** * Setter for the list offset attribute. * @param value String */ public void setOffset(int value) { if (value < 1) { // negative values has no meaning, simply treat them as 0 this.offset = 0; } else { this.offset = value - 1; } } /** * Sets the unique id used to identify for this table. * @param value String */ public void setUid(String value) { this.uid = value; } /** * Returns the unique id used to identify for this table. * @return id for this table */ public String getUid() { return this.uid; } /** * Returns the properties. * @return TableProperties */ protected TableProperties getProperties() { return this.properties; } /** * Returns the base href with parameters. This is the instance used for links, need to be cloned before being * modified. * @return base Href with parameters */ protected Href getBaseHref() { return this.baseHref; } /** * Called by interior column tags to help this tag figure out how it is supposed to display the information in the * List it is supposed to display. * @param column an internal tag describing a column in this tableview */ public void addColumn(HeaderCell column) { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] addColumn " + column); } if ((this.paginatedList != null) && (column.getSortable())) { String sortCriterion = paginatedList.getSortCriterion(); String sortProperty = column.getSortProperty(); if (sortProperty == null) { sortProperty = column.getBeanPropertyName(); } if ((sortCriterion != null) && sortCriterion.equals(sortProperty)) { this.tableModel.setSortedColumnNumber(this.tableModel.getNumberOfColumns()); column.setAlreadySorted(); } } this.tableModel.addColumnHeader(column); } /** * Adds a cell to the current row. This method is usually called by a contained ColumnTag * @param cell Cell to add to the current row */ public void addCell(Cell cell) { // check if null: could be null if list is empty, we don't need to fill rows if (this.currentRow != null) { int columnNumber = this.currentRow.getCellList().size(); this.currentRow.addCell(cell); // just be sure that the number of columns has not been altered by conditionally including column tags in // different rows. This is not supported, but better avoid IndexOutOfBounds... if (columnNumber < tableModel.getHeaderCellList().size()) { HeaderCell header = (HeaderCell) tableModel.getHeaderCellList().get(columnNumber); header.addCell(new Column(header, cell, currentRow)); } } } /** * Is this the first iteration? * @return boolean <code>true</code> if this is the first iteration */ protected boolean isFirstIteration() { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] first iteration=" + (this.rowNumber == 1) + " (row number=" + this.rowNumber + ")"); } // in first iteration this.rowNumber is 1 // (this.rowNumber is incremented in doAfterBody) return this.rowNumber == 1; } /** * When the tag starts, we just initialize some of our variables, and do a little bit of error checking to make sure * that the user is not trying to give us parameters that we don't expect. * @return int * @throws JspException generic exception * @see javax.servlet.jsp.tagext.Tag#doStartTag() */ public int doStartTag() throws JspException { DependencyChecker.check(); // needed before column processing, elsewhere registered views will not be added ExportViewFactory.getInstance(); if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doStartTag called"); } this.properties = TableProperties.getInstance((HttpServletRequest) pageContext.getRequest()); this.tableModel = new TableModel(this.properties, pageContext.getResponse().getCharacterEncoding(), pageContext); // copying id to the table model for logging this.tableModel.setId(getUid()); initParameters(); this.tableModel.setMedia(this.currentMediaType); Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA); // set the PAGE_ATTRIBUTE_MEDIA attribute in the page scope if (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType)) { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] setting media [" + this.currentMediaType + "] in this.pageContext"); } this.pageContext.setAttribute(PAGE_ATTRIBUTE_MEDIA, this.currentMediaType); } doIteration(); // always return EVAL_BODY_TAG to get column headers also if the table is empty // using int to avoid deprecation error in compilation using j2ee 1.3 return 2; } /** * @see javax.servlet.jsp.tagext.BodyTag#doAfterBody() */ public int doAfterBody() { // doAfterBody() has been called, body is not empty this.doAfterBodyExecuted = true; if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doAfterBody called - iterating on row " + this.rowNumber); } // increment this.rowNumber this.rowNumber++; // Call doIteration() to do the common work return doIteration(); } /** * Utility method that is used by both doStartTag() and doAfterBody() to perform an iteration. * @return <code>int</code> either EVAL_BODY_TAG or SKIP_BODY depending on whether another iteration is desired. */ protected int doIteration() { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doIteration called"); } // Row already filled? if (this.currentRow != null) { // if yes add to table model and remove this.tableModel.addRow(this.currentRow); this.currentRow = null; } if (this.tableIterator.hasNext()) { Object iteratedObject = this.tableIterator.next(); if (getUid() != null) { if ((iteratedObject != null)) { // set object into this.pageContext if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] setting attribute \"" + getUid() + "\" in pageContext"); } this.pageContext.setAttribute(getUid(), iteratedObject); } else { // if row is null remove previous object this.pageContext.removeAttribute(getUid()); } // set the current row number into this.pageContext this.pageContext.setAttribute(getUid() + TableTagExtraInfo.ROWNUM_SUFFIX, new Integer(this.rowNumber)); } // Row object for Cell values this.currentRow = new Row(iteratedObject, this.rowNumber); this.lastIteration = !this.tableIterator.hasNext(); // new iteration // using int to avoid deprecation error in compilation using j2ee 1.3 return 2; } this.lastIteration = true; if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doIteration() - iterator ended after " + (this.rowNumber - 1) + " rows"); } // end iteration return SKIP_BODY; } /** * Reads parameters from the request and initialize all the needed table model attributes. * @throws FactoryInstantiationException for problems in instantiating a RequestHelperFactory */ private void initParameters() throws JspTagException, FactoryInstantiationException { if (rhf == null) { // first time initialization rhf = this.properties.getRequestHelperFactoryInstance(); } String fullName = getFullObjectName(); // only evaluate if needed, else use list attribute if (fullName != null) { this.list = evaluateExpression(fullName); } else if (this.list == null) { // needed to allow removing the collection of objects if not set directly this.list = this.listAttribute; } if (this.list instanceof PaginatedList) { this.paginatedList = (PaginatedList) this.list; this.list = this.paginatedList.getList(); } // set the table model to perform in memory local sorting this.tableModel.setLocalSort(this.localSort && (this.paginatedList == null)); RequestHelper requestHelper = rhf.getRequestHelperInstance(this.pageContext); initHref(requestHelper); Integer pageNumberParameter = requestHelper.getIntParameter(encodeParameter(TableTagParameters.PARAMETER_PAGE)); this.pageNumber = (pageNumberParameter == null) ? 1 : pageNumberParameter.intValue(); int sortColumn = -1; if (!this.tableModel.isLocalSort()) { // our sort column parameter may be a string, check that first String sortColumnName = requestHelper.getParameter(encodeParameter(TableTagParameters.PARAMETER_SORT)); // if usename is not null, sortColumnName is the name, if not is the column index String usename = requestHelper.getParameter(encodeParameter(TableTagParameters.PARAMETER_SORTUSINGNAME)); if (sortColumnName == null) { this.tableModel.setSortedColumnNumber(this.defaultSortedColumn); } else { if (usename != null) { this.tableModel.setSortedColumnName(sortColumnName); // its a string, set as string } else if (NumberUtils.isNumber(sortColumnName)) { sortColumn = Integer.parseInt(sortColumnName); this.tableModel.setSortedColumnNumber(sortColumn); // its an int set as normal } } } else if (this.paginatedList == null) { Integer sortColumnParameter = requestHelper .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_SORT)); sortColumn = (sortColumnParameter == null) ? this.defaultSortedColumn : sortColumnParameter.intValue(); this.tableModel.setSortedColumnNumber(sortColumn); } else { sortColumn = defaultSortedColumn; } // default value boolean finalSortFull = this.properties.getSortFullList(); // user value for this single table if (this.sortFullTable != null) { finalSortFull = this.sortFullTable.booleanValue(); } // if a partial list is used and sort="list" is specified, assume the partial list is already sorted if (!this.partialList || !finalSortFull) { this.tableModel.setSortFullTable(finalSortFull); } if (this.paginatedList == null) { SortOrderEnum paramOrder = SortOrderEnum.fromCode(requestHelper .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_ORDER))); // if no order parameter is set use default if (paramOrder == null) { paramOrder = this.defaultSortOrder; } boolean order = SortOrderEnum.DESCENDING != paramOrder; this.tableModel.setSortOrderAscending(order); } else { SortOrderEnum direction = paginatedList.getSortDirection(); this.tableModel.setSortOrderAscending(direction == SortOrderEnum.ASCENDING); } Integer exportTypeParameter = requestHelper .getIntParameter(encodeParameter(TableTagParameters.PARAMETER_EXPORTTYPE)); this.currentMediaType = (MediaTypeEnum) ObjectUtils.defaultIfNull( MediaTypeEnum.fromCode(exportTypeParameter), MediaTypeEnum.HTML); // if we are doing partialLists then ensure we have our size object if (this.partialList) { if ((this.sizeObjectName == null) && (this.size == null)) { // ? } if (this.sizeObjectName != null) { // retrieve the object from scope this.size = evaluateExpression(this.sizeObjectName); } if (size == null) { throw new JspTagException(Messages.getString("MissingAttributeException.msg", new Object[]{"size"})); } else if (!(size instanceof Integer)) { throw new JspTagException(Messages.getString( "InvalidTypeException.msg", new Object[]{"size", "Integer"})); } } // do we really need to skip any row? boolean wishOptimizedIteration = ((this.pagesize > 0 // we are paging || this.offset > 0 // or we are skipping some records using offset || this.length > 0 // or we are limiting the records using length ) && !partialList); // only optimize if we have the full list // can we actually skip any row? if (wishOptimizedIteration && (this.list instanceof Collection) // we need to know the size && ((sortColumn == -1 // and we are not sorting || !finalSortFull // or we are sorting with the "page" behaviour ) && (this.currentMediaType == MediaTypeEnum.HTML // and we are not exporting || !this.properties.getExportFullList()) // or we are exporting a single page )) { int start = 0; int end = 0; if (this.offset > 0) { start = this.offset; } if (length > 0) { end = start + this.length; } if (this.pagesize > 0) { int fullSize = ((Collection) this.list).size(); start = (this.pageNumber - 1) * this.pagesize; // invalid page requested, go back to last page if (start > fullSize) { int div = fullSize / this.pagesize; start = (fullSize % this.pagesize == 0) ? div : div + 1; } end = start + this.pagesize; } // rowNumber starts from 1 filteredRows = new LongRange(start + 1, end); } else { filteredRows = new LongRange(1, Long.MAX_VALUE); } this.tableIterator = IteratorUtils.getIterator(this.list); } /** * Is the current row included in the "to-be-evaluated" range? Called by nested ColumnTags. If <code>false</code> * column body is skipped. * @return <code>true</code> if the current row must be evaluated because is included in output or because is * included in sorting. */ protected boolean isIncludedRow() { return ((Range) filteredRows).containsLong(this.rowNumber); } /** * Create a complete string for compatibility with previous version before expression evaluation. This approach is * optimized for new expressions, not for previous property/scope parameters. * @return Expression composed by scope + name + property */ private String getFullObjectName() { // only evaluate if needed, else preserve original list if (this.name == null) { return null; } return this.name; } /** * init the href object used to generate all the links for pagination, sorting, exporting. * @param requestHelper request helper used to extract the base Href */ protected void initHref(RequestHelper requestHelper) { // get the href for this request this.baseHref = requestHelper.getHref(); if (this.excludedParams != null) { String[] splittedExcludedParams = StringUtils.split(this.excludedParams); // handle * keyword if (splittedExcludedParams.length == 1 && "*".equals(splittedExcludedParams[0])) { // @todo cleanup: paramEncoder initialization should not be done here if (this.paramEncoder == null) { this.paramEncoder = new ParamEncoder(getUid()); } Iterator paramsIterator = baseHref.getParameterMap().keySet().iterator(); while (paramsIterator.hasNext()) { String key = (String) paramsIterator.next(); // don't remove parameters added by the table tag if (!this.paramEncoder.isParameterEncoded(key)) { baseHref.removeParameter(key); } } } else { for (int j = 0; j < splittedExcludedParams.length; j++) { baseHref.removeParameter(splittedExcludedParams[j]); } } } if (this.requestUri != null) { // if user has added a requestURI create a new href String fullURI = requestUri; if (!this.dontAppendContext) { String contextPath = ((HttpServletRequest) this.pageContext.getRequest()).getContextPath(); // prepend the context path if any. // actually checks if context path is already there for people which manually add it if (!StringUtils.isEmpty(contextPath) && requestUri != null && requestUri.startsWith("/") && !requestUri.startsWith(contextPath)) { fullURI = contextPath + this.requestUri; } } // call encodeURL to preserve session id when cookies are disabled fullURI = ((HttpServletResponse) this.pageContext.getResponse()).encodeURL(fullURI); baseHref.setFullUrl(fullURI); // // ... and copy parameters from the current request // Map parameterMap = normalHref.getParameterMap(); // this.baseHref.addParameterMap(parameterMap); } } /** * Draw the table. This is where everything happens, we figure out what values we are supposed to be showing, we * figure out how we are supposed to be showing them, then we draw them. * @return int * @throws JspException generic exception * @see javax.servlet.jsp.tagext.Tag#doEndTag() */ public int doEndTag() throws JspException { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doEndTag called"); } if (!this.doAfterBodyExecuted) { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] tag body is empty."); } // first row (created in doStartTag) if (this.currentRow != null) { // if yes add to table model and remove this.tableModel.addRow(this.currentRow); } // other rows while (this.tableIterator.hasNext()) { Object iteratedObject = this.tableIterator.next(); this.rowNumber++; // Row object for Cell values this.currentRow = new Row(iteratedObject, this.rowNumber); this.tableModel.addRow(this.currentRow); } } // if no rows are defined automatically get all properties from bean if (this.tableModel.isEmpty()) { describeEmptyTable(); } // TableDecorator tableDecorator = DecoratorFactory.loadTableDecorator(this.decoratorName); String tableDecoratorName = null; Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA); if (MediaTypeEnum.HTML.equals(this.currentMediaType) && (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType))) { tableDecoratorName = this.decoratorName; } else if (!MediaTypeEnum.HTML.equals(this.currentMediaType)) { tableDecoratorName = this.properties.getExportDecoratorName(this.currentMediaType); } TableDecorator tableDecorator = this.properties.getDecoratorFactoryInstance().loadTableDecorator( this.pageContext, tableDecoratorName); if (tableDecorator != null) { tableDecorator.init(this.pageContext, this.list, this.tableModel); this.tableModel.setTableDecorator(tableDecorator); } setupViewableData(); // Figure out how we should sort this data, typically we just sort // the data being shown, but the programmer can override this behavior if (this.paginatedList == null && this.tableModel.isLocalSort()) { if (!this.tableModel.isSortFullTable()) { this.tableModel.sortPageList(); } } // Get the data back in the representation that the user is after, do they want HTML/XML/CSV/EXCEL/etc... int returnValue = EVAL_PAGE; // check for nested tables // Object previousMediaType = this.pageContext.getAttribute(PAGE_ATTRIBUTE_MEDIA); if (MediaTypeEnum.HTML.equals(this.currentMediaType) && (previousMediaType == null || MediaTypeEnum.HTML.equals(previousMediaType))) { writeHTMLData(); } else if (!MediaTypeEnum.HTML.equals(this.currentMediaType)) { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doEndTag - exporting"); } returnValue = doExport(); } // do not remove media attribute! if the table is nested in other tables this is still needed // this.pageContext.removeAttribute(PAGE_ATTRIBUTE_MEDIA); if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] doEndTag - end"); } cleanUp(); return returnValue; } /** * clean up instance variables, but not the ones representing tag attributes. */ private void cleanUp() { // reset instance variables (non attributes) this.currentMediaType = null; this.baseHref = null; this.caption = null; this.captionTag = null; this.currentRow = null; this.doAfterBodyExecuted = false; this.footer = null; this.listHelper = null; this.pageNumber = 0; this.paramEncoder = null; this.properties = null; this.rowNumber = 1; this.tableIterator = null; this.tableModel = null; this.list = null; } /** * If no columns are provided, automatically add them from bean properties. Get the first object in the list and get * all the properties (except the "class" property which is automatically skipped). Of course this isn't possible * for empty lists. */ private void describeEmptyTable() { this.tableIterator = IteratorUtils.getIterator(this.list); if (this.tableIterator.hasNext()) { Object iteratedObject = this.tableIterator.next(); Map objectProperties = new HashMap(); // if it's a String don't add the "Bytes" column if (iteratedObject instanceof String) { return; } // if it's a map already use key names for column headers if (iteratedObject instanceof Map) { objectProperties = (Map) iteratedObject; } else { try { objectProperties = BeanUtils.describe(iteratedObject); } catch (Exception e) { log.warn("Unable to automatically add columns: " + e.getMessage(), e); } } // iterator on properties names Iterator propertiesIterator = objectProperties.keySet().iterator(); while (propertiesIterator.hasNext()) { // get the property name String propertyName = (String) propertiesIterator.next(); // dont't want to add the standard "class" property if (!"class".equals(propertyName)) //$NON-NLS-1$ { // creates a new header and add to the table model HeaderCell headerCell = new HeaderCell(); headerCell.setBeanPropertyName(propertyName); // handle title i18n headerCell.setTitle(this.properties.geResourceProvider().getResource( null, propertyName, this, this.pageContext)); this.tableModel.addColumnHeader(headerCell); } } } } /** * Called when data are not displayed in a html page but should be exported. * @return int SKIP_PAGE * @throws JspException generic exception */ protected int doExport() throws JspException { boolean exportFullList = this.properties.getExportFullList(); if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] currentMediaType=" + this.currentMediaType); } boolean exportHeader = this.properties.getExportHeader(this.currentMediaType); boolean exportDecorated = this.properties.getExportDecorated(); ExportView exportView = ExportViewFactory.getInstance().getView( this.currentMediaType, this.tableModel, exportFullList, exportHeader, exportDecorated); try { writeExport(exportView); } catch (IOException e) { throw new WrappedRuntimeException(getClass(), e); } return SKIP_PAGE; } /** * Will write the export. The default behavior is to write directly to the response. If the ResponseOverrideFilter * is configured for this request, will instead write the exported content to a map in the Request object. * @param exportView export view * @throws JspException for problem in clearing the response or for invalid export views * @throws IOException exception thrown when writing content to the response */ protected void writeExport(ExportView exportView) throws IOException, JspException { String filename = properties.getExportFileName(this.currentMediaType); HttpServletResponse response = (HttpServletResponse) this.pageContext.getResponse(); HttpServletRequest request = (HttpServletRequest) this.pageContext.getRequest(); Map bean = (Map) request.getAttribute(FILTER_CONTENT_OVERRIDE_BODY); boolean usingFilter = bean != null; String mimeType = exportView.getMimeType(); // original encoding, be sure to add it back after reset() String characterEncoding = response.getCharacterEncoding(); if (usingFilter) { if (!bean.containsKey(TableTagParameters.BEAN_BUFFER)) { // We are running under the export filter, call it log.debug("Exportfilter enabled in unbuffered mode, setting headers"); response.addHeader(TableTagParameters.PARAMETER_EXPORTING, TagConstants.EMPTY_STRING); } else { // We are running under the export filter in buffered mode bean.put(TableTagParameters.BEAN_CONTENTTYPE, mimeType); bean.put(TableTagParameters.BEAN_FILENAME, filename); if (exportView instanceof TextExportView) { StringWriter writer = new StringWriter(); ((TextExportView) exportView).doExport(writer); bean.put(TableTagParameters.BEAN_BODY, writer.toString()); } else if (exportView instanceof BinaryExportView) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); ((BinaryExportView) exportView).doExport(stream); bean.put(TableTagParameters.BEAN_BODY, stream.toByteArray()); } else { throw new JspTagException("Export view " + exportView.getClass().getName() + " must implement TextExportView or BinaryExportView"); } return; } } else { log.debug("Exportfilter NOT enabled"); // response can't be already committed at this time if (response.isCommitted()) { throw new ExportException(getClass()); } try { response.reset(); pageContext.getOut().clearBuffer(); } catch (Exception e) { throw new ExportException(getClass()); } } if (!usingFilter && characterEncoding != null && mimeType.indexOf("charset") == -1) //$NON-NLS-1$ { mimeType += "; charset=" + characterEncoding; //$NON-NLS-1$ } response.setContentType(mimeType); if (StringUtils.isNotEmpty(filename)) { response.setHeader("Content-Disposition", //$NON-NLS-1$ "attachment; filename=\"" + filename + "\""); //$NON-NLS-1$ //$NON-NLS-2$ } if (exportView instanceof TextExportView) { Writer writer; if (usingFilter) { writer = response.getWriter(); } else { writer = pageContext.getOut(); } ((TextExportView) exportView).doExport(writer); } else if (exportView instanceof BinaryExportView) { // dealing with binary content // note that this is not assured to work on any application server if the filter is not enabled. According // to the jsp specs response.getOutputStream() should no be called in jsps. ((BinaryExportView) exportView).doExport(response.getOutputStream()); } else { throw new JspTagException("Export view " + exportView.getClass().getName() + " must implement TextExportView or BinaryExportView"); } log.debug("Export completed"); } /** * This sets the list of all of the data that will be displayed on the page via the table tag. This might include * 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 * subset, etc... */ protected void setupViewableData() { // If the user has changed the way our default behavior works, then we need to look for it now, and resort // things if needed before we ask for the viewable part. (this is a bad place for this, this should be // refactored and moved somewhere else). if (this.paginatedList == null || this.tableModel.isLocalSort()) { if (this.tableModel.isSortFullTable()) { // Sort the total list... this.tableModel.sortFullList(); } } Object originalData = this.tableModel.getRowListFull(); // If they have asked for a subset of the list via the length // attribute, then only fetch those items out of the master list. List fullList = CollectionUtil.getListFromObject(originalData, this.offset, this.length); int pageOffset = this.offset; // If they have asked for just a page of the data, then use the // SmartListHelper to figure out what page they are after, etc... if (this.paginatedList == null && this.pagesize > 0) { this.listHelper = new SmartListHelper(fullList, (this.partialList) ? ((Integer) size).intValue() : fullList .size(), this.pagesize, this.properties, this.partialList); this.listHelper.setCurrentPage(this.pageNumber); pageOffset = this.listHelper.getFirstIndexForCurrentPage(); fullList = this.listHelper.getListForCurrentPage(); } else if (this.paginatedList != null) { this.listHelper = new PaginatedListSmartListHelper(this.paginatedList, this.properties); } this.tableModel.setRowListPage(fullList); this.tableModel.setPageOffset(pageOffset); } /** * Uses HtmlTableWriter to write table called when data have to be displayed in a html page. * @throws JspException generic exception */ private void writeHTMLData() throws JspException { JspWriter out = this.pageContext.getOut(); String css = this.properties.getCssTable(); if (StringUtils.isNotBlank(css)) { this.addClass(css); } // use HtmlTableWriter to write table new HtmlTableWriter( this.tableModel, this.properties, this.baseHref, this.export, out, getCaptionTag(), this.paginatedList, this.listHelper, this.pagesize, getAttributeMap(), this.uid).writeTable(this.tableModel, this.getUid()); if (this.varTotals != null) { pageContext.setAttribute(this.varTotals, getTotals()); } } /** * Get the column totals Map. If there is no varTotals defined, there are no totals. * @return a Map of totals where the key is the column number and the value is the total for that column */ public Map getTotals() { Map totalsMap = new HashMap(); if (this.varTotals != null) { List headers = this.tableModel.getHeaderCellList(); for (Iterator iterator = headers.iterator(); iterator.hasNext();) { HeaderCell headerCell = (HeaderCell) iterator.next(); if (headerCell.isTotaled()) { totalsMap.put("column" + (headerCell.getColumnNumber() + 1), new Double(headerCell.getTotal())); } } } return totalsMap; } /** * Get the table model for this tag. Sometimes required by local tags that cooperate with DT. USE THIS METHOD WITH * EXTREME CAUTION; IT PROVIDES ACCESS TO THE INTERNALS OF DISPLAYTAG, WHICH ARE NOT TO BE CONSIDERED STABLE PUBLIC * INTERFACES. * @return the TableModel */ public TableModel getTableModel() { return this.tableModel; } /** * Called by the setProperty tag to override some default behavior or text String. * @param propertyName String property name * @param propertyValue String property value */ public void setProperty(String propertyName, String propertyValue) { this.properties.setProperty(propertyName, propertyValue); } /** * @see javax.servlet.jsp.tagext.Tag#release() */ public void release() { if (log.isDebugEnabled()) { log.debug("[" + getUid() + "] release() called"); } super.release(); // tag attributes this.decoratorName = null; this.defaultSortedColumn = -1; this.defaultSortOrder = null; this.export = false; this.length = 0; this.listAttribute = null; this.localSort = true; this.name = null; this.offset = 0; this.pagesize = 0; this.partialList = false; this.requestUri = null; this.dontAppendContext = false; this.sortFullTable = null; this.excludedParams = null; this.filteredRows = null; this.uid = null; this.paginatedList = null; } /** * Returns the name. * @return String */ protected String getName() { return this.name; } /** * encode a parameter name to be unique in the page using ParamEncoder. * @param parameterName parameter name to encode * @return String encoded parameter name */ private String encodeParameter(String parameterName) { // paramEncoder has been already instantiated? if (this.paramEncoder == null) { // use the id attribute to get the unique identifier this.paramEncoder = new ParamEncoder(getUid()); } return this.paramEncoder.encodeParameterName(parameterName); } }