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.decorator; 13 14 import java.util; 15 import java.text.MessageFormat; 16 17 import javax.servlet.jsp.PageContext; 18 19 import org.apache.commons.lang.ObjectUtils; 20 import org.apache.commons.lang.StringUtils; 21 import org.apache.commons.logging.Log; 22 import org.apache.commons.logging.LogFactory; 23 import org.displaytag.exception.DecoratorException; 24 import org.displaytag.exception.ObjectLookupException; 25 import org.displaytag.model.Column; 26 import org.displaytag.model.ColumnIterator; 27 import org.displaytag.model.HeaderCell; 28 import org.displaytag.model.Row; 29 import org.displaytag.model.TableModel; 30 import org.displaytag.util.TagConstants; 31 32 33 /** 34 * A TableDecorator that, in conjunction with totaled and grouped columns, produces multi level subtotals on arbitrary 35 * String groupings. Use it directly, subclass it, or use it as an example to better meet your local needs. 36 * @author rapruitt 37 * @author Fabrizio Giustina 38 */ 39 public class MultilevelTotalTableDecorator extends TableDecorator 40 { 41 42 /** 43 * If there are no columns that are totaled, we should not issue a totals row. 44 */ 45 private boolean containsTotaledColumns = false; 46 47 /** 48 * No current reset group. 49 */ 50 private static final int NO_RESET_GROUP = 4200; 51 52 /** 53 * Maps the groups to their current totals. 54 */ 55 private Map groupNumberToGroupTotal = new HashMap(); 56 57 /** 58 * The deepest reset group. Resets on an outer group will force any deeper groups to reset as well. 59 */ 60 private int deepestResetGroup = NO_RESET_GROUP; 61 62 /** 63 * Controls when the subgroup is ended. 64 */ 65 protected int innermostGroup; 66 67 /** 68 * Logger. 69 */ 70 private Log logger = LogFactory.getLog(MultilevelTotalTableDecorator.class); 71 72 /** 73 * CSS class applied to grand total totals. 74 */ 75 protected String grandTotalSum = "grandtotal-sum"; 76 77 /** 78 * CSS class applied to grand total cells where the column is not totaled. 79 */ 80 protected String grandTotalNoSum = "grandtotal-nosum"; 81 82 /** 83 * CSS class applied to grand total lablels. 84 */ 85 protected String grandTotalLabel = "grandtotal-label"; 86 87 /** 88 * Grandtotal description. 89 */ 90 protected String grandTotalDescription = "Grand Total"; 91 92 93 /** 94 * CSS class appplied to subtotal headers. 95 */ 96 private String subtotalHeaderClass = "subtotal-header"; 97 98 /** 99 * CSS class applied to subtotal labels. 100 */ 101 private String subtotalLabelClass = "subtotal-label"; 102 103 /** 104 * Message format for subtotal descriptions. 105 */ 106 private MessageFormat subtotalDesc = new MessageFormat("{0} Total"); 107 108 /** 109 * CSS class applied to subtotal totals. 110 */ 111 private String subtotalValueClass = "subtotal-sum"; 112 113 114 /** 115 * Holds the header rows and their content for a particular group. 116 */ 117 private List headerRows = new ArrayList(5); 118 119 public void init(PageContext context, Object decorated, TableModel model) 120 { 121 super.init(context, decorated, model); 122 List headerCells = model.getHeaderCellList(); 123 // go through each column, looking for grouped columns; add them to the group number map 124 for (Iterator iterator = headerCells.iterator(); iterator.hasNext();) 125 { 126 HeaderCell headerCell = (HeaderCell) iterator.next(); 127 containsTotaledColumns = containsTotaledColumns || headerCell.isTotaled(); 128 if (headerCell.getGroup() > 0) 129 { 130 groupNumberToGroupTotal.put(new Integer(headerCell.getGroup()), new GroupTotals(headerCell 131 .getColumnNumber())); 132 if (headerCell.getGroup() > innermostGroup) 133 { 134 innermostGroup = headerCell.getGroup(); 135 } 136 } 137 } 138 } 139 140 public String getGrandTotalDescription() 141 { 142 return grandTotalDescription; 143 } 144 145 public void setGrandTotalDescription(String grandTotalDescription) 146 { 147 this.grandTotalDescription = grandTotalDescription; 148 } 149 150 /** 151 * The pattern to use to generate the subtotal labels. The grouping value of the cell will be the first arg. 152 * The default value is "{0} Total". 153 * @param pattern 154 * @param locale 155 */ 156 public void setSubtotalLabel(String pattern, Locale locale) 157 { 158 this.subtotalDesc = new MessageFormat(pattern, locale); 159 } 160 161 public String getGrandTotalLabel() 162 { 163 return grandTotalLabel; 164 } 165 166 public String getGrandTotalSum() 167 { 168 return grandTotalSum; 169 } 170 171 public String getGrandTotalNoSum() 172 { 173 return grandTotalNoSum; 174 } 175 176 public void setGrandTotalNoSum(String grandTotalNoSum) 177 { 178 this.grandTotalNoSum = grandTotalNoSum; 179 } 180 181 public void setGrandTotalSum(String grandTotalSum) 182 { 183 this.grandTotalSum = grandTotalSum; 184 } 185 186 public void setGrandTotalLabel(String grandTotalLabel) 187 { 188 this.grandTotalLabel = grandTotalLabel; 189 } 190 191 public String getSubtotalValueClass() 192 { 193 return subtotalValueClass; 194 } 195 196 public void setSubtotalValueClass(String subtotalValueClass) 197 { 198 this.subtotalValueClass = subtotalValueClass; 199 } 200 201 public String getSubtotalLabelClass() 202 { 203 return subtotalLabelClass; 204 } 205 206 public void setSubtotalLabelClass(String subtotalLabelClass) 207 { 208 this.subtotalLabelClass = subtotalLabelClass; 209 } 210 211 public String getSubtotalHeaderClass() 212 { 213 return subtotalHeaderClass; 214 } 215 216 public void setSubtotalHeaderClass(String subtotalHeaderClass) 217 { 218 this.subtotalHeaderClass = subtotalHeaderClass; 219 } 220 221 public void startOfGroup(String value, int group) 222 { 223 if (containsTotaledColumns) 224 { 225 StringBuffer tr = new StringBuffer(); 226 tr.append("<tr>"); 227 GroupTotals groupTotals = (GroupTotals) groupNumberToGroupTotal.get(new Integer(group)); 228 int myColumnNumber = groupTotals.columnNumber; 229 for (int i = 0; i < myColumnNumber; i++) 230 { 231 tr.append("<td></td>\n"); 232 } 233 tr.append("<td class=\"").append(getSubtotalHeaderClass()).append(" group-").append(group).append("\" >"); 234 tr.append(value).append("</td>\n"); 235 List headerCells = tableModel.getHeaderCellList(); 236 for (int i = myColumnNumber; i < headerCells.size() - 1; i++) 237 { 238 tr.append("<td></td>\n"); 239 } 240 tr.append("</tr>\n"); 241 headerRows.add(tr); 242 } 243 } 244 245 public String displayGroupedValue(String value, short groupingStatus, int columnNumber) 246 { 247 // if (groupingStatus == TableWriterTemplate.GROUP_START_AND_END && columnNumber > 1) 248 // { 249 // return value; 250 // } 251 // else 252 // { 253 return ""; 254 // } 255 } 256 257 public String startRow() 258 { 259 StringBuffer sb = new StringBuffer(); 260 for (Iterator iterator = headerRows.iterator(); iterator.hasNext();) 261 { 262 StringBuffer stringBuffer = (StringBuffer) iterator.next(); 263 sb.append(stringBuffer); 264 } 265 return sb.toString(); 266 } 267 268 public void endOfGroup(String value, int groupNumber) 269 { 270 if (deepestResetGroup > groupNumber) 271 { 272 deepestResetGroup = groupNumber; 273 } 274 } 275 276 public String finishRow() 277 { 278 String returnValue = ""; 279 if (containsTotaledColumns) 280 { 281 if (innermostGroup > 0 && deepestResetGroup != NO_RESET_GROUP) 282 { 283 StringBuffer out = new StringBuffer(); 284 // Starting with the deepest group, print the current total and reset. Do not reset unaffected groups. 285 for (int i = innermostGroup; i >= deepestResetGroup; i--) 286 { 287 Integer groupNumber = new Integer(i); 288 289 GroupTotals totals = (GroupTotals) groupNumberToGroupTotal.get(groupNumber); 290 if (totals == null) 291 { 292 logger.warn("There is a gap in the defined groups - no group defined for " + groupNumber); 293 continue; 294 } 295 totals.printTotals(getListIndex(), out); 296 totals.setStartRow(getListIndex() + 1); 297 } 298 returnValue = out.toString(); 299 } 300 else 301 { 302 returnValue = null; 303 } 304 deepestResetGroup = NO_RESET_GROUP; 305 headerRows.clear(); 306 if (isLastRow()) 307 { 308 returnValue = StringUtils.defaultString(returnValue); 309 returnValue += totalAllRows(); 310 } 311 } 312 return returnValue; 313 } 314 315 /** 316 * Issue a grand total row at the bottom. 317 * @return the suitable string 318 */ 319 protected String totalAllRows() 320 { 321 if (containsTotaledColumns) 322 { 323 List headerCells = tableModel.getHeaderCellList(); 324 StringBuffer output = new StringBuffer(); 325 int currentRow = getListIndex(); 326 output.append(TagConstants.TAG_OPEN + TagConstants.TAGNAME_ROW 327 + " class=\"grandtotal-row\"" + TagConstants.TAG_CLOSE); 328 boolean first = true; 329 for (Iterator iterator = headerCells.iterator(); iterator.hasNext();) 330 { 331 HeaderCell headerCell = (HeaderCell) iterator.next(); 332 if (first) 333 { 334 output.append(getTotalsTdOpen(headerCell, getGrandTotalLabel())); 335 output.append(getGrandTotalDescription()); 336 first = false; 337 } 338 else if (headerCell.isTotaled()) 339 { 340 // a total if the column should be totaled 341 Object total = getTotalForColumn(headerCell.getColumnNumber(), 0, currentRow); 342 output.append(getTotalsTdOpen(headerCell, getGrandTotalSum())); 343 output.append(formatTotal(headerCell, total)); 344 } 345 else 346 { 347 // blank, if it is not a totals column 348 output.append(getTotalsTdOpen(headerCell, getGrandTotalNoSum())); 349 } 350 output.append(TagConstants.TAG_OPENCLOSING + TagConstants.TAGNAME_COLUMN + TagConstants.TAG_CLOSE); 351 } 352 output.append("\n</tr>\n"); 353 354 return output.toString(); 355 } 356 else 357 { 358 return ""; 359 } 360 } 361 362 protected String getCellValue(int columnNumber, int rowNumber) 363 { 364 List fullList = tableModel.getRowListFull(); 365 Row row = (Row) fullList.get(rowNumber); 366 ColumnIterator columnIterator = row.getColumnIterator(tableModel.getHeaderCellList()); 367 while (columnIterator.hasNext()) 368 { 369 Column column = columnIterator.nextColumn(); 370 if (column.getHeaderCell().getColumnNumber() == columnNumber) 371 { 372 try 373 { 374 column.initialize(); 375 return column.getChoppedAndLinkedValue(); 376 } 377 catch (ObjectLookupException e) 378 { 379 logger.error("Error: " + e.getMessage(), e); 380 throw new RuntimeException("Error: " + e.getMessage(), e); 381 } 382 catch (DecoratorException e) 383 { 384 logger.error("Error: " + e.getMessage(), e); 385 throw new RuntimeException("Error: " + e.getMessage(), e); 386 } 387 } 388 } 389 throw new RuntimeException("Unable to find column " + columnNumber + " in the list of columns"); 390 } 391 392 protected Object getTotalForColumn(int columnNumber, int startRow, int stopRow) 393 { 394 List fullList = tableModel.getRowListFull(); 395 List window = fullList.subList(startRow, stopRow + 1); 396 Object total = null; 397 for (Iterator iterator = window.iterator(); iterator.hasNext();) 398 { 399 Row row = (Row) iterator.next(); 400 ColumnIterator columnIterator = row.getColumnIterator(tableModel.getHeaderCellList()); 401 while (columnIterator.hasNext()) 402 { 403 Column column = columnIterator.nextColumn(); 404 if (column.getHeaderCell().getColumnNumber() == columnNumber) 405 { 406 Object value = null; 407 try 408 { 409 value = column.getValue(false); 410 } 411 catch (ObjectLookupException e) 412 { 413 logger.error(e); 414 } 415 catch (DecoratorException e) 416 { 417 logger.error(e); 418 } 419 if (value != null && ! TagConstants.EMPTY_STRING.equals(value)) 420 { 421 total = add(column, total, value); 422 } 423 } 424 } 425 } 426 return total; 427 } 428 429 protected Object add(Column column, Object total, Object value) { 430 if (value == null) 431 { 432 return total; 433 } 434 else if (value instanceof Number) 435 { 436 Number oldTotal = new Double(0); 437 if (total != null) 438 { 439 oldTotal = (Number)total; 440 } 441 return new Double(oldTotal.doubleValue() + ((Number) value).doubleValue()); 442 } 443 else 444 { 445 throw new UnsupportedOperationException("Cannot add a value of " + value + " in column " + column.getHeaderCell().getTitle()); 446 } 447 } 448 449 public String getTotalsTdOpen(HeaderCell header, String totalClass) 450 { 451 452 String cssClass = ObjectUtils.toString(header.getHtmlAttributes().get("class")); 453 454 StringBuffer buffer = new StringBuffer(); 455 buffer.append(TagConstants.TAG_OPEN); 456 buffer.append(TagConstants.TAGNAME_COLUMN); 457 if (cssClass != null || totalClass != null) 458 { 459 buffer.append(" class=\""); 460 461 if (cssClass != null) 462 { 463 buffer.append(cssClass); 464 if (totalClass != null) 465 { 466 buffer.append(" "); 467 } 468 } 469 if (totalClass != null) 470 { 471 buffer.append(totalClass); 472 } 473 buffer.append("\""); 474 } 475 buffer.append(TagConstants.TAG_CLOSE); 476 return buffer.toString(); 477 } 478 479 public String getTotalsRowOpen() 480 { 481 return TagConstants.TAG_OPEN + TagConstants.TAGNAME_ROW + " class=\"subtotal\"" + TagConstants.TAG_CLOSE; 482 } 483 484 public String getTotalRowLabel(String groupingValue) 485 { 486 return subtotalDesc.format(new Object[]{groupingValue}); 487 } 488 489 public String formatTotal(HeaderCell header, Object total) 490 { 491 Object displayValue = total; 492 if (header.getColumnDecorators().length > 0) 493 { 494 for (int i = 0; i < header.getColumnDecorators().length; i++) 495 { 496 DisplaytagColumnDecorator decorator = header.getColumnDecorators()[i]; 497 try 498 { 499 displayValue = decorator.decorate(total, this.getPageContext(), tableModel.getMedia()); 500 } 501 catch (DecoratorException e) 502 { 503 logger.warn(e.getMessage(), e); 504 // ignore, use undecorated value for totals 505 } 506 } 507 } 508 return displayValue != null ? displayValue.toString() : ""; 509 } 510 511 class GroupTotals 512 { 513 514 /** 515 * The label class. 516 */ 517 protected String totalLabelClass = getSubtotalLabelClass(); 518 /** 519 * The row opener 520 */ 521 protected String totalsRowOpen = getTotalsRowOpen(); 522 523 /** 524 * The value class. 525 */ 526 protected String totalValueClass = getSubtotalValueClass(); 527 528 private int columnNumber; 529 530 private int firstRowOfCurrentSet; 531 532 public GroupTotals(int headerCellColumn) 533 { 534 this.columnNumber = headerCellColumn; 535 this.firstRowOfCurrentSet = 0; 536 } 537 538 public void printTotals(int currentRow, StringBuffer out) 539 { 540 541 // For each column, output: 542 List headerCells = tableModel.getHeaderCellList(); 543 if (firstRowOfCurrentSet < currentRow) // If there is more than one row, show a total 544 { 545 out.append(totalsRowOpen); 546 for (Iterator iterator = headerCells.iterator(); iterator.hasNext();) 547 { 548 HeaderCell headerCell = (HeaderCell) iterator.next(); 549 550 if (columnNumber == headerCell.getColumnNumber()) 551 { 552 // a totals label if it is the column for the current group 553 String currentLabel = getCellValue(columnNumber, firstRowOfCurrentSet); 554 out.append(getTotalsTdOpen(headerCell, getTotalLabelClass() + " group-" + (columnNumber + 1))); 555 out.append(getTotalRowLabel(currentLabel)); 556 } 557 else if (headerCell.isTotaled()) 558 { 559 // a total if the column should be totaled 560 Object total = getTotalForColumn(headerCell.getColumnNumber(), 561 firstRowOfCurrentSet, currentRow); 562 out.append(getTotalsTdOpen(headerCell, getTotalValueClass() + " group-" + (columnNumber + 1))); 563 out.append(formatTotal(headerCell, total)); 564 } 565 else 566 { 567 // blank, if it is not a totals column 568 String style = "group-" + (columnNumber + 1); 569 if (headerCell.getColumnNumber() < innermostGroup) 570 { 571 style += " " + getTotalLabelClass() + " "; 572 } 573 out.append(getTotalsTdOpen(headerCell, style)); 574 } 575 out.append(TagConstants.TAG_OPENCLOSING + TagConstants.TAGNAME_COLUMN + TagConstants.TAG_CLOSE); 576 } 577 out.append("\n</tr>\n"); 578 } 579 } 580 581 public void setStartRow(int i) 582 { 583 firstRowOfCurrentSet = i; 584 } 585 586 public String getTotalLabelClass() 587 { 588 return totalLabelClass; 589 } 590 591 public void setTotalsRowOpen(String totalsRowOpen) 592 { 593 this.totalsRowOpen = totalsRowOpen; 594 } 595 596 public void setTotalLabelClass(String totalLabelClass) 597 { 598 this.totalLabelClass = totalLabelClass; 599 } 600 601 public String getTotalValueClass() 602 { 603 return totalValueClass; 604 } 605 606 public void setTotalValueClass(String totalValueClass) 607 { 608 this.totalValueClass = totalValueClass; 609 } 610 } 611 }