/**
* 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.decorator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.jsp.PageContext;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.displaytag.exception.DecoratorException;
import org.displaytag.exception.ObjectLookupException;
import org.displaytag.model.Column;
import org.displaytag.model.ColumnIterator;
import org.displaytag.model.HeaderCell;
import org.displaytag.model.Row;
import org.displaytag.model.TableModel;
import org.displaytag.util.TagConstants;
/**
* A TableDecorator that, in conjunction with totaled and grouped columns, produces multi level subtotals on arbitrary
* String groupings.
* @author rapruitt
* @author Fabrizio Giustina
*/
public class MultilevelTotalTableDecorator extends TableDecorator
{
/**
* No current reset group.
*/
private static final int NO_RESET_GROUP = 4200;
/**
* Controls when the subgroup is ended.
*/
protected int innermostGroup;
/**
* Logger.
*/
private Log logger = LogFactory.getLog(MultilevelTotalTableDecorator.class);
/**
* CSS class applied to grand totals.
*/
private String grandTotalLabel = "grandtotal-sum";
/**
* CSS class appplied to subtotal headers.
*/
private String subtotalHeaderClass = "subtotal-header";
/**
* CSS class applied to subtotal labels.
*/
private String subtotalLabelClass = "subtotal-label";
/**
* CSS class applied to subtotal totals.
*/
private String subtotalValueClass = "subtotal-sum";
/**
* Maps the groups to their current totals.
*/
private Map groupNumberToGroupTotal = new HashMap();
/**
* The deepest reset group.
*/
private int deepestResetGroup = NO_RESET_GROUP;
/**
* Holds the header rows and their content for a particular group.
*/
private List headerRows = new ArrayList(5);
public void init(PageContext context, Object decorated, TableModel model)
{
super.init(context, decorated, model);
List headerCells = model.getHeaderCellList();
// go through each column, looking for grouped columns; add them to the group number map
for (Iterator iterator = headerCells.iterator(); iterator.hasNext();)
{
HeaderCell headerCell = (HeaderCell) iterator.next();
if (headerCell.getGroup() > 0)
{
groupNumberToGroupTotal.put(new Integer(headerCell.getGroup()), new GroupTotals(headerCell
.getColumnNumber()));
if (headerCell.getGroup() > innermostGroup)
{
innermostGroup = headerCell.getGroup();
}
}
}
}
public String getGrandTotalLabel()
{
return grandTotalLabel;
}
public void setGrandTotalLabel(String grandTotalLabel)
{
this.grandTotalLabel = grandTotalLabel;
}
public String getSubtotalValueClass()
{
return subtotalValueClass;
}
public void setSubtotalValueClass(String subtotalValueClass)
{
this.subtotalValueClass = subtotalValueClass;
}
public String getSubtotalLabelClass()
{
return subtotalLabelClass;
}
public void setSubtotalLabelClass(String subtotalLabelClass)
{
this.subtotalLabelClass = subtotalLabelClass;
}
public String getSubtotalHeaderClass()
{
return subtotalHeaderClass;
}
public void setSubtotalHeaderClass(String subtotalHeaderClass)
{
this.subtotalHeaderClass = subtotalHeaderClass;
}
public void startOfGroup(String value, int group)
{
StringBuffer tr = new StringBuffer();
tr.append("<tr>");
for (int i = 1; i < group; i++)
{
tr.append("<td></td>\n");
}
tr.append("<td class=\"").append(getSubtotalHeaderClass()).append(" group-").append(group).append("\">");
tr.append(value).append("</td>\n");
tr.append("</tr>");
headerRows.add(tr);
}
public String displayGroupedValue(String value, short groupingStatus)
{
return "";
}
public String startRow()
{
StringBuffer sb = new StringBuffer();
for (Iterator iterator = headerRows.iterator(); iterator.hasNext();)
{
StringBuffer stringBuffer = (StringBuffer) iterator.next();
sb.append(stringBuffer);
}
return sb.toString();
}
public void endOfGroup(String value, int groupNumber)
{
if (deepestResetGroup > groupNumber)
{
deepestResetGroup = groupNumber;
}
}
public String finishRow()
{
String returnValue;
if (innermostGroup > 0 && deepestResetGroup != NO_RESET_GROUP)
{
StringBuffer out = new StringBuffer();
// Starting with the deepest group, print the current total and reset. Do not reset unaffected groups.
for (int i = innermostGroup; i >= deepestResetGroup; i--)
{
Integer groupNumber = new Integer(i);
GroupTotals totals = (GroupTotals) groupNumberToGroupTotal.get(groupNumber);
if (totals == null)
{
logger.warn("There is a gap in the defined groups - no group defined for " + groupNumber);
continue;
}
totals.printTotals(getListIndex(), out);
totals.setStartRow(getListIndex() + 1);
}
returnValue = out.toString();
}
else
{
returnValue = null;
}
deepestResetGroup = NO_RESET_GROUP;
headerRows.clear();
if (isLastRow())
{
returnValue = StringUtils.defaultString(returnValue);
returnValue += totalAllRows();
}
return returnValue;
}
/**
* Issue a grand total row at the bottom.
* @return the suitable string
*/
protected String totalAllRows()
{
GroupTotals grandTotal = new GroupTotals(-1);
StringBuffer out = new StringBuffer();
grandTotal.setStartRow(0);
grandTotal.setTotalValueClass(getGrandTotalLabel());
grandTotal.printTotals(getListIndex(), out);
return out.toString();
}
protected String getCellValue(int columnNumber, int rowNumber)
{
List fullList = tableModel.getRowListFull();
Row row = (Row) fullList.get(rowNumber);
ColumnIterator columnIterator = row.getColumnIterator(tableModel.getHeaderCellList());
while (columnIterator.hasNext())
{
Column column = columnIterator.nextColumn();
if (column.getHeaderCell().getColumnNumber() == columnNumber)
{
try
{
column.initialize();
return column.getChoppedAndLinkedValue();
}
catch (ObjectLookupException e)
{
logger.error("Error: " + e.getMessage(), e);
throw new RuntimeException("Error: " + e.getMessage(), e);
}
catch (DecoratorException e)
{
logger.error("Error: " + e.getMessage(), e);
throw new RuntimeException("Error: " + e.getMessage(), e);
}
}
}
throw new RuntimeException("Unable to find column " + columnNumber + " in the list of columns");
}
protected double getTotalForColumn(int columnNumber, int startRow, int stopRow)
{
List fullList = tableModel.getRowListFull();
List window = fullList.subList(startRow, stopRow + 1);
double total = 0;
for (Iterator iterator = window.iterator(); iterator.hasNext();)
{
Row row = (Row) iterator.next();
ColumnIterator columnIterator = row.getColumnIterator(tableModel.getHeaderCellList());
while (columnIterator.hasNext())
{
Column column = columnIterator.nextColumn();
if (column.getHeaderCell().getColumnNumber() == columnNumber)
{
Number value = null;
try
{
value = (Number) column.getValue(false);
}
catch (ObjectLookupException e)
{
logger.error(e);
}
catch (DecoratorException e)
{
logger.error(e);
}
if (value != null)
{
total += value.doubleValue();
}
}
}
}
return total;
}
public String getTotalsTdOpen(HeaderCell header, String totalClass)
{
String cssClass = ObjectUtils.toString(header.getHtmlAttributes().get("class"));
StringBuffer buffer = new StringBuffer();
buffer.append(TagConstants.TAG_OPEN);
buffer.append(TagConstants.TAGNAME_COLUMN);
if (cssClass != null || totalClass != null)
{
buffer.append(" class=\"");
if (cssClass != null)
{
buffer.append(cssClass);
if (totalClass != null)
{
buffer.append(" ");
}
}
if (totalClass != null)
{
buffer.append(totalClass);
}
buffer.append("\"");
}
buffer.append(TagConstants.TAG_CLOSE);
return buffer.toString();
}
public String getTotalsRowOpen()
{
return TagConstants.TAG_OPEN + TagConstants.TAGNAME_ROW + " class=\"subtotal\"" + TagConstants.TAG_CLOSE;
}
public String getTotalRowLabel(String groupingValue)
{
return groupingValue + " Total";
}
public String formatTotal(HeaderCell header, double total)
{
Object displayValue = new Double(total);
if (header.getColumnDecorators().length > 0)
{
for (int i = 0; i < header.getColumnDecorators().length; i++)
{
DisplaytagColumnDecorator decorator = header.getColumnDecorators()[i];
try
{
displayValue = decorator.decorate(displayValue, this.getPageContext(), tableModel.getMedia());
}
catch (DecoratorException e)
{
logger.warn(e.getMessage(), e);
// ignore, use undecorated value for totals
}
}
}
return displayValue.toString();
}
class GroupTotals
{
/**
* The label class.
*/
protected String totalLabelClass = getSubtotalLabelClass();
/**
* The value class.
*/
protected String totalValueClass = getSubtotalValueClass();
private int columnNumber;
private int firstRowOfCurrentSet;
public GroupTotals(int headerCellColumn)
{
this.columnNumber = headerCellColumn;
this.firstRowOfCurrentSet = 0;
}
public void printTotals(int currentRow, StringBuffer out)
{
// For each column, output:
List headerCells = tableModel.getHeaderCellList();
if (firstRowOfCurrentSet < currentRow) // If there is more than one row, show a total
{
out.append(getTotalsRowOpen());
for (Iterator iterator = headerCells.iterator(); iterator.hasNext();)
{
HeaderCell headerCell = (HeaderCell) iterator.next();
if (columnNumber == headerCell.getColumnNumber())
{
// a totals label if it is the column for the current group
String currentLabel = getCellValue(columnNumber, firstRowOfCurrentSet);
out.append(getTotalsTdOpen(headerCell, getTotalLabelClass() + " group-" + (columnNumber + 1)));
out.append(getTotalRowLabel(currentLabel));
}
else if (headerCell.isTotaled())
{
// a total if the column should be totaled
double total = getTotalForColumn(headerCell.getColumnNumber(), firstRowOfCurrentSet, currentRow);
out.append(getTotalsTdOpen(headerCell, getTotalValueClass() + " group-" + (columnNumber + 1)));
out.append(formatTotal(headerCell, total));
}
else
{
// blank, if it is not a totals column
String style = "group-" + (columnNumber + 1);
if (headerCell.getColumnNumber() < innermostGroup)
{
style += " " + getTotalLabelClass() + " ";
}
out.append(getTotalsTdOpen(headerCell, style));
}
out.append(TagConstants.TAG_OPENCLOSING + TagConstants.TAGNAME_COLUMN + TagConstants.TAG_CLOSE);
}
out.append("</tr>\n");
}
}
public void setStartRow(int i)
{
firstRowOfCurrentSet = i;
}
public String getTotalLabelClass()
{
return totalLabelClass;
}
public void setTotalLabelClass(String totalLabelClass)
{
this.totalLabelClass = totalLabelClass;
}
public String getTotalValueClass()
{
return totalValueClass;
}
public void setTotalValueClass(String totalValueClass)
{
this.totalValueClass = totalValueClass;
}
}
}