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 }