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