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}