/*
* 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.gl.batch;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStreamReader;
import java.sql.Date;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.gl.batch.service.CollectorHelperService;
import org.kuali.kfs.gl.batch.service.impl.OriginEntryTotals;
import org.kuali.kfs.gl.businessobject.CollectorDetail;
import org.kuali.kfs.gl.businessobject.OriginEntryFull;
import org.kuali.kfs.sys.KFSConstants;
import org.kuali.kfs.sys.KFSKeyConstants;
import org.kuali.kfs.sys.KFSPropertyConstants;
import org.kuali.kfs.sys.Message;
import org.kuali.kfs.sys.batch.BatchInputFileTypeBase;
import org.kuali.kfs.sys.businessobject.UniversityDate;
import org.kuali.kfs.sys.context.SpringContext;
import org.kuali.kfs.sys.exception.ParseException;
import org.kuali.kfs.sys.service.UniversityDateService;
import org.kuali.rice.core.api.datetime.DateTimeService;
import org.kuali.rice.krad.util.ErrorMessage;
import org.kuali.rice.krad.util.GlobalVariables;
import org.kuali.rice.krad.util.MessageMap;
import org.springframework.util.AutoPopulatingList;
public class CollectorFlatFileInputType extends BatchInputFileTypeBase {
protected static Logger LOG = Logger.getLogger(CollectorFlatFileInputType.class);
protected DateTimeService dateTimeService;
protected CollectorHelperService collectorHelperService;
protected static final String FILE_NAME_PREFIX = "gl_collectorflatfile_";
/**
* @see org.kuali.kfs.sys.batch.BatchInputType#getAuthorPrincipalName(java.io.File)
*/
public String getAuthorPrincipalName(File file) {
return org.apache.commons.lang.StringUtils.substringBetween(file.getName(), FILE_NAME_PREFIX, "_");
}
/**
* Builds the file name using the following construction: All collector files start with gl_collectorflatfile_ append the chartorg
* from the batch header append the username of the user who is uploading the file then the user supplied indentifier finally
* the timestamp
*
* @param user who uploaded the file
* @param parsedFileContents represents collector batch object
* @param userIdentifier user identifier for user who uploaded file
* @return String returns file name using the convention mentioned in the description
*
* @see org.kuali.kfs.sys.batch.BatchInputFileType#getFileName(org.kuali.rice.kim.api.identity.Person, java.lang.Object,
* java.lang.String)
*/
public String getFileName(String principalName, Object parsedFileContents, String fileUserIdentifer) {
String fileName = FILE_NAME_PREFIX;
fileName += principalName;
if (org.apache.commons.lang.StringUtils.isNotBlank(fileUserIdentifer)) {
fileName += "_" + fileUserIdentifer;
}
fileName += "_" + dateTimeService.toDateTimeStringForFilename(dateTimeService.getCurrentDate());
// remove spaces in filename
fileName = org.apache.commons.lang.StringUtils.remove(fileName, " ");
return fileName;
}
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#getFileTypeIdentifer()
*/
public String getFileTypeIdentifer() {
return KFSConstants.COLLECTOR_FLAT_FILE_TYPE_INDENTIFIER;
}
/**
* @see org.kuali.kfs.sys.batch.BatchInputType#getTitleKey()
*/
public String getTitleKey() {
return KFSKeyConstants.MESSAGE_BATCH_UPLOAD_TITLE_COLLECTOR_FLAT_FILE;
}
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#parse(byte[])
*/
public Object parse(byte[] fileByteContent) throws ParseException {
List<CollectorBatch> batchList = new ArrayList<CollectorBatch>();
CollectorBatch currentBatch = null;
BufferedReader bufferedFileReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(fileByteContent)));
String fileLine;
Date curDate = SpringContext.getBean(DateTimeService.class).getCurrentSqlDate();
UniversityDate universityDate = SpringContext.getBean(UniversityDateService.class).getCurrentUniversityDate();
int lineNumber = 0;
//temporary storage of the balance type map of each accounting record/origin entry
Map<String, String> accountRecordBalanceTypeMap = new HashMap<String, String>();
try {
while ((fileLine = bufferedFileReader.readLine()) != null) {
lineNumber++;
String preprocessedLine = preprocessLine(fileLine);
if (fileLine.length() >= 27) { //if no rec_type, probably a blank or almost blank line at end of file
String recordType = extractRecordType(preprocessedLine);
if ("HD".equals(recordType)) { // this is a header line
ensurePreviousBatchTerminated(currentBatch, lineNumber);
try {
currentBatch = createCollectorBatch(preprocessedLine, batchList);
}
catch (RuntimeException e) {
if (currentBatch == null){
currentBatch = new CollectorBatch();
batchList.add(currentBatch);
}
currentBatch.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_CUSTOM, e.getMessage());
}
}
else if ("DT".equals(recordType)) { //ID billing detail
currentBatch = createHeaderlessBatchIfNecessary(currentBatch, batchList, lineNumber);
try {
CollectorDetail collectorDetail = createCollectorDetail(preprocessedLine, accountRecordBalanceTypeMap, curDate, universityDate, lineNumber, currentBatch.getMessageMap());
currentBatch.addCollectorDetail(collectorDetail);
}
catch (RuntimeException e) {
currentBatch.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_CUSTOM, e.getMessage());
}
}
else if ("TL".equals(recordType)) { //trailer record
currentBatch = createHeaderlessBatchIfNecessary(currentBatch, batchList, lineNumber);
try {
updateCollectorDetailWithTrailerRecords(currentBatch, preprocessedLine, lineNumber);
}
catch (RuntimeException e) {
currentBatch.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_CUSTOM, e.getMessage());
}
currentBatch = null;
}
else { // accounting record/origin entry
currentBatch = createHeaderlessBatchIfNecessary(currentBatch, batchList, lineNumber);
try {
OriginEntryFull originEntry = createOriginEntry(preprocessedLine, curDate, universityDate, lineNumber, currentBatch.getMessageMap());
currentBatch.addOriginEntry(originEntry);
//use origin entry to derive a key to store the balance type to the map
String accountRecordKey = generateAccountRecordBalanceTypeKey(originEntry);
accountRecordBalanceTypeMap.put(accountRecordKey, originEntry.getFinancialBalanceTypeCode());
}
catch (RuntimeException e) {
currentBatch.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_CUSTOM, e.getMessage());
}
}
}
}
// in case we come across a batch that didn't have a trailer record
ensurePreviousBatchTerminated(currentBatch, lineNumber);
} catch (Exception e) {
LOG.error(e.getMessage() + " happend in CollectorFlatFileInputType.parse.", e);
throw new ParseException(e.getMessage() + " happend in CollectorFlatFileInputType.parse.", e);
}
for (CollectorBatch batch : batchList) {
OriginEntryTotals totals = new OriginEntryTotals();
totals.addToTotals(batch.getOriginEntries().iterator());
batch.setOriginEntryTotals(totals);
// now copy all the messages from the per-batch message map into the global map, so that we can display the messages on the upload screen
copyAllMessages(batch.getMessageMap(), GlobalVariables.getMessageMap());
}
return batchList;
}
/**
* Account record balance type key
* Fiscal Year - Chart Code - Account Number - Sub-account Number - Object code - Sub-object Code
*
* For the two optional fields sub-account and sub-object code, create an additional filter to replace
* the usual place holder - with spaces
*
* @param originEntry
* @return
*/
private String generateAccountRecordBalanceTypeKey(OriginEntryFull originEntry) {
StringBuilder builder = new StringBuilder();
builder.append(originEntry.getUniversityFiscalYear()).append("|")
.append(originEntry.getChartOfAccountsCode()).append("|")
.append(originEntry.getAccountNumber()).append("|")
.append(StringUtils.replace(originEntry.getSubAccountNumber(), "-", "")).append("|")
.append(originEntry.getFinancialObjectCode()).append("|")
.append(StringUtils.replace(originEntry.getFinancialSubObjectCode(), "-", ""));
return builder.toString();
}
private void copyAllMessages(MessageMap sourceMap, MessageMap destMap) {
copyAllMessagesHelper(sourceMap.getInfoMessages(), "info", destMap);
copyAllMessagesHelper(sourceMap.getWarningMessages(), "warning", destMap);
copyAllMessagesHelper(sourceMap.getErrorMessages(), "error", destMap);
}
private void copyAllMessagesHelper(Map<String, AutoPopulatingList<ErrorMessage>> sourceMessages, String type, MessageMap destMap) {
for (String key : sourceMessages.keySet()) {
AutoPopulatingList<ErrorMessage> messages = sourceMessages.get(key);
if (messages != null) {
for (Object o : messages) {
ErrorMessage message = (ErrorMessage) o;
if ("info".equals(type)) {
destMap.putInfoWithoutFullErrorPath(key, message.getErrorKey(), message.getMessageParameters());
}
else if ("warning".equals(type)) {
destMap.putWarningWithoutFullErrorPath(key, message.getErrorKey(), message.getMessageParameters());
}
else if ("error".equals(type)) {
destMap.putErrorWithoutFullErrorPath(key, message.getErrorKey(), message.getMessageParameters());
}
else {
throw new IllegalArgumentException();
}
}
}
}
}
protected void ensurePreviousBatchTerminated(CollectorBatch currentBatch, int lineNumber) {
if (currentBatch != null) {
// we've encountered a new header, when we're still not done parsing the previous batch (i.e. trailer not found). This is an error, and mark the old batch as such
currentBatch.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.MISSING_TRAILER_RECORD, Integer.toString(lineNumber));
currentBatch.setTotalAmount("0");
currentBatch.setTotalRecords(0);
}
}
protected CollectorBatch createHeaderlessBatchIfNecessary(CollectorBatch currentBatch, List<CollectorBatch> batchList, int lineNumber) {
if (currentBatch != null) {
return currentBatch;
}
CollectorBatch headerlessBatch = new CollectorBatch();
headerlessBatch.setHeaderlessBatch(true);
headerlessBatch.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.Collector.MISSING_HEADER_RECORD, Integer.toString(lineNumber));
batchList.add(headerlessBatch);
return headerlessBatch;
}
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#process(java.lang.String, java.lang.Object)
*/
public void process(String fileName, Object parsedFileContents) {
// do not do anything
}
protected String addDecimalPoint (String amount) {
if (!amount.contains(".")) { //have to add decimal point if it's missing
int length = amount.length();
amount = amount.substring(0, length - 2) + "." + amount.substring(length - 2, length);
}
return amount;
}
protected CollectorBatch createCollectorBatch(String headerLine, List<CollectorBatch> batchList) {
CollectorBatch newBatch = new CollectorBatch();
newBatch.setFromTextFileForCollectorBatch(headerLine);
batchList.add(newBatch);
return newBatch;
}
protected CollectorDetail createCollectorDetail(String detailLine, Map<String, String>accountRecordBalanceTypeMap, Date curDate, UniversityDate universityDate, int lineNumber, MessageMap messageMap) {
CollectorDetail collectorDetail = new CollectorDetail();
collectorDetail.setFromFileForCollectorDetail(detailLine, accountRecordBalanceTypeMap, curDate, universityDate, lineNumber, messageMap);
return collectorDetail;
}
protected void updateCollectorDetailWithTrailerRecords(CollectorBatch currentBatch, String fileLine, int lineNumber) {
currentBatch.setFromTextFileForCollectorBatchTrailerRecord(fileLine, lineNumber);
}
protected OriginEntryFull createOriginEntry(String fileLine, Date curDate, UniversityDate universityDate, int lineNumber, MessageMap messageMap) {
OriginEntryFull originEntry = new OriginEntryFull();
try{
List<Message> originEntryErrorMessages = originEntry.setFromTextFileForBatch(fileLine, lineNumber);
if (null == originEntry.getTransactionDate()) {
originEntry.setTransactionDate(curDate);
}
if (StringUtils.isBlank(originEntry.getUniversityFiscalPeriodCode())) {
originEntry.setUniversityFiscalPeriodCode(universityDate.getUniversityFiscalAccountingPeriod());
}
if ( null == originEntry.getTransactionLedgerEntrySequenceNumber() ) {
originEntry.setTransactionLedgerEntrySequenceNumber(new Integer(1));
}
if (StringUtils.isBlank(originEntry.getSubAccountNumber())) {
originEntry.setSubAccountNumber(" ");
}
if (StringUtils.isBlank(originEntry.getFinancialSubObjectCode())) {
originEntry.setFinancialSubObjectCode(" ");
}
for (Message orginEntryError : originEntryErrorMessages) {
messageMap.putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_CUSTOM, orginEntryError.getMessage());
}
} catch (Exception e){
throw new RuntimeException(e + " occurred in CollectorFlatFileInputType.createOriginEntry()",e);
}
originEntry.setTransactionLedgerEntryAmount(addDecimalPoint(originEntry.getTransactionLedgerEntryAmount().toString()));
return originEntry;
}
public boolean validate(Object parsedFileContents) {
List<CollectorBatch> collectorBatches = (List<CollectorBatch>) parsedFileContents;
for (CollectorBatch collectorBatch : collectorBatches) {
boolean isValid = collectorHelperService.performValidation(collectorBatch);
if (isValid) {
isValid = collectorHelperService.checkTrailerTotals(collectorBatch, null);
}
if (!isValid) {
return false;
}
}
return true;
}
/**
* Sets the dateTimeService attribute value.
* @param dateTimeService The dateTimeService to set.
*/
public void setDateTimeService(DateTimeService dateTimeService) {
this.dateTimeService = dateTimeService;
}
/**
* Sets the collectorHelperService attribute value.
* @param collectorHelperService The collectorHelperService to set.
*/
public void setCollectorHelperService(CollectorHelperService collectorHelperService) {
this.collectorHelperService = collectorHelperService;
}
protected String extractRecordType(String line) {
if (line == null || line.length() < 27) {
return null;
}
String recordType = null;
CollectorBatch collectorBatch = new CollectorBatch();
final Map<String, Integer> pMap = CollectorBatch.getCollectorBatchHeaderFieldUtil().getFieldBeginningPositionMap();
recordType = (collectorBatch.getValue(line, pMap.get(KFSPropertyConstants.COLLECTOR_BATCH_RECORD_TYPE), pMap.get(KFSPropertyConstants.BATCH_SEQUENCE_NUMBER)));
return recordType;
}
protected String preprocessLine(String line) {
return line;
}
}