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    }