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 }