/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.sys.report; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.sys.KFSConstants; import org.kuali.rice.core.web.format.BigDecimalFormatter; import org.kuali.rice.core.web.format.CurrencyFormatter; import org.kuali.rice.core.web.format.Formatter; import org.kuali.rice.core.web.format.IntegerFormatter; import org.kuali.rice.core.web.format.KualiIntegerCurrencyFormatter; import org.kuali.rice.core.web.format.LongFormatter; import org.kuali.rice.core.web.format.PercentageFormatter; import org.kuali.rice.kns.service.DataDictionaryService; import org.kuali.rice.krad.bo.BusinessObject; import org.kuali.rice.krad.util.ObjectUtils; /** * Helper class for business objects to assist formatting them for error reporting. Utilizes spring injection for modularization and * configurability * * @see org.kuali.kfs.sys.service.impl.ReportWriterTextServiceImpl */ public class BusinessObjectReportHelper { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BusinessObjectReportHelper.class); protected int minimumMessageLength; protected String messageLabel; protected Class<? extends BusinessObject> dataDictionaryBusinessObjectClass; protected Map<String, String> orderedPropertyNameToHeaderLabelMap; protected DataDictionaryService dataDictionaryService; private int columnCount = 0; private Map<String, Integer> columnSpanDefinition; public final static String LEFT_ALIGNMENT = "LEFT"; public final static String RIGHT_ALIGNMENT = "RIGHT"; public final static String LINE_BREAK = "\n"; /** * Returns the values in a list of the passed in business object in order of the spring definition. * * @param businessObject for which to return the values * @return the values */ public List<Object> getValues(BusinessObject businessObject) { List<Object> keys = new ArrayList<Object>(); for (Iterator<String> propertyNames = orderedPropertyNameToHeaderLabelMap.keySet().iterator(); propertyNames.hasNext();) { String propertyName = propertyNames.next(); keys.add(retrievePropertyValue(businessObject, propertyName)); } return keys; } /** * Returns a value for a given property, can be overridden to allow for pseudo-properties * * @param businessObject * @param propertyName * @return */ protected Object retrievePropertyValue(BusinessObject businessObject, String propertyName) { try { return PropertyUtils.getProperty(businessObject, propertyName); } catch (Exception e) { throw new RuntimeException("Failed getting propertyName=" + propertyName + " from businessObjecName=" + businessObject.getClass().getName(), e); } } /** * Returns the maximum length of a value for a given propery, can be overridden to allow for pseudo-properties * * @param businessObjectClass * @param propertyName * @return */ protected int retrievePropertyValueMaximumLength(Class<? extends BusinessObject> businessObjectClass, String propertyName) { return dataDictionaryService.getAttributeMaxLength(businessObjectClass, propertyName); } /** * Returns the maximum length of a value for a given propery, can be overridden to allow for pseudo-properties * * @param businessObjectClass * @param propertyName * @return */ protected Class<? extends Formatter> retrievePropertyFormatterClass(Class<? extends BusinessObject> businessObjectClass, String propertyName) { return dataDictionaryService.getAttributeFormatter(businessObjectClass, propertyName); } /** * Same as getValues except that it actually doesn't retrieve the values from the BO but instead returns a blank linke. This is * useful if indentation for message printing is necessary. * * @param businessObject for which to return the values * @return spaces in the length of values */ public List<Object> getBlankValues(BusinessObject businessObject) { List<Object> keys = new ArrayList<Object>(); for (Iterator<String> propertyNames = orderedPropertyNameToHeaderLabelMap.keySet().iterator(); propertyNames.hasNext();) { String propertyName = propertyNames.next(); keys.add(""); } return keys; } /** * Returns multiple lines of what represent a table header. The last line in this list is the format of the table cells. * * @param maximumPageWidth maximum before line is out of bounds. Used to fill message to the end of this range. Note that if * there isn't at least maximumPageWidth characters available it will go minimumMessageLength out of bounds. It is up to * the calling class to handle that * @return table header. Last element is the format of the table cells. */ public List<String> getTableHeader(int maximumPageWidth) { String separatorLine = StringUtils.EMPTY; String messageFormat = StringUtils.EMPTY; // Construct the header based on orderedPropertyNameToHeaderLabelMap. It will pick the longest of label or DD size for (Iterator<Map.Entry<String, String>> entries = orderedPropertyNameToHeaderLabelMap.entrySet().iterator(); entries.hasNext();) { Map.Entry<String, String> entry = entries.next(); int longest; try { longest = retrievePropertyValueMaximumLength(dataDictionaryBusinessObjectClass, entry.getKey()); } catch (Exception e) { throw new RuntimeException("Failed getting propertyName=" + entry.getKey() + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); } if (entry.getValue().length() > longest) { longest = entry.getValue().length(); } separatorLine = separatorLine + StringUtils.rightPad("", longest, KFSConstants.DASH) + " "; messageFormat = messageFormat + "%-" + longest + "s "; } // Now fill to the end of pageWidth for the message column. If there is not enough space go out of bounds int availableWidth = maximumPageWidth - (separatorLine.length() + 1); if (availableWidth < minimumMessageLength) { availableWidth = minimumMessageLength; } separatorLine = separatorLine + StringUtils.rightPad("", availableWidth, KFSConstants.DASH); messageFormat = messageFormat + "%-" + availableWidth + "s"; // Fill in the header labels. We use the errorFormat to do this to get justification right List<Object> formatterArgs = new ArrayList<Object>(); formatterArgs.addAll(orderedPropertyNameToHeaderLabelMap.values()); formatterArgs.add(messageLabel); String tableHeaderLine = String.format(messageFormat, formatterArgs.toArray()); // Construct return list List<String> tableHeader = new ArrayList<String>(); tableHeader.add(tableHeaderLine); tableHeader.add(separatorLine); tableHeader.add(messageFormat); return tableHeader; } /** * get the primary information that can define a table structure * * @return the primary information that can define a table structure */ public Map<String, String> getTableDefinition() { List<Integer> cellWidthList = this.getTableCellWidth(); String separatorLine = this.getSepartorLine(cellWidthList); String tableCellFormat = this.getTableCellFormat(false, true, null); String tableHeaderLineFormat = this.getTableCellFormat(false, false, separatorLine); // fill in the header labels int numberOfCell = cellWidthList.size(); List<String> tableHeaderLabelValues = new ArrayList<String>(orderedPropertyNameToHeaderLabelMap.values()); this.paddingTableCellValues(numberOfCell, tableHeaderLabelValues); String tableHeaderLine = String.format(tableHeaderLineFormat, tableHeaderLabelValues.toArray()); Map<String, String> tableDefinition = new HashMap<String, String>(); tableDefinition.put(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY, tableHeaderLine); tableDefinition.put(KFSConstants.ReportConstants.SEPARATOR_LINE_KEY, separatorLine); tableDefinition.put(KFSConstants.ReportConstants.TABLE_CELL_FORMAT_KEY, tableCellFormat); return tableDefinition; } /** * Returns the values in a list of the passed in business object in order of the spring definition. The value for the * "EMPTY_CELL" entry is an empty string. * * @param businessObject for which to return the values * @param allowColspan indicate whether colspan definition can be applied * @return the values being put into the table cells */ public List<String> getTableCellValues(BusinessObject businessObject, boolean allowColspan) { List<String> tableCellValues = new ArrayList<String>(); for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { String attributeName = entry.getKey(); if (attributeName.startsWith(KFSConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { tableCellValues.add(StringUtils.EMPTY); } else { try { Object propertyValue = retrievePropertyValue(businessObject, attributeName); if (ObjectUtils.isNotNull(propertyValue)) { Formatter formatter = Formatter.getFormatter(propertyValue.getClass()); if(ObjectUtils.isNotNull(formatter) && ObjectUtils.isNotNull(propertyValue)) { propertyValue = formatter.format(propertyValue); } else { propertyValue = StringUtils.EMPTY; } } else { propertyValue = StringUtils.EMPTY; } tableCellValues.add(propertyValue.toString()); } catch (Exception e) { throw new RuntimeException("Failed getting propertyName=" + entry.getKey() + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); } } } if(allowColspan) { this.applyColspanOnCellValues(tableCellValues); } return tableCellValues; } /** * get the format string for all cells in a table row. Colspan definition will be applied if allowColspan is true * * @param allowColspan indicate whether colspan definition can be applied * @param allowRightAlignment indicate whether the right alignment can be applied * @param separatorLine the separation line for better look * * @return the format string for all cells in a table row */ public String getTableCellFormat(boolean allowColspan, boolean allowRightAlignment, String separatorLine) { List<Integer> cellWidthList = this.getTableCellWidth(); List<String> cellAlignmentList = this.getTableCellAlignment(); if(allowColspan) { this.applyColspanOnCellWidth(cellWidthList); } int numberOfCell = cellWidthList.size(); int rowCount = (int) Math.ceil(numberOfCell * 1.0 / columnCount); StringBuffer tableCellFormat = new StringBuffer(); for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { StringBuffer singleRowFormat = new StringBuffer(); for (int columnIndex = 0; columnIndex < this.columnCount; columnIndex++) { int index = columnCount * rowIndex + columnIndex; if(index >= numberOfCell) { break; } int width = cellWidthList.get(index); String alignment = (allowRightAlignment && cellAlignmentList.get(index).equals(RIGHT_ALIGNMENT)) ? StringUtils.EMPTY : "-"; if(width > 0) { // following translates to %<alignment><width>.<precision>s where the precision for Strings forces a maxLength singleRowFormat = singleRowFormat.append("%").append(alignment).append(width).append("." + width).append("s "); } } tableCellFormat = tableCellFormat.append(singleRowFormat).append(LINE_BREAK); if(StringUtils.isNotBlank(separatorLine)) { tableCellFormat = tableCellFormat.append(separatorLine).append(LINE_BREAK); } } return tableCellFormat.toString(); } /** * get the separator line * @param cellWidthList the given cell width list * @return the separator line */ public String getSepartorLine(List<Integer> cellWidthList) { StringBuffer separatorLine = new StringBuffer(); for (int index = 0; index < this.columnCount; index++) { Integer cellWidth = cellWidthList.get(index); separatorLine = separatorLine.append(StringUtils.rightPad(StringUtils.EMPTY, cellWidth, KFSConstants.DASH)).append(" "); } return separatorLine.toString(); } /** * apply the colspan definition on the default width of the table cells * * @param the default width of the table cells */ public void applyColspanOnCellWidth(List<Integer> cellWidthList) { if(ObjectUtils.isNull(columnSpanDefinition)) { return; } int indexOfCurrentCell = 0; for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { String attributeName = entry.getKey(); if (columnSpanDefinition.containsKey(attributeName)) { int columnSpan = columnSpanDefinition.get(attributeName); int widthOfCurrentNonEmptyCell = cellWidthList.get(indexOfCurrentCell); for (int i = 1; i < columnSpan; i++) { widthOfCurrentNonEmptyCell += cellWidthList.get(indexOfCurrentCell + i); cellWidthList.set(indexOfCurrentCell + i, 0); } cellWidthList.set(indexOfCurrentCell, widthOfCurrentNonEmptyCell + columnSpan - 1); } indexOfCurrentCell++; } } /** * apply the colspan definition on the default values of the table cells. The values will be removed if their positions are taken by others. * * @param the default values of the table cells */ public void applyColspanOnCellValues(List<String> cellValues) { if(ObjectUtils.isNull(columnSpanDefinition)) { return; } String REMOVE_ME = "REMOVE-ME-!"; int indexOfCurrentCell = 0; for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { String attributeName = entry.getKey(); if (columnSpanDefinition.containsKey(attributeName)) { int columnSpan = columnSpanDefinition.get(attributeName); for (int i = 1; i < columnSpan; i++) { cellValues.set(indexOfCurrentCell + i, REMOVE_ME); } } indexOfCurrentCell++; } int originalLength = cellValues.size(); for(int index = originalLength -1; index>=0; index-- ) { if(StringUtils.equals(cellValues.get(index), REMOVE_ME)) { cellValues.remove(index); } } } /** * 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. * * @param businessObject the given business object whose property values will be collected * @param allowColspan indicate whether colspan definition can be applied * @return */ public List<String> getTableCellValuesPaddingWithEmptyCell(BusinessObject businessObject, boolean allowColspan) { List<String> tableCellValues = this.getTableCellValues(businessObject, allowColspan); int numberOfCell = orderedPropertyNameToHeaderLabelMap.entrySet().size(); this.paddingTableCellValues(numberOfCell, tableCellValues); return tableCellValues; } /** * get the width of all table cells according to the definition * * @return the width of all table cells. The width is in the order defined as the orderedPropertyNameToHeaderLabelMap */ public List<Integer> getTableCellWidth() { List<Integer> cellWidthList = new ArrayList<Integer>(); for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { String attributeName = entry.getKey(); String attributeValue = entry.getValue(); int cellWidth = attributeValue.length(); if (!attributeName.startsWith(KFSConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { try { cellWidth = retrievePropertyValueMaximumLength(dataDictionaryBusinessObjectClass, attributeName); } catch (Exception e) { throw new RuntimeException("Failed getting propertyName=" + attributeName + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); } } if (attributeValue.length() > cellWidth) { cellWidth = attributeValue.length(); } cellWidthList.add(cellWidth); } int numberOfCell = cellWidthList.size(); int rowCount = (int) Math.ceil(numberOfCell * 1.0 / columnCount); for (int colIndex = 0; colIndex < columnCount; colIndex++) { int longestLength = cellWidthList.get(colIndex); for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) { int currentIndex = rowIndex * columnCount + colIndex; if (currentIndex >= numberOfCell) { break; } int currentLength = cellWidthList.get(currentIndex); if (currentLength > longestLength) { cellWidthList.set(colIndex, currentLength); } } } for (int colIndex = 0; colIndex < columnCount; colIndex++) { int longestLength = cellWidthList.get(colIndex); for (int rowIndex = 1; rowIndex < rowCount; rowIndex++) { int currentIndex = rowIndex * columnCount + colIndex; if (currentIndex >= numberOfCell) { break; } cellWidthList.set(currentIndex, longestLength); } } return cellWidthList; } /** * get the alignment definitions of all table cells in one row according to the property's formatter class * * @return the alignment definitions of all table cells in one row according to the property's formatter class */ public List<String> getTableCellAlignment() { List<String> cellWidthList = new ArrayList<String>(); List<Class<? extends Formatter>> numberFormatters = this.getNumberFormatters(); for (Map.Entry<String, String> entry : orderedPropertyNameToHeaderLabelMap.entrySet()) { String attributeName = entry.getKey(); boolean isNumber = false; if (!attributeName.startsWith(KFSConstants.ReportConstants.EMPTY_CELL_ENTRY_KEY_PREFIX)) { try { Class<? extends Formatter> formatterClass = this.retrievePropertyFormatterClass(dataDictionaryBusinessObjectClass, attributeName); isNumber = numberFormatters.contains(formatterClass); } catch (Exception e) { throw new RuntimeException("Failed getting propertyName=" + attributeName + " from businessObjecName=" + dataDictionaryBusinessObjectClass.getName(), e); } } cellWidthList.add(isNumber ? RIGHT_ALIGNMENT : LEFT_ALIGNMENT); } return cellWidthList; } // put empty strings into the table cell values if the values are not enough to feed the table protected void paddingTableCellValues(int numberOfCell, List<String> tableCellValues) { int reminder = columnCount - numberOfCell % columnCount; if (reminder < columnCount) { List<String> paddingObject = new ArrayList<String>(reminder); for (int index = 0; index < reminder; index++) { paddingObject.add(StringUtils.EMPTY); } tableCellValues.addAll(paddingObject); } } /** * get formatter classes defined for numbers * * @return the formatter classes defined for numbers */ protected List<Class<? extends Formatter>> getNumberFormatters(){ List<Class<? extends Formatter>> numberFormatters = new ArrayList<Class<? extends Formatter>>(); numberFormatters.add(BigDecimalFormatter.class); numberFormatters.add(CurrencyFormatter.class); numberFormatters.add(KualiIntegerCurrencyFormatter.class); numberFormatters.add(PercentageFormatter.class); numberFormatters.add(IntegerFormatter.class); numberFormatters.add(LongFormatter.class); return numberFormatters; } /** * Sets the minimumMessageLength * * @param minimumMessageLength The minimumMessageLength to set. */ public void setMinimumMessageLength(int minimumMessageLength) { this.minimumMessageLength = minimumMessageLength; } /** * Sets the messageLabel * * @param messageLabel The messageLabel to set. */ public void setMessageLabel(String messageLabel) { this.messageLabel = messageLabel; } /** * Sets the dataDictionaryBusinessObjectClass * * @param dataDictionaryBusinessObjectClass The dataDictionaryBusinessObjectClass to set. */ public void setDataDictionaryBusinessObjectClass(Class<? extends BusinessObject> dataDictionaryBusinessObjectClass) { this.dataDictionaryBusinessObjectClass = dataDictionaryBusinessObjectClass; } /** * Sets the orderedPropertyNameToHeaderLabelMap * * @param orderedPropertyNameToHeaderLabelMap The orderedPropertyNameToHeaderLabelMap to set. */ public void setOrderedPropertyNameToHeaderLabelMap(Map<String, String> orderedPropertyNameToHeaderLabelMap) { this.orderedPropertyNameToHeaderLabelMap = orderedPropertyNameToHeaderLabelMap; } /** * Sets the dataDictionaryService * * @param dataDictionaryService The dataDictionaryService to set. */ public void setDataDictionaryService(DataDictionaryService dataDictionaryService) { this.dataDictionaryService = dataDictionaryService; } /** * Sets the columnCount attribute value. * * @param columnCount The columnCount to set. */ public void setColumnCount(int columnCount) { this.columnCount = columnCount; } /** * Sets the columnSpanDefinition attribute value. * * @param columnSpanDefinition The columnSpanDefinition to set. */ public void setColumnSpanDefinition(Map<String, Integer> columnSpanDefinition) { this.columnSpanDefinition = columnSpanDefinition; } }