View Javadoc
1   /*
2    * Copyright 2009 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl2.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.ole.sys.report;
17  
18  import java.util.ArrayList;
19  import java.util.HashMap;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.Map;
23  
24  import org.apache.commons.beanutils.PropertyUtils;
25  import org.apache.commons.lang.StringUtils;
26  import org.kuali.ole.sys.OLEConstants;
27  import org.kuali.rice.core.web.format.BigDecimalFormatter;
28  import org.kuali.rice.core.web.format.CurrencyFormatter;
29  import org.kuali.rice.core.web.format.Formatter;
30  import org.kuali.rice.core.web.format.IntegerFormatter;
31  import org.kuali.rice.core.web.format.KualiIntegerCurrencyFormatter;
32  import org.kuali.rice.core.web.format.LongFormatter;
33  import org.kuali.rice.core.web.format.PercentageFormatter;
34  import org.kuali.rice.kns.service.DataDictionaryService;
35  import org.kuali.rice.krad.bo.BusinessObject;
36  import org.kuali.rice.krad.util.ObjectUtils;
37  
38  /**
39   * Helper class for business objects to assist formatting them for error reporting. Utilizes spring injection for modularization and
40   * configurability
41   * 
42   * @see org.kuali.ole.sys.service.impl.ReportWriterTextServiceImpl
43   */
44  public class BusinessObjectReportHelper {
45      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BusinessObjectReportHelper.class);
46  
47      protected int minimumMessageLength;
48      protected String messageLabel;
49      protected Class<? extends BusinessObject> dataDictionaryBusinessObjectClass;
50      protected Map<String, String> orderedPropertyNameToHeaderLabelMap;
51      protected DataDictionaryService dataDictionaryService;
52  
53      private int columnCount = 0;
54      private Map<String, Integer> columnSpanDefinition;
55      
56      public final static String LEFT_ALIGNMENT = "LEFT"; 
57      public final static String RIGHT_ALIGNMENT = "RIGHT"; 
58      public final static String LINE_BREAK = "\n";
59  
60      /**
61       * Returns the values in a list of the passed in business object in order of the spring definition.
62       * 
63       * @param businessObject for which to return the values
64       * @return the values
65       */
66      public List<Object> getValues(BusinessObject businessObject) {
67          List<Object> keys = new ArrayList<Object>();
68  
69          for (Iterator<String> propertyNames = orderedPropertyNameToHeaderLabelMap.keySet().iterator(); propertyNames.hasNext();) {
70              String propertyName = propertyNames.next();
71              keys.add(retrievePropertyValue(businessObject, propertyName));
72          }
73  
74          return keys;
75      }
76  
77      /**
78       * Returns a value for a given property, can be overridden to allow for pseudo-properties
79       * 
80       * @param businessObject
81       * @param propertyName
82       * @return
83       */
84      protected Object retrievePropertyValue(BusinessObject businessObject, String propertyName) {
85          try {
86              return PropertyUtils.getProperty(businessObject, propertyName);
87          }
88          catch (Exception e) {
89              throw new RuntimeException("Failed getting propertyName=" + propertyName + " from businessObjecName=" + businessObject.getClass().getName(), e);
90          }
91      }
92  
93      /**
94       * Returns the maximum length of a value for a given propery, can be overridden to allow for pseudo-properties
95       * 
96       * @param businessObjectClass
97       * @param propertyName
98       * @return
99       */
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 }