001 /* 002 * Copyright 2011 The Kuali Foundation. 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package org.kuali.kfs.sys.report; 017 018 import java.util.ArrayList; 019 import java.util.HashMap; 020 import java.util.Iterator; 021 import java.util.List; 022 import java.util.Map; 023 024 import org.apache.commons.beanutils.PropertyUtils; 025 import org.apache.commons.lang.StringUtils; 026 import org.kuali.kfs.sys.KFSConstants; 027 import org.kuali.rice.kns.bo.BusinessObject; 028 import org.kuali.rice.kns.service.DataDictionaryService; 029 import org.kuali.rice.kns.util.ObjectUtils; 030 import org.kuali.rice.kns.web.format.BigDecimalFormatter; 031 import org.kuali.rice.kns.web.format.CurrencyFormatter; 032 import org.kuali.rice.kns.web.format.Formatter; 033 import org.kuali.rice.kns.web.format.IntegerFormatter; 034 import org.kuali.rice.kns.web.format.KualiIntegerCurrencyFormatter; 035 import org.kuali.rice.kns.web.format.LongFormatter; 036 import org.kuali.rice.kns.web.format.PercentageFormatter; 037 038 /** 039 * Helper class for business objects to assist formatting them for error reporting. Utilizes spring injection for modularization and 040 * configurability 041 * 042 * @see org.kuali.kfs.sys.service.impl.ReportWriterTextServiceImpl 043 */ 044 public class BusinessObjectReportHelper { 045 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BusinessObjectReportHelper.class); 046 047 protected int minimumMessageLength; 048 protected String messageLabel; 049 protected Class<? extends BusinessObject> dataDictionaryBusinessObjectClass; 050 protected Map<String, String> orderedPropertyNameToHeaderLabelMap; 051 protected DataDictionaryService dataDictionaryService; 052 053 private int columnCount = 0; 054 private Map<String, Integer> columnSpanDefinition; 055 056 public final static String LEFT_ALIGNMENT = "LEFT"; 057 public final static String RIGHT_ALIGNMENT = "RIGHT"; 058 public final static String LINE_BREAK = "\n"; 059 060 /** 061 * Returns the values in a list of the passed in business object in order of the spring definition. 062 * 063 * @param businessObject for which to return the values 064 * @return the values 065 */ 066 public List<Object> getValues(BusinessObject businessObject) { 067 List<Object> keys = new ArrayList<Object>(); 068 069 for (Iterator<String> propertyNames = orderedPropertyNameToHeaderLabelMap.keySet().iterator(); propertyNames.hasNext();) { 070 String propertyName = propertyNames.next(); 071 keys.add(retrievePropertyValue(businessObject, propertyName)); 072 } 073 074 return keys; 075 } 076 077 /** 078 * Returns a value for a given property, can be overridden to allow for pseudo-properties 079 * 080 * @param businessObject 081 * @param propertyName 082 * @return 083 */ 084 protected Object retrievePropertyValue(BusinessObject businessObject, String propertyName) { 085 try { 086 return PropertyUtils.getProperty(businessObject, propertyName); 087 } 088 catch (Exception e) { 089 throw new RuntimeException("Failed getting propertyName=" + propertyName + " from businessObjecName=" + businessObject.getClass().getName(), e); 090 } 091 } 092 093 /** 094 * Returns the maximum length of a value for a given propery, can be overridden to allow for pseudo-properties 095 * 096 * @param businessObjectClass 097 * @param propertyName 098 * @return 099 */ 100 protected int retrievePropertyValueMaximumLength(Class<? extends BusinessObject> businessObjectClass, String propertyName) { 101 return dataDictionaryService.getAttributeMaxLength(businessObjectClass, propertyName); 102 } 103 104 /** 105 * Returns the maximum length of a value for a given propery, can be overridden to allow for pseudo-properties 106 * 107 * @param businessObjectClass 108 * @param propertyName 109 * @return 110 */ 111 protected Class<? extends Formatter> retrievePropertyFormatterClass(Class<? extends BusinessObject> businessObjectClass, String propertyName) { 112 return dataDictionaryService.getAttributeFormatter(businessObjectClass, propertyName); 113 } 114 115 /** 116 * Same as getValues except that it actually doesn't retrieve the values from the BO but instead returns a blank linke. This is 117 * useful if indentation for message printing is necessary. 118 * 119 * @param businessObject for which to return the values 120 * @return spaces in the length of values 121 */ 122 public List<Object> getBlankValues(BusinessObject businessObject) { 123 List<Object> keys = new ArrayList<Object>(); 124 125 for (Iterator<String> propertyNames = orderedPropertyNameToHeaderLabelMap.keySet().iterator(); propertyNames.hasNext();) { 126 String propertyName = propertyNames.next(); 127 128 keys.add(""); 129 } 130 131 return keys; 132 } 133 134 /** 135 * Returns multiple lines of what represent a table header. The last line in this list is the format of the table cells. 136 * 137 * @param maximumPageWidth maximum before line is out of bounds. Used to fill message to the end of this range. Note that if 138 * there isn't at least maximumPageWidth characters available it will go minimumMessageLength out of bounds. It is up to 139 * the calling class to handle that 140 * @return table header. Last element is the format of the table cells. 141 */ 142 public List<String> getTableHeader(int maximumPageWidth) { 143 String separatorLine = StringUtils.EMPTY; 144 String messageFormat = StringUtils.EMPTY; 145 146 // Construct the header based on orderedPropertyNameToHeaderLabelMap. It will pick the longest of label or DD size 147 for (Iterator<Map.Entry<String, String>> entries = orderedPropertyNameToHeaderLabelMap.entrySet().iterator(); entries.hasNext();) { 148 Map.Entry<String, String> entry = entries.next(); 149 150 int longest; 151 try { 152 longest = retrievePropertyValueMaximumLength(dataDictionaryBusinessObjectClass, entry.getKey()); 153 } 154 catch (Exception e) { 155 throw new RuntimeException("Failed getting propertyName=" + entry.getKey() + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); 156 } 157 if (entry.getValue().length() > longest) { 158 longest = entry.getValue().length(); 159 } 160 161 separatorLine = separatorLine + StringUtils.rightPad("", longest, KFSConstants.DASH) + " "; 162 messageFormat = messageFormat + "%-" + longest + "s "; 163 } 164 165 // Now fill to the end of pageWidth for the message column. If there is not enough space go out of bounds 166 int availableWidth = maximumPageWidth - (separatorLine.length() + 1); 167 if (availableWidth < minimumMessageLength) { 168 availableWidth = minimumMessageLength; 169 } 170 separatorLine = separatorLine + StringUtils.rightPad("", availableWidth, KFSConstants.DASH); 171 messageFormat = messageFormat + "%-" + availableWidth + "s"; 172 173 // Fill in the header labels. We use the errorFormat to do this to get justification right 174 List<Object> formatterArgs = new ArrayList<Object>(); 175 formatterArgs.addAll(orderedPropertyNameToHeaderLabelMap.values()); 176 formatterArgs.add(messageLabel); 177 String tableHeaderLine = String.format(messageFormat, formatterArgs.toArray()); 178 179 // Construct return list 180 List<String> tableHeader = new ArrayList<String>(); 181 tableHeader.add(tableHeaderLine); 182 tableHeader.add(separatorLine); 183 tableHeader.add(messageFormat); 184 185 return tableHeader; 186 } 187 188 /** 189 * get the primary information that can define a table structure 190 * 191 * @return the primary information that can define a table structure 192 */ 193 public Map<String, String> getTableDefinition() { 194 List<Integer> cellWidthList = this.getTableCellWidth(); 195 196 String separatorLine = this.getSepartorLine(cellWidthList); 197 String tableCellFormat = this.getTableCellFormat(false, true, null); 198 String tableHeaderLineFormat = this.getTableCellFormat(false, false, separatorLine); 199 200 // fill in the header labels 201 int numberOfCell = cellWidthList.size(); 202 List<String> tableHeaderLabelValues = new ArrayList<String>(orderedPropertyNameToHeaderLabelMap.values()); 203 this.paddingTableCellValues(numberOfCell, tableHeaderLabelValues); 204 205 String tableHeaderLine = String.format(tableHeaderLineFormat, tableHeaderLabelValues.toArray()); 206 207 Map<String, String> tableDefinition = new HashMap<String, String>(); 208 tableDefinition.put(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY, tableHeaderLine); 209 tableDefinition.put(KFSConstants.ReportConstants.SEPARATOR_LINE_KEY, separatorLine); 210 tableDefinition.put(KFSConstants.ReportConstants.TABLE_CELL_FORMAT_KEY, tableCellFormat); 211 212 return tableDefinition; 213 } 214 215 /** 216 * Returns the values in a list of the passed in business object in order of the spring definition. The value for the 217 * "EMPTY_CELL" entry is an empty string. 218 * 219 * @param businessObject for which to return the values 220 * @param allowColspan indicate whether colspan definition can be applied 221 * @return the values being put into the table cells 222 */ 223 public List<String> getTableCellValues(BusinessObject businessObject, boolean allowColspan) { 224 List<String> tableCellValues = new ArrayList<String>(); 225 226 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 227 String attributeName = entry.getKey(); 228 229 if (attributeName.startsWith(KFSConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { 230 tableCellValues.add(StringUtils.EMPTY); 231 } 232 else { 233 try { 234 Object propertyValue = retrievePropertyValue(businessObject, attributeName); 235 236 if (ObjectUtils.isNotNull(propertyValue)) { 237 Formatter formatter = Formatter.getFormatter(propertyValue.getClass()); 238 if(ObjectUtils.isNotNull(formatter) && ObjectUtils.isNotNull(propertyValue)) { 239 propertyValue = formatter.format(propertyValue); 240 } 241 else { 242 propertyValue = StringUtils.EMPTY; 243 } 244 } else { 245 propertyValue = StringUtils.EMPTY; 246 } 247 248 tableCellValues.add(propertyValue.toString()); 249 } 250 catch (Exception e) { 251 throw new RuntimeException("Failed getting propertyName=" + entry.getKey() + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); 252 } 253 } 254 } 255 256 if(allowColspan) { 257 this.applyColspanOnCellValues(tableCellValues); 258 } 259 260 return tableCellValues; 261 } 262 263 /** 264 * get the format string for all cells in a table row. Colspan definition will be applied if allowColspan is true 265 * 266 * @param allowColspan indicate whether colspan definition can be applied 267 * @param allowRightAlignment indicate whether the right alignment can be applied 268 * @param separatorLine the separation line for better look 269 * 270 * @return the format string for all cells in a table row 271 */ 272 public String getTableCellFormat(boolean allowColspan, boolean allowRightAlignment, String separatorLine) { 273 List<Integer> cellWidthList = this.getTableCellWidth(); 274 List<String> cellAlignmentList = this.getTableCellAlignment(); 275 276 if(allowColspan) { 277 this.applyColspanOnCellWidth(cellWidthList); 278 } 279 280 int numberOfCell = cellWidthList.size(); 281 int rowCount = (int) Math.ceil(numberOfCell * 1.0 / columnCount); 282 283 StringBuffer tableCellFormat = new StringBuffer(); 284 for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { 285 StringBuffer singleRowFormat = new StringBuffer(); 286 287 for (int columnIndex = 0; columnIndex < this.columnCount; columnIndex++) { 288 int index = columnCount * rowIndex + columnIndex; 289 290 if(index >= numberOfCell) { 291 break; 292 } 293 294 int width = cellWidthList.get(index); 295 String alignment = (allowRightAlignment && cellAlignmentList.get(index).equals(RIGHT_ALIGNMENT)) ? StringUtils.EMPTY : "-"; 296 if(width > 0) { 297 singleRowFormat = singleRowFormat.append("%").append(alignment).append(width).append("s "); 298 } 299 } 300 301 tableCellFormat = tableCellFormat.append(singleRowFormat).append(LINE_BREAK); 302 if(StringUtils.isNotBlank(separatorLine)) { 303 tableCellFormat = tableCellFormat.append(separatorLine).append(LINE_BREAK); 304 } 305 } 306 307 return tableCellFormat.toString(); 308 } 309 310 /** 311 * get the separator line 312 * @param cellWidthList the given cell width list 313 * @return the separator line 314 */ 315 public String getSepartorLine(List<Integer> cellWidthList) { 316 StringBuffer separatorLine = new StringBuffer(); 317 318 for (int index = 0; index < this.columnCount; index++) { 319 Integer cellWidth = cellWidthList.get(index); 320 separatorLine = separatorLine.append(StringUtils.rightPad(StringUtils.EMPTY, cellWidth, KFSConstants.DASH)).append(" "); 321 } 322 323 return separatorLine.toString(); 324 } 325 326 /** 327 * apply the colspan definition on the default width of the table cells 328 * 329 * @param the default width of the table cells 330 */ 331 public void applyColspanOnCellWidth(List<Integer> cellWidthList) { 332 if(ObjectUtils.isNull(columnSpanDefinition)) { 333 return; 334 } 335 336 int indexOfCurrentCell = 0; 337 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 338 String attributeName = entry.getKey(); 339 340 if (columnSpanDefinition.containsKey(attributeName)) { 341 int columnSpan = columnSpanDefinition.get(attributeName); 342 343 int widthOfCurrentNonEmptyCell = cellWidthList.get(indexOfCurrentCell); 344 for (int i = 1; i < columnSpan; i++) { 345 widthOfCurrentNonEmptyCell += cellWidthList.get(indexOfCurrentCell + i); 346 cellWidthList.set(indexOfCurrentCell + i, 0); 347 } 348 cellWidthList.set(indexOfCurrentCell, widthOfCurrentNonEmptyCell + columnSpan - 1); 349 } 350 351 indexOfCurrentCell++; 352 } 353 } 354 355 /** 356 * apply the colspan definition on the default values of the table cells. The values will be removed if their positions are taken by others. 357 * 358 * @param the default values of the table cells 359 */ 360 public void applyColspanOnCellValues(List<String> cellValues) { 361 if(ObjectUtils.isNull(columnSpanDefinition)) { 362 return; 363 } 364 365 String REMOVE_ME = "REMOVE-ME-!"; 366 367 int indexOfCurrentCell = 0; 368 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 369 String attributeName = entry.getKey(); 370 371 if (columnSpanDefinition.containsKey(attributeName)) { 372 int columnSpan = columnSpanDefinition.get(attributeName); 373 374 for (int i = 1; i < columnSpan; i++) { 375 cellValues.set(indexOfCurrentCell + i, REMOVE_ME); 376 } 377 } 378 379 indexOfCurrentCell++; 380 } 381 382 int originalLength = cellValues.size(); 383 for(int index = originalLength -1; index>=0; index-- ) { 384 if(StringUtils.equals(cellValues.get(index), REMOVE_ME)) { 385 cellValues.remove(index); 386 } 387 } 388 } 389 390 /** 391 * get the values that can be fed into a predefined table. If the values are not enought to occupy the table cells, a number of empty values are provided. 392 * 393 * @param businessObject the given business object whose property values will be collected 394 * @param allowColspan indicate whether colspan definition can be applied 395 * @return 396 */ 397 public List<String> getTableCellValuesPaddingWithEmptyCell(BusinessObject businessObject, boolean allowColspan) { 398 List<String> tableCellValues = this.getTableCellValues(businessObject, allowColspan); 399 400 int numberOfCell = orderedPropertyNameToHeaderLabelMap.entrySet().size(); 401 this.paddingTableCellValues(numberOfCell, tableCellValues); 402 403 return tableCellValues; 404 } 405 406 /** 407 * get the width of all table cells according to the definition 408 * 409 * @return the width of all table cells. The width is in the order defined as the orderedPropertyNameToHeaderLabelMap 410 */ 411 public List<Integer> getTableCellWidth() { 412 List<Integer> cellWidthList = new ArrayList<Integer>(); 413 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 414 String attributeName = entry.getKey(); 415 String attributeValue = entry.getValue(); 416 417 int cellWidth = attributeValue.length(); 418 if (!attributeName.startsWith(KFSConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { 419 try { 420 cellWidth = retrievePropertyValueMaximumLength(dataDictionaryBusinessObjectClass, attributeName); 421 } 422 catch (Exception e) { 423 throw new RuntimeException("Failed getting propertyName=" + attributeName + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); 424 } 425 } 426 427 if (attributeValue.length() > cellWidth) { 428 cellWidth = attributeValue.length(); 429 } 430 431 cellWidthList.add(cellWidth); 432 } 433 434 int numberOfCell = cellWidthList.size(); 435 int rowCount = (int) Math.ceil(numberOfCell * 1.0 / columnCount); 436 for (int colIndex = 0; colIndex < columnCount; colIndex++) { 437 int longestLength = cellWidthList.get(colIndex); 438 439 for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) { 440 int currentIndex = rowIndex * columnCount + colIndex; 441 if (currentIndex >= numberOfCell) { 442 break; 443 } 444 445 int currentLength = cellWidthList.get(currentIndex); 446 if (currentLength > longestLength) { 447 cellWidthList.set(colIndex, currentLength); 448 } 449 } 450 } 451 452 for (int colIndex = 0; colIndex < columnCount; colIndex++) { 453 int longestLength = cellWidthList.get(colIndex); 454 455 for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) { 456 int currentIndex = rowIndex * columnCount + colIndex; 457 if (currentIndex >= numberOfCell) { 458 break; 459 } 460 461 cellWidthList.set(currentIndex, longestLength); 462 } 463 } 464 465 return cellWidthList; 466 } 467 468 /** 469 * get the alignment definitions of all table cells in one row according to the property's formatter class 470 * 471 * @return the alignment definitions of all table cells in one row according to the property's formatter class 472 */ 473 public List<String> getTableCellAlignment() { 474 List<String> cellWidthList = new ArrayList<String>(); 475 List<Class<? extends Formatter>> numberFormatters = this.getNumberFormatters(); 476 477 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 478 String attributeName = entry.getKey(); 479 480 boolean isNumber = false; 481 if (!attributeName.startsWith(KFSConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { 482 try { 483 Class<? extends Formatter> formatterClass = this.retrievePropertyFormatterClass(dataDictionaryBusinessObjectClass, attributeName); 484 485 isNumber = numberFormatters.contains(formatterClass); 486 } 487 catch (Exception e) { 488 throw new RuntimeException("Failed getting propertyName=" + attributeName + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); 489 } 490 } 491 492 cellWidthList.add(isNumber ? RIGHT_ALIGNMENT : LEFT_ALIGNMENT); 493 } 494 495 return cellWidthList; 496 } 497 498 // put empty strings into the table cell values if the values are not enough to feed the table 499 protected void paddingTableCellValues(int numberOfCell, List<String> tableCellValues) { 500 int reminder = columnCount - numberOfCell % columnCount; 501 if (reminder < columnCount) { 502 List<String> paddingObject = new ArrayList<String>(reminder); 503 for (int index = 0; index < reminder; index++) { 504 paddingObject.add(StringUtils.EMPTY); 505 } 506 507 tableCellValues.addAll(paddingObject); 508 } 509 } 510 511 /** 512 * get formatter classes defined for numbers 513 * 514 * @return the formatter classes defined for numbers 515 */ 516 protected List<Class<? extends Formatter>> getNumberFormatters(){ 517 List<Class<? extends Formatter>> numberFormatters = new ArrayList<Class<? extends Formatter>>(); 518 519 numberFormatters.add(BigDecimalFormatter.class); 520 numberFormatters.add(CurrencyFormatter.class); 521 numberFormatters.add(KualiIntegerCurrencyFormatter.class); 522 numberFormatters.add(PercentageFormatter.class); 523 numberFormatters.add(IntegerFormatter.class); 524 numberFormatters.add(LongFormatter.class); 525 526 return numberFormatters; 527 } 528 529 /** 530 * Sets the minimumMessageLength 531 * 532 * @param minimumMessageLength The minimumMessageLength to set. 533 */ 534 public void setMinimumMessageLength(int minimumMessageLength) { 535 this.minimumMessageLength = minimumMessageLength; 536 } 537 538 /** 539 * Sets the messageLabel 540 * 541 * @param messageLabel The messageLabel to set. 542 */ 543 public void setMessageLabel(String messageLabel) { 544 this.messageLabel = messageLabel; 545 } 546 547 /** 548 * Sets the dataDictionaryBusinessObjectClass 549 * 550 * @param dataDictionaryBusinessObjectClass The dataDictionaryBusinessObjectClass to set. 551 */ 552 public void setDataDictionaryBusinessObjectClass(Class<? extends BusinessObject> dataDictionaryBusinessObjectClass) { 553 this.dataDictionaryBusinessObjectClass = dataDictionaryBusinessObjectClass; 554 } 555 556 /** 557 * Sets the orderedPropertyNameToHeaderLabelMap 558 * 559 * @param orderedPropertyNameToHeaderLabelMap The orderedPropertyNameToHeaderLabelMap to set. 560 */ 561 public void setOrderedPropertyNameToHeaderLabelMap(Map<String, String> orderedPropertyNameToHeaderLabelMap) { 562 this.orderedPropertyNameToHeaderLabelMap = orderedPropertyNameToHeaderLabelMap; 563 } 564 565 /** 566 * Sets the dataDictionaryService 567 * 568 * @param dataDictionaryService The dataDictionaryService to set. 569 */ 570 public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { 571 this.dataDictionaryService = dataDictionaryService; 572 } 573 574 /** 575 * Sets the columnCount attribute value. 576 * 577 * @param columnCount The columnCount to set. 578 */ 579 public void setColumnCount(int columnCount) { 580 this.columnCount = columnCount; 581 } 582 583 /** 584 * Sets the columnSpanDefinition attribute value. 585 * 586 * @param columnSpanDefinition The columnSpanDefinition to set. 587 */ 588 public void setColumnSpanDefinition(Map<String, Integer> columnSpanDefinition) { 589 this.columnSpanDefinition = columnSpanDefinition; 590 } 591 }