/*
* 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.businessobject;
import static org.kuali.kfs.sys.KFSKeyConstants.AccountingLineParser.ERROR_INVALID_FILE_FORMAT;
import static org.kuali.kfs.sys.KFSKeyConstants.AccountingLineParser.ERROR_INVALID_PROPERTY_VALUE;
import static org.kuali.kfs.sys.KFSPropertyConstants.ACCOUNT_NUMBER;
import static org.kuali.kfs.sys.KFSPropertyConstants.AMOUNT;
import static org.kuali.kfs.sys.KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE;
import static org.kuali.kfs.sys.KFSPropertyConstants.FINANCIAL_OBJECT_CODE;
import static org.kuali.kfs.sys.KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE;
import static org.kuali.kfs.sys.KFSPropertyConstants.ORGANIZATION_REFERENCE_ID;
import static org.kuali.kfs.sys.KFSPropertyConstants.OVERRIDE_CODE;
import static org.kuali.kfs.sys.KFSPropertyConstants.POSTING_YEAR;
import static org.kuali.kfs.sys.KFSPropertyConstants.PROJECT_CODE;
import static org.kuali.kfs.sys.KFSPropertyConstants.SEQUENCE_NUMBER;
import static org.kuali.kfs.sys.KFSPropertyConstants.SUB_ACCOUNT_NUMBER;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.coa.service.AccountService;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.document.AccountingDocument;
import org.kuali.kfs.sys.exception.AccountingLineParserException;
import org.kuali.rice.core.web.format.FormatException;
import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
import org.kuali.rice.kns.service.DataDictionaryService;
import org.kuali.rice.krad.exception.InfrastructureException;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.ObjectUtils;
/**
* Base class for parsing serialized <code>AccountingLine</code>s for <code>TransactionalDocument</code>s
*/
public class AccountingLineParserBase implements AccountingLineParser {
protected static final String[] DEFAULT_FORMAT = { CHART_OF_ACCOUNTS_CODE, ACCOUNT_NUMBER, SUB_ACCOUNT_NUMBER, FINANCIAL_OBJECT_CODE, FINANCIAL_SUB_OBJECT_CODE, PROJECT_CODE, ORGANIZATION_REFERENCE_ID, AMOUNT };
private String fileName;
private Integer lineNo = 0;
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#getSourceAccountingLineFormat()
*/
public String[] getSourceAccountingLineFormat() {
return removeChartFromFormatIfNeeded(DEFAULT_FORMAT);
}
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#getTargetAccountingLineFormat()
*/
public String[] getTargetAccountingLineFormat() {
return removeChartFromFormatIfNeeded(DEFAULT_FORMAT);
}
/**
* If accounts can cross charts, returns the given format;
* otherwise returns the format with ChartOfAccountsCode field removed.
*/
public String[] removeChartFromFormatIfNeeded(String[] format) {
if (SpringContext.getBean(AccountService.class).accountsCanCrossCharts()) {
return format;
}
// if accounts can't cross charts, exclude ChartOfAccountsCode field from the format
String[] formatNoChart = new String[format.length-1];
int idx = 0;
for (int i=0; i<format.length; i++) {
if (format[i].equals(CHART_OF_ACCOUNTS_CODE))
continue;
else {
formatNoChart[idx] = format[i];
idx++;
}
}
return formatNoChart;
}
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#getExpectedAccountingLineFormatAsString(java.lang.Class)
*/
public String getExpectedAccountingLineFormatAsString(Class<? extends AccountingLine> accountingLineClass) {
StringBuffer sb = new StringBuffer();
boolean first = true;
for (String attributeName : chooseFormat(accountingLineClass)) {
if (!first) {
sb.append(",");
}
else {
first = false;
}
sb.append(retrieveAttributeLabel(accountingLineClass, attributeName));
}
return sb.toString();
}
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#parseSourceAccountingLine(org.kuali.rice.krad.document.TransactionalDocument,
* java.lang.String)
*/
public SourceAccountingLine parseSourceAccountingLine(AccountingDocument transactionalDocument, String sourceAccountingLineString) {
Class sourceAccountingLineClass = getSourceAccountingLineClass(transactionalDocument);
SourceAccountingLine sourceAccountingLine = (SourceAccountingLine) populateAccountingLine(transactionalDocument, sourceAccountingLineClass, sourceAccountingLineString, parseAccountingLine(sourceAccountingLineClass, sourceAccountingLineString), transactionalDocument.getNextSourceLineNumber());
return sourceAccountingLine;
}
/**
* Given a document, determines what class the source lines of that document uses
* @param accountingDocument the document to find the class of the source lines for
* @return the class of the source lines
*/
protected Class getSourceAccountingLineClass(final AccountingDocument accountingDocument) {
return accountingDocument.getSourceAccountingLineClass();
}
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#parseTargetAccountingLine(org.kuali.rice.krad.document.TransactionalDocument,
* java.lang.String)
*/
public TargetAccountingLine parseTargetAccountingLine(AccountingDocument transactionalDocument, String targetAccountingLineString) {
Class targetAccountingLineClass = getTargetAccountingLineClass(transactionalDocument);
TargetAccountingLine targetAccountingLine = (TargetAccountingLine) populateAccountingLine(transactionalDocument, targetAccountingLineClass, targetAccountingLineString, parseAccountingLine(targetAccountingLineClass, targetAccountingLineString), transactionalDocument.getNextTargetLineNumber());
return targetAccountingLine;
}
/**
* Given a document, determines what class that document uses for target accounting lines
* @param accountingDocument the document to determine the target accounting line class for
* @return the class of the target lines for the given document
*/
protected Class getTargetAccountingLineClass(final AccountingDocument accountingDocument) {
return accountingDocument.getTargetAccountingLineClass();
}
/**
* Populates a source/target line with values
*
* @param transactionalDocument
* @param accountingLineClass
* @param accountingLineAsString
* @param attributeValueMap
* @param sequenceNumber
* @return AccountingLine
*/
protected AccountingLine populateAccountingLine(AccountingDocument transactionalDocument, Class<? extends AccountingLine> accountingLineClass, String accountingLineAsString, Map<String, String> attributeValueMap, Integer sequenceNumber) {
putCommonAttributesInMap(attributeValueMap, transactionalDocument, sequenceNumber);
// create line and populate fields
AccountingLine accountingLine;
try {
accountingLine = (AccountingLine) accountingLineClass.newInstance();
// perform custom line population
if (SourceAccountingLine.class.isAssignableFrom(accountingLineClass)) {
performCustomSourceAccountingLinePopulation(attributeValueMap, (SourceAccountingLine) accountingLine, accountingLineAsString);
}
else if (TargetAccountingLine.class.isAssignableFrom(accountingLineClass)) {
performCustomTargetAccountingLinePopulation(attributeValueMap, (TargetAccountingLine) accountingLine, accountingLineAsString);
}
else {
throw new IllegalArgumentException("invalid (unknown) accounting line type: " + accountingLineClass);
}
for (Entry<String, String> entry : attributeValueMap.entrySet()) {
try {
try {
Class entryType = ObjectUtils.easyGetPropertyType(accountingLine, entry.getKey());
if (String.class.isAssignableFrom(entryType)) {
entry.setValue(entry.getValue().toUpperCase());
}
ObjectUtils.setObjectProperty(accountingLine, entry.getKey(), entryType, entry.getValue());
}
catch (IllegalArgumentException e) {
throw new InfrastructureException("unable to complete accounting line population.", e);
}
}
catch (FormatException e) {
String[] errorParameters = { entry.getValue().toString(), retrieveAttributeLabel(accountingLine.getClass(), entry.getKey()), accountingLineAsString };
// KULLAB-408
GlobalVariables.getMessageMap().putError(KFSConstants.ACCOUNTING_LINE_ERRORS, ERROR_INVALID_PROPERTY_VALUE, entry.getValue().toString(), entry.getKey(), accountingLineAsString + " : Line Number " + lineNo.toString());
throw new AccountingLineParserException("invalid '" + entry.getKey() + "=" + entry.getValue() + "for " + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters);
}
}
// override chart code if accounts can't cross charts
SpringContext.getBean(AccountService.class).populateAccountingLineChartIfNeeded(accountingLine);
}
catch (SecurityException e) {
throw new InfrastructureException("unable to complete accounting line population.", e);
}
catch (NoSuchMethodException e) {
throw new InfrastructureException("unable to complete accounting line population.", e);
}
catch (IllegalAccessException e) {
throw new InfrastructureException("unable to complete accounting line population.", e);
}
catch (InvocationTargetException e) {
throw new InfrastructureException("unable to complete accounting line population.", e);
}
catch (InstantiationException e) {
throw new InfrastructureException("unable to complete accounting line population.", e);
}
// force input to uppercase
SpringContext.getBean(BusinessObjectDictionaryService.class).performForceUppercase(accountingLine);
accountingLine.refresh();
return accountingLine;
}
/**
* Places fields common to both source/target accounting lines in the attribute map
*
* @param attributeValueMap
* @param document
* @param sequenceNumber
*/
protected void putCommonAttributesInMap(Map<String, String> attributeValueMap, AccountingDocument document, Integer sequenceNumber) {
attributeValueMap.put(KFSPropertyConstants.DOCUMENT_NUMBER, document.getDocumentNumber());
attributeValueMap.put(POSTING_YEAR, document.getPostingYear().toString());
attributeValueMap.put(SEQUENCE_NUMBER, sequenceNumber.toString());
}
/**
* Parses the csv line
*
* @param accountingLineClass
* @param lineToParse
* @return Map containing accounting line attribute,value pairs
*/
protected Map<String, String> parseAccountingLine(Class<? extends AccountingLine> accountingLineClass, String lineToParse) {
if (StringUtils.isNotBlank(fileName) && !StringUtils.lowerCase(fileName).endsWith(".csv")) {
throw new AccountingLineParserException("unsupported file format: " + fileName, ERROR_INVALID_FILE_FORMAT, fileName);
}
String[] attributes = chooseFormat(accountingLineClass);
String[] attributeValues = StringUtils.splitPreserveAllTokens(lineToParse, ",");
Map<String, String> attributeValueMap = new HashMap<String, String>();
for (int i = 0; i < Math.min(attributeValues.length, attributes.length); i++) {
attributeValueMap.put(attributes[i], attributeValues[i]);
}
return attributeValueMap;
}
/**
* Should be voerriden by documents to perform any additional <code>SourceAccountingLine</code> population
*
* @param attributeValueMap
* @param sourceAccountingLine
* @param accountingLineAsString
*/
protected void performCustomSourceAccountingLinePopulation(Map<String, String> attributeValueMap, SourceAccountingLine sourceAccountingLine, String accountingLineAsString) {
}
/**
* Should be overridden by documents to perform any additional <code>TargetAccountingLine</code> attribute population
*
* @param attributeValueMap
* @param targetAccountingLine
* @param accountingLineAsString
*/
protected void performCustomTargetAccountingLinePopulation(Map<String, String> attributeValueMap, TargetAccountingLine targetAccountingLine, String accountingLineAsString) {
}
/**
* Calls the appropriate parseAccountingLine method
*
* @param stream
* @param transactionalDocument
* @param isSource
* @return List
*/
protected List<AccountingLine> importAccountingLines(String fileName, InputStream stream, AccountingDocument transactionalDocument, boolean isSource) {
List<AccountingLine> importedAccountingLines = new ArrayList<AccountingLine>();
this.fileName = fileName;
BufferedReader br = new BufferedReader(new InputStreamReader(stream));
try {
String accountingLineAsString = null;
lineNo = 0;
while ((accountingLineAsString = br.readLine()) != null) {
lineNo++;
if (StringUtils.isBlank(StringUtils.remove(StringUtils.deleteWhitespace(accountingLineAsString),KFSConstants.COMMA))) {
continue;
}
AccountingLine accountingLine = null;
try {
if (isSource) {
accountingLine = parseSourceAccountingLine(transactionalDocument, accountingLineAsString);
}
else {
accountingLine = parseTargetAccountingLine(transactionalDocument, accountingLineAsString);
}
validateImportedAccountingLine(accountingLine, accountingLineAsString);
importedAccountingLines.add(accountingLine);
}
catch (AccountingLineParserException e) {
GlobalVariables.getMessageMap().putError((isSource ? "sourceAccountingLines" : "targetAccountingLines"), KFSKeyConstants.ERROR_ACCOUNTING_DOCUMENT_ACCOUNTING_LINE_IMPORT_GENERAL, new String[] { e.getMessage() });
}
}
}
catch (IOException e) {
throw new InfrastructureException("unable to readLine from bufferReader in accountingLineParserBase", e);
}
finally {
try {
br.close();
}
catch (IOException e) {
throw new InfrastructureException("unable to close bufferReader in accountingLineParserBase", e);
}
}
return importedAccountingLines;
}
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#importSourceAccountingLines(java.io.InputStream,
* org.kuali.rice.krad.document.TransactionalDocument)
*/
public final List importSourceAccountingLines(String fileName, InputStream stream, AccountingDocument document) {
return importAccountingLines(fileName, stream, document, true);
}
/**
* @see org.kuali.rice.krad.bo.AccountingLineParser#importTargetAccountingLines(java.io.InputStream,
* org.kuali.rice.krad.document.TransactionalDocument)
*/
public final List importTargetAccountingLines(String fileName, InputStream stream, AccountingDocument document) {
return importAccountingLines(fileName, stream, document, false);
}
/**
* performs any additional accounting line validation
*
* @param line
* @param accountingLineAsString
* @throws AccountingLineParserException
*/
protected void validateImportedAccountingLine(AccountingLine line, String accountingLineAsString) throws AccountingLineParserException {
// This check isn't done for the web UI because the code is never input from the user and doesn't correspond to a displayed
// property that could be error highlighted. Throwing an exception here follows the convention of TooFewFieldsException
// and the unchecked NumberFormatException, altho todo: reconsider design, e.g., KULFDBCK-478
String overrideCode = line.getOverrideCode();
if (!AccountingLineOverride.isValidCode(overrideCode)) {
String[] errorParameters = { overrideCode, retrieveAttributeLabel(line.getClass(), OVERRIDE_CODE), accountingLineAsString };
throw new AccountingLineParserException("invalid overrride code '" + overrideCode + "' for:" + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters);
}
}
protected String retrieveAttributeLabel(Class clazz, String attributeName) {
String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(clazz, attributeName);
if (StringUtils.isBlank(label)) {
label = attributeName;
}
return label;
}
protected String[] chooseFormat(Class<? extends AccountingLine> accountingLineClass) {
String[] format = null;
if (SourceAccountingLine.class.isAssignableFrom(accountingLineClass)) {
format = getSourceAccountingLineFormat();
}
else if (TargetAccountingLine.class.isAssignableFrom(accountingLineClass)) {
format = getTargetAccountingLineFormat();
}
else {
throw new IllegalStateException("unknow accounting line class: " + accountingLineClass);
}
return format;
}
}