/*
* 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.service.impl;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.StringTokenizer;
import org.apache.commons.lang.StringUtils;
import org.kuali.kfs.gl.GeneralLedgerConstants;
import org.kuali.kfs.gl.batch.service.ReconciliationParserService;
import org.kuali.rice.core.api.util.type.KualiDecimal;
/**
* Format of the reconciliation file:
*
* <pre>
* C tableid rowcount ;
* S field1 dollaramount ;
* S field2 dollaramount ;
* E checksum ;
* </pre>
*
* The character '#' and everything following it on that line is ignored. Whitespace characters are tab and space.<br>
* <br>
* A 'C' 'S' or 'E' must be the first character on a line unless the line is entirely whitespace or a comment. The case of these
* three codes is not significant.<br>
* <br>
* Semi-colons are required before any possible comments on C S or E lines. Any amount of whitespace delimits the elements of C, S
* and E lines. (If an S line contains field1+field2 for the field element, take care NOT to put any whitespace between the
* 'field1', the '+' and the 'field2'.) <br>
* <br>
* Tableid is an arbitrary identifier for the record<br>
* <br>
* Rowcount must be a non-negative integer. Fieldn is the technical fieldname(s) in the target database. Case *is* significant,
* since this must match the database name(s) exactly.<br>
* <br>
* Dollaramount may be negative; the check is significant to 4 decimal places.<br>
* <br>
* The checksum on line E is the number of C and S lines. A C line and a terminating E line are mandatory; S lines are optional.<br>
* <br>
* There may be more than one C-E block per metadata file.<br>
* <br>
* In general, this implementation of the parser attempts to be error tolerant. It primarily looks at the C-E block that is being
* looked for, by ignoring all other C-E blocks. A C-E block is "looked for" when the table ID of the C line is passed in as a
* parameter of {@link #parseReconciliationData(Reader, String)}. However, if the C lines of any blocks before the looked for block
* are incorrect, then it is likely to cause undesired behavior.
*/
public class ReconciliationParserServiceImpl implements ReconciliationParserService {
private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FileEnterpriseFeederHelperServiceImpl.class);
private enum ParseState {
INIT, TABLE_DEF, COLUMN_DEF, CHECKSUM_DEF;
};
/**
* Parses a reconciliation file
*
* @param reader a source of data from which to build a reconciliation
* @param tableId defined within the reconciliation file; defines which block to parse
* @return parsed reconciliation data
* @throws IOException thrown if the file cannot be written for any reason
* @see org.kuali.kfs.gl.batch.service.ReconciliationParserService#parseReconciliatioData(java.io.Reader)
*/
public ReconciliationBlock parseReconciliationBlock(Reader reader, String tableId) throws IOException {
BufferedReader bufReader;
if (reader instanceof BufferedReader) {
bufReader = (BufferedReader) reader;
}
else {
bufReader = new BufferedReader(reader);
}
// this variable is not null when we find the C line corresponding to the param table ID
ReconciliationBlock reconciliationBlock = null;
int linesInBlock = 0;
// find the first "C" line of the C-E block by matching the table Id
String line = bufReader.readLine();
while (line != null && reconciliationBlock == null) {
line = stripCommentsAndTrim(line);
if (StringUtils.isBlank(line)) {
line = bufReader.readLine();
continue;
}
StringTokenizer strTok = new StringTokenizer(line);
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find TABLE_DEF_STRING");
throw new RuntimeException();
}
String command = strTok.nextToken();
if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.TABLE_DEF_STRING)) {
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find TABLE_DEF_STRING");
throw new RuntimeException();
}
String parsedTableId = strTok.nextToken();
if (parsedTableId.equalsIgnoreCase(tableId)) {
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find Parsed Table Id");
throw new RuntimeException();
}
String parsedRowCountStr = StringUtils.removeEnd(strTok.nextToken(), ";");
parsedRowCountStr = StringUtils.removeEnd(parsedRowCountStr, ".00");
int parsedRowCount = Integer.parseInt(parsedRowCountStr);
reconciliationBlock = new ReconciliationBlock();
reconciliationBlock.setTableId(parsedTableId);
reconciliationBlock.setRowCount(parsedRowCount);
linesInBlock++;
break;
}
}
line = bufReader.readLine();
}
if (reconciliationBlock == null) {
return null;
}
boolean endBlockLineEncountered = false;
line = bufReader.readLine();
while (line != null && !endBlockLineEncountered) {
line = stripCommentsAndTrim(line);
if (StringUtils.isBlank(line)) {
continue;
}
StringTokenizer strTok = new StringTokenizer(line);
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find COLUMN_DEF_STRING");
throw new RuntimeException();
}
String command = strTok.nextToken();
if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.COLUMN_DEF_STRING)) {
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find COLUMN_DEF_STRING");
throw new RuntimeException();
}
String fieldName = strTok.nextToken();
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find COLUMN_DEF_STRING");
throw new RuntimeException();
}
String columnAmountStr = strTok.nextToken();
columnAmountStr = StringUtils.removeEnd(columnAmountStr, ";");
KualiDecimal columnAmount = new KualiDecimal(columnAmountStr);
ColumnReconciliation columnReconciliation = new ColumnReconciliation();
columnReconciliation.setFieldName(fieldName);
columnReconciliation.setDollarAmount(columnAmount);
reconciliationBlock.addColumn(columnReconciliation);
linesInBlock++;
}
else if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.CHECKSUM_DEF_STRING)) {
if (!strTok.hasMoreTokens()) {
LOG.error("Cannot find CHECKSUM_DEF_STRING");
throw new RuntimeException();
}
String checksumStr = strTok.nextToken();
checksumStr = StringUtils.removeEnd(checksumStr, ";");
int checksum = Integer.parseInt(checksumStr);
if (checksum != linesInBlock) {
LOG.error("Check Sum String is not same as Lines in Block");
throw new RuntimeException();
}
break;
}
else {
LOG.error("Cannot find any fields");
throw new RuntimeException();
}
line = bufReader.readLine();
}
return reconciliationBlock;
}
/**
* Removes comments and trims whitespace
*
* @param line the line
* @return stripped and trimmed line
*/
protected String stripCommentsAndTrim(String line) {
int commentIndex = line.indexOf(GeneralLedgerConstants.Reconciliation.COMMENT_STRING);
if (commentIndex > -1) {
// chop off comments
line = line.substring(0, commentIndex);
}
line = line.trim();
return line;
}
}