001/* 002 * Copyright 2009 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 */ 016package org.kuali.ole.sys.report; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Map; 023 024import org.apache.commons.beanutils.PropertyUtils; 025import org.apache.commons.lang.StringUtils; 026import org.kuali.ole.sys.OLEConstants; 027import org.kuali.rice.core.web.format.BigDecimalFormatter; 028import org.kuali.rice.core.web.format.CurrencyFormatter; 029import org.kuali.rice.core.web.format.Formatter; 030import org.kuali.rice.core.web.format.IntegerFormatter; 031import org.kuali.rice.core.web.format.KualiIntegerCurrencyFormatter; 032import org.kuali.rice.core.web.format.LongFormatter; 033import org.kuali.rice.core.web.format.PercentageFormatter; 034import org.kuali.rice.kns.service.DataDictionaryService; 035import org.kuali.rice.krad.bo.BusinessObject; 036import org.kuali.rice.krad.util.ObjectUtils; 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.ole.sys.service.impl.ReportWriterTextServiceImpl 043 */ 044public 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, OLEConstants.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, OLEConstants.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(OLEConstants.ReportConstants.TABLE_HEADER_LINE_KEY, tableHeaderLine); 209 tableDefinition.put(OLEConstants.ReportConstants.SEPARATOR_LINE_KEY, separatorLine); 210 tableDefinition.put(OLEConstants.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(OLEConstants.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 // following translates to %<alignment><width>.<precision>s where the precision for Strings forces a maxLength 298 singleRowFormat = singleRowFormat.append("%").append(alignment).append(width).append("." + width).append("s "); 299 } 300 } 301 302 tableCellFormat = tableCellFormat.append(singleRowFormat).append(LINE_BREAK); 303 if(StringUtils.isNotBlank(separatorLine)) { 304 tableCellFormat = tableCellFormat.append(separatorLine).append(LINE_BREAK); 305 } 306 } 307 308 return tableCellFormat.toString(); 309 } 310 311 /** 312 * get the separator line 313 * @param cellWidthList the given cell width list 314 * @return the separator line 315 */ 316 public String getSepartorLine(List<Integer> cellWidthList) { 317 StringBuffer separatorLine = new StringBuffer(); 318 319 for (int index = 0; index < this.columnCount; index++) { 320 Integer cellWidth = cellWidthList.get(index); 321 separatorLine = separatorLine.append(StringUtils.rightPad(StringUtils.EMPTY, cellWidth, OLEConstants.DASH)).append(" "); 322 } 323 324 return separatorLine.toString(); 325 } 326 327 /** 328 * apply the colspan definition on the default width of the table cells 329 * 330 * @param the default width of the table cells 331 */ 332 public void applyColspanOnCellWidth(List<Integer> cellWidthList) { 333 if(ObjectUtils.isNull(columnSpanDefinition)) { 334 return; 335 } 336 337 int indexOfCurrentCell = 0; 338 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 339 String attributeName = entry.getKey(); 340 341 if (columnSpanDefinition.containsKey(attributeName)) { 342 int columnSpan = columnSpanDefinition.get(attributeName); 343 344 int widthOfCurrentNonEmptyCell = cellWidthList.get(indexOfCurrentCell); 345 for (int i = 1; i < columnSpan; i++) { 346 widthOfCurrentNonEmptyCell += cellWidthList.get(indexOfCurrentCell + i); 347 cellWidthList.set(indexOfCurrentCell + i, 0); 348 } 349 cellWidthList.set(indexOfCurrentCell, widthOfCurrentNonEmptyCell + columnSpan - 1); 350 } 351 352 indexOfCurrentCell++; 353 } 354 } 355 356 /** 357 * apply the colspan definition on the default values of the table cells. The values will be removed if their positions are taken by others. 358 * 359 * @param the default values of the table cells 360 */ 361 public void applyColspanOnCellValues(List<String> cellValues) { 362 if(ObjectUtils.isNull(columnSpanDefinition)) { 363 return; 364 } 365 366 String REMOVE_ME = "REMOVE-ME-!"; 367 368 int indexOfCurrentCell = 0; 369 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 370 String attributeName = entry.getKey(); 371 372 if (columnSpanDefinition.containsKey(attributeName)) { 373 int columnSpan = columnSpanDefinition.get(attributeName); 374 375 for (int i = 1; i < columnSpan; i++) { 376 cellValues.set(indexOfCurrentCell + i, REMOVE_ME); 377 } 378 } 379 380 indexOfCurrentCell++; 381 } 382 383 int originalLength = cellValues.size(); 384 for(int index = originalLength -1; index>=0; index-- ) { 385 if(StringUtils.equals(cellValues.get(index), REMOVE_ME)) { 386 cellValues.remove(index); 387 } 388 } 389 } 390 391 /** 392 * 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. 393 * 394 * @param businessObject the given business object whose property values will be collected 395 * @param allowColspan indicate whether colspan definition can be applied 396 * @return 397 */ 398 public List<String> getTableCellValuesPaddingWithEmptyCell(BusinessObject businessObject, boolean allowColspan) { 399 List<String> tableCellValues = this.getTableCellValues(businessObject, allowColspan); 400 401 int numberOfCell = orderedPropertyNameToHeaderLabelMap.entrySet().size(); 402 this.paddingTableCellValues(numberOfCell, tableCellValues); 403 404 return tableCellValues; 405 } 406 407 /** 408 * get the width of all table cells according to the definition 409 * 410 * @return the width of all table cells. The width is in the order defined as the orderedPropertyNameToHeaderLabelMap 411 */ 412 public List<Integer> getTableCellWidth() { 413 List<Integer> cellWidthList = new ArrayList<Integer>(); 414 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 415 String attributeName = entry.getKey(); 416 String attributeValue = entry.getValue(); 417 418 int cellWidth = attributeValue.length(); 419 if (!attributeName.startsWith(OLEConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { 420 try { 421 cellWidth = retrievePropertyValueMaximumLength(dataDictionaryBusinessObjectClass, attributeName); 422 } 423 catch (Exception e) { 424 throw new RuntimeException("Failed getting propertyName=" + attributeName + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); 425 } 426 } 427 428 if (attributeValue.length() > cellWidth) { 429 cellWidth = attributeValue.length(); 430 } 431 432 cellWidthList.add(cellWidth); 433 } 434 435 int numberOfCell = cellWidthList.size(); 436 int rowCount = (int) Math.ceil(numberOfCell * 1.0 / columnCount); 437 for (int colIndex = 0; colIndex < columnCount; colIndex++) { 438 int longestLength = cellWidthList.get(colIndex); 439 440 for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) { 441 int currentIndex = rowIndex * columnCount + colIndex; 442 if (currentIndex >= numberOfCell) { 443 break; 444 } 445 446 int currentLength = cellWidthList.get(currentIndex); 447 if (currentLength > longestLength) { 448 cellWidthList.set(colIndex, currentLength); 449 } 450 } 451 } 452 453 for (int colIndex = 0; colIndex < columnCount; colIndex++) { 454 int longestLength = cellWidthList.get(colIndex); 455 456 for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) { 457 int currentIndex = rowIndex * columnCount + colIndex; 458 if (currentIndex >= numberOfCell) { 459 break; 460 } 461 462 cellWidthList.set(currentIndex, longestLength); 463 } 464 } 465 466 return cellWidthList; 467 } 468 469 /** 470 * get the alignment definitions of all table cells in one row according to the property's formatter class 471 * 472 * @return the alignment definitions of all table cells in one row according to the property's formatter class 473 */ 474 public List<String> getTableCellAlignment() { 475 List<String> cellWidthList = new ArrayList<String>(); 476 List<Class<? extends Formatter>> numberFormatters = this.getNumberFormatters(); 477 478 for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { 479 String attributeName = entry.getKey(); 480 481 boolean isNumber = false; 482 if (!attributeName.startsWith(OLEConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { 483 try { 484 Class<? extends Formatter> formatterClass = this.retrievePropertyFormatterClass(dataDictionaryBusinessObjectClass, attributeName); 485 486 isNumber = numberFormatters.contains(formatterClass); 487 } 488 catch (Exception e) { 489 throw new RuntimeException("Failed getting propertyName=" + attributeName + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); 490 } 491 } 492 493 cellWidthList.add(isNumber ? RIGHT_ALIGNMENT : LEFT_ALIGNMENT); 494 } 495 496 return cellWidthList; 497 } 498 499 // put empty strings into the table cell values if the values are not enough to feed the table 500 protected void paddingTableCellValues(int numberOfCell, List<String> tableCellValues) { 501 int reminder = columnCount - numberOfCell % columnCount; 502 if (reminder < columnCount) { 503 List<String> paddingObject = new ArrayList<String>(reminder); 504 for (int index = 0; index < reminder; index++) { 505 paddingObject.add(StringUtils.EMPTY); 506 } 507 508 tableCellValues.addAll(paddingObject); 509 } 510 } 511 512 /** 513 * get formatter classes defined for numbers 514 * 515 * @return the formatter classes defined for numbers 516 */ 517 protected List<Class<? extends Formatter>> getNumberFormatters(){ 518 List<Class<? extends Formatter>> numberFormatters = new ArrayList<Class<? extends Formatter>>(); 519 520 numberFormatters.add(BigDecimalFormatter.class); 521 numberFormatters.add(CurrencyFormatter.class); 522 numberFormatters.add(KualiIntegerCurrencyFormatter.class); 523 numberFormatters.add(PercentageFormatter.class); 524 numberFormatters.add(IntegerFormatter.class); 525 numberFormatters.add(LongFormatter.class); 526 527 return numberFormatters; 528 } 529 530 /** 531 * Sets the minimumMessageLength 532 * 533 * @param minimumMessageLength The minimumMessageLength to set. 534 */ 535 public void setMinimumMessageLength(int minimumMessageLength) { 536 this.minimumMessageLength = minimumMessageLength; 537 } 538 539 /** 540 * Sets the messageLabel 541 * 542 * @param messageLabel The messageLabel to set. 543 */ 544 public void setMessageLabel(String messageLabel) { 545 this.messageLabel = messageLabel; 546 } 547 548 /** 549 * Sets the dataDictionaryBusinessObjectClass 550 * 551 * @param dataDictionaryBusinessObjectClass The dataDictionaryBusinessObjectClass to set. 552 */ 553 public void setDataDictionaryBusinessObjectClass(Class<? extends BusinessObject> dataDictionaryBusinessObjectClass) { 554 this.dataDictionaryBusinessObjectClass = dataDictionaryBusinessObjectClass; 555 } 556 557 /** 558 * Sets the orderedPropertyNameToHeaderLabelMap 559 * 560 * @param orderedPropertyNameToHeaderLabelMap The orderedPropertyNameToHeaderLabelMap to set. 561 */ 562 public void setOrderedPropertyNameToHeaderLabelMap(Map<String, String> orderedPropertyNameToHeaderLabelMap) { 563 this.orderedPropertyNameToHeaderLabelMap = orderedPropertyNameToHeaderLabelMap; 564 } 565 566 /** 567 * Sets the dataDictionaryService 568 * 569 * @param dataDictionaryService The dataDictionaryService to set. 570 */ 571 public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { 572 this.dataDictionaryService = dataDictionaryService; 573 } 574 575 /** 576 * Sets the columnCount attribute value. 577 * 578 * @param columnCount The columnCount to set. 579 */ 580 public void setColumnCount(int columnCount) { 581 this.columnCount = columnCount; 582 } 583 584 /** 585 * Sets the columnSpanDefinition attribute value. 586 * 587 * @param columnSpanDefinition The columnSpanDefinition to set. 588 */ 589 public void setColumnSpanDefinition(Map<String, Integer> columnSpanDefinition) { 590 this.columnSpanDefinition = columnSpanDefinition; 591 } 592}