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    }