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.gl.batch.service.impl;
017
018 import java.io.BufferedReader;
019 import java.io.IOException;
020 import java.io.Reader;
021 import java.util.StringTokenizer;
022
023 import org.apache.commons.lang.StringUtils;
024 import org.kuali.kfs.gl.GeneralLedgerConstants;
025 import org.kuali.kfs.gl.batch.service.ReconciliationParserService;
026 import org.kuali.rice.kns.util.KualiDecimal;
027
028 /**
029 * Format of the reconciliation file:
030 *
031 * <pre>
032 * C tableid rowcount ;
033 * S field1 dollaramount ;
034 * S field2 dollaramount ;
035 * E checksum ;
036 * </pre>
037 *
038 * The character '#' and everything following it on that line is ignored. Whitespace characters are tab and space.<br>
039 * <br>
040 * 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
041 * three codes is not significant.<br>
042 * <br>
043 * Semi-colons are required before any possible comments on C S or E lines. Any amount of whitespace delimits the elements of C, S
044 * and E lines. (If an S line contains field1+field2 for the field element, take care NOT to put any whitespace between the
045 * 'field1', the '+' and the 'field2'.) <br>
046 * <br>
047 * Tableid is an arbitrary identifier for the record<br>
048 * <br>
049 * Rowcount must be a non-negative integer. Fieldn is the technical fieldname(s) in the target database. Case *is* significant,
050 * since this must match the database name(s) exactly.<br>
051 * <br>
052 * Dollaramount may be negative; the check is significant to 4 decimal places.<br>
053 * <br>
054 * 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>
055 * <br>
056 * There may be more than one C-E block per metadata file.<br>
057 * <br>
058 * In general, this implementation of the parser attempts to be error tolerant. It primarily looks at the C-E block that is being
059 * 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
060 * parameter of {@link #parseReconciliationData(Reader, String)}. However, if the C lines of any blocks before the looked for block
061 * are incorrect, then it is likely to cause undesired behavior.
062 */
063 public class ReconciliationParserServiceImpl implements ReconciliationParserService {
064 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(FileEnterpriseFeederHelperServiceImpl.class);
065 private enum ParseState {
066 INIT, TABLE_DEF, COLUMN_DEF, CHECKSUM_DEF;
067 };
068
069
070
071 /**
072 * Parses a reconciliation file
073 *
074 * @param reader a source of data from which to build a reconciliation
075 * @param tableId defined within the reconciliation file; defines which block to parse
076 * @return parsed reconciliation data
077 * @throws IOException thrown if the file cannot be written for any reason
078 * @see org.kuali.kfs.gl.batch.service.ReconciliationParserService#parseReconciliatioData(java.io.Reader)
079 */
080 public ReconciliationBlock parseReconciliationBlock(Reader reader, String tableId) throws IOException {
081 BufferedReader bufReader;
082 if (reader instanceof BufferedReader) {
083 bufReader = (BufferedReader) reader;
084 }
085 else {
086 bufReader = new BufferedReader(reader);
087 }
088
089 // this variable is not null when we find the C line corresponding to the param table ID
090 ReconciliationBlock reconciliationBlock = null;
091
092 int linesInBlock = 0;
093
094 // find the first "C" line of the C-E block by matching the table Id
095 String line = bufReader.readLine();
096 while (line != null && reconciliationBlock == null) {
097 line = stripCommentsAndTrim(line);
098 if (StringUtils.isBlank(line)) {
099 continue;
100 }
101
102 StringTokenizer strTok = new StringTokenizer(line);
103 if (!strTok.hasMoreTokens()) {
104 LOG.error("Cannot find TABLE_DEF_STRING");
105 throw new RuntimeException();
106 }
107 String command = strTok.nextToken();
108 if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.TABLE_DEF_STRING)) {
109 if (!strTok.hasMoreTokens()) {
110 LOG.error("Cannot find TABLE_DEF_STRING");
111 throw new RuntimeException();
112 }
113 String parsedTableId = strTok.nextToken();
114 if (parsedTableId.equalsIgnoreCase(tableId)) {
115 if (!strTok.hasMoreTokens()) {
116 LOG.error("Cannot find Parsed Table Id");
117 throw new RuntimeException();
118 }
119 String parsedRowCountStr = StringUtils.removeEnd(strTok.nextToken(), ";");
120 parsedRowCountStr = StringUtils.removeEnd(parsedRowCountStr, ".00");
121 int parsedRowCount = Integer.parseInt(parsedRowCountStr);
122
123 reconciliationBlock = new ReconciliationBlock();
124 reconciliationBlock.setTableId(parsedTableId);
125 reconciliationBlock.setRowCount(parsedRowCount);
126
127 linesInBlock++;
128
129 break;
130 }
131 }
132 line = bufReader.readLine();
133 }
134
135 if (reconciliationBlock == null) {
136 return null;
137 }
138
139 boolean endBlockLineEncountered = false;
140 line = bufReader.readLine();
141 while (line != null && !endBlockLineEncountered) {
142 line = stripCommentsAndTrim(line);
143 if (StringUtils.isBlank(line)) {
144 continue;
145 }
146
147 StringTokenizer strTok = new StringTokenizer(line);
148 if (!strTok.hasMoreTokens()) {
149 LOG.error("Cannot find COLUMN_DEF_STRING");
150 throw new RuntimeException();
151 }
152
153 String command = strTok.nextToken();
154 if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.COLUMN_DEF_STRING)) {
155 if (!strTok.hasMoreTokens()) {
156 LOG.error("Cannot find COLUMN_DEF_STRING");
157 throw new RuntimeException();
158 }
159 String fieldName = strTok.nextToken();
160 if (!strTok.hasMoreTokens()) {
161 LOG.error("Cannot find COLUMN_DEF_STRING");
162 throw new RuntimeException();
163 }
164 String columnAmountStr = strTok.nextToken();
165 columnAmountStr = StringUtils.removeEnd(columnAmountStr, ";");
166
167 KualiDecimal columnAmount = new KualiDecimal(columnAmountStr);
168 ColumnReconciliation columnReconciliation = new ColumnReconciliation();
169 columnReconciliation.setFieldName(fieldName);
170 columnReconciliation.setDollarAmount(columnAmount);
171 reconciliationBlock.addColumn(columnReconciliation);
172 linesInBlock++;
173 }
174 else if (command.equalsIgnoreCase(GeneralLedgerConstants.Reconciliation.CHECKSUM_DEF_STRING)) {
175 if (!strTok.hasMoreTokens()) {
176 LOG.error("Cannot find CHECKSUM_DEF_STRING");
177 throw new RuntimeException();
178 }
179 String checksumStr = strTok.nextToken();
180 checksumStr = StringUtils.removeEnd(checksumStr, ";");
181
182 int checksum = Integer.parseInt(checksumStr);
183
184 if (checksum != linesInBlock) {
185 LOG.error("Check Sum String is not same as Lines in Block");
186 throw new RuntimeException();
187 }
188 break;
189 }
190 else {
191 LOG.error("Cannot find any fields");
192 throw new RuntimeException();
193 }
194
195 line = bufReader.readLine();
196 }
197 return reconciliationBlock;
198 }
199
200 /**
201 * Removes comments and trims whitespace
202 *
203 * @param line the line
204 * @return stripped and trimmed line
205 */
206 protected String stripCommentsAndTrim(String line) {
207 int commentIndex = line.indexOf(GeneralLedgerConstants.Reconciliation.COMMENT_STRING);
208 if (commentIndex > -1) {
209 // chop off comments
210 line = line.substring(0, commentIndex);
211 }
212
213 line = line.trim();
214 return line;
215 }
216 }