001/* 002 * Copyright 2007 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 */ 016package org.kuali.ole.gl.batch.service.impl; 017 018import java.util.ArrayList; 019import java.util.Iterator; 020import java.util.List; 021 022import org.apache.commons.lang.StringUtils; 023import org.kuali.ole.gl.batch.dataaccess.ReconciliationDao; 024import org.kuali.ole.gl.batch.service.ReconciliationService; 025import org.kuali.ole.gl.businessobject.OriginEntryFull; 026import org.kuali.ole.gl.exception.LoadException; 027import org.kuali.ole.sys.Message; 028import org.kuali.rice.core.api.util.type.KualiDecimal; 029import org.kuali.rice.core.api.util.type.TypeUtils; 030import org.springframework.transaction.annotation.Transactional; 031 032/** 033 * Default implementation of ReconciliationService 034 */ 035@Transactional 036public class ReconciliationServiceImpl implements ReconciliationService { 037 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReconciliationServiceImpl.class); 038 039 private ReconciliationDao reconciliationDao; 040 private Class<? extends OriginEntryFull> originEntryClass; 041 042 /** 043 * A wrapper around {@link ColumnReconciliation} objects to provide it with information specific to the java beans representing 044 * each BO. <br/><br/> In the default implementation of {@link org.kuali.ole.gl.batch.service.ReconciliationParserService}, each 045 * {@link ColumnReconciliation} object may actually represent the sum of multiple fields across all origin entries (i.e. 046 * ColumnReconciliation.getTokenizedFieldNames().length may be > 1). <br/><br/> Furthermore, the parser service returns 047 * database field names as the identifier. This service requires the use of java bean names, so this class is used to maintain a 048 * mapping between the DB names (in columnReconciliation.getTokenizedFieldNames()) and the java bean names (in 049 * javaAttributeNames). These lists/arrays are the same size, and each element at the same position in both lists are mapped to 050 * each other. 051 */ 052 protected class JavaAttributeAugmentedColumnReconciliation { 053 protected ColumnReconciliation columnReconciliation; 054 protected List<String> javaAttributeNames; 055 056 protected JavaAttributeAugmentedColumnReconciliation() { 057 columnReconciliation = null; 058 javaAttributeNames = null; 059 } 060 061 /** 062 * Gets the columnReconciliation attribute. 063 * 064 * @return Returns the columnReconciliation. 065 */ 066 protected ColumnReconciliation getColumnReconciliation() { 067 return columnReconciliation; 068 } 069 070 /** 071 * Sets the columnReconciliation attribute value. 072 * 073 * @param columnReconciliation The columnReconciliation to set. 074 */ 075 protected void setColumnReconciliation(ColumnReconciliation columnReconciliation) { 076 this.columnReconciliation = columnReconciliation; 077 } 078 079 /** 080 * Sets the javaAttributeNames attribute value. 081 * 082 * @param javaAttributeNames The javaAttributeNames to set. 083 */ 084 protected void setJavaAttributeNames(List<String> javaAttributeNames) { 085 this.javaAttributeNames = javaAttributeNames; 086 } 087 088 protected String getJavaAttributeName(int index) { 089 return javaAttributeNames.get(index); 090 } 091 092 /** 093 * Returns the number of attributes this object is holing 094 * 095 * @return the count of attributes this holding 096 */ 097 protected int size() { 098 return javaAttributeNames.size(); 099 } 100 } 101 102 /** 103 * Performs the reconciliation on origin entries using the data from the {@link ReconciliationBlock} parameter 104 * 105 * @param entries origin entries 106 * @param reconBlock reconciliation data 107 * @param errorMessages a non-null list onto which error messages will be appended. This list will be modified by reference. 108 * @see org.kuali.ole.gl.batch.service.ReconciliationService#reconcile(java.util.Iterator, 109 * org.kuali.ole.gl.batch.service.impl.ReconciliationBlock, java.util.List) 110 */ 111 public void reconcile(Iterator<OriginEntryFull> entries, ReconciliationBlock reconBlock, List<Message> errorMessages) { 112 List<ColumnReconciliation> columns = reconBlock.getColumns(); 113 114 int numEntriesSuccessfullyLoaded = 0; 115 116 // this value gets incremented every time the hasNext method of the iterator is called 117 int numEntriesAttemptedToLoad = 1; 118 119 // precompute the DB -> java name mappings so that we don't have to recompute them once for each row 120 List<JavaAttributeAugmentedColumnReconciliation> javaAttributeNames = resolveJavaAttributeNames(columns); 121 KualiDecimal[] columnSums = createColumnSumsArray(columns.size()); 122 123 // because of the way the OriginEntryFileIterator works (which is likely to be the type passed in as a parameter), 124 // there are 2 primary causes of exceptions to be thrown by the Iterator.hasNext method: 125 // 126 // - Underlying IO exception, this is a fatal error (i.e. we no longer attempt to continue parsing the file) 127 // - Misformatted origin entry line, which is not fatal (i.e. continue parsing the file and report further misformatted 128 // lines), but if it occurs, we don't want to do the final reconciliation step after this loop 129 130 // operator short-circuiting is utilized to ensure that if there's a fatal error then we don't try to keep reading 131 132 boolean entriesFullyIterated = false; 133 134 // set to true if there's a problem parsing origin entry line(s) 135 boolean loadExceptionEncountered = false; 136 137 while (!entriesFullyIterated) { 138 try { 139 while (entries.hasNext()) { 140 numEntriesAttemptedToLoad++; 141 OriginEntryFull entry = entries.next(); 142 for (int c = 0; c < columns.size(); c++) { 143 // this is for each definition of the "S" line in the reconciliation file 144 KualiDecimal columnValue = KualiDecimal.ZERO; 145 146 for (int f = 0; f < javaAttributeNames.get(c).size(); f++) { 147 String javaAttributeName = javaAttributeNames.get(c).getJavaAttributeName(f); 148 Object fieldValue = entry.getFieldValue(javaAttributeName); 149 150 if (fieldValue == null) { 151 // what to do about nulls? 152 } 153 else { 154 if (TypeUtils.isIntegralClass(fieldValue.getClass()) || TypeUtils.isDecimalClass(fieldValue.getClass())) { 155 KualiDecimal castValue; 156 if (fieldValue instanceof KualiDecimal) { 157 castValue = (KualiDecimal) fieldValue; 158 } 159 else { 160 castValue = new KualiDecimal(fieldValue.toString()); 161 } 162 columnValue = columnValue.add(castValue); 163 } 164 else { 165 throw new LoadException("The value for " + columns.get(c).getTokenizedFieldNames()[f] + " is not a numeric value."); 166 } 167 } 168 } 169 columnSums[c] = columnSums[c].add(columnValue); 170 } 171 numEntriesSuccessfullyLoaded++; 172 } 173 } 174 catch (LoadException e) { 175 loadExceptionEncountered = true; 176 LOG.error("Line " + numEntriesAttemptedToLoad + " parse error: " + e.getMessage(), e); 177 Message newMessage = new Message("Line " + numEntriesAttemptedToLoad + " parse error: " + e.getMessage(), Message.TYPE_FATAL); 178 errorMessages.add(newMessage); 179 180 numEntriesAttemptedToLoad++; 181 continue; 182 } 183 catch (Exception e) { 184 // entriesFullyIterated will stay false when we break out 185 186 // encountered a potentially serious problem, abort reading of the data 187 LOG.error("Error encountered trying to iterate through origin entry iterator", e); 188 189 Message newMessage = new Message(e.getMessage(), Message.TYPE_FATAL); 190 errorMessages.add(newMessage); 191 192 break; 193 } 194 entriesFullyIterated = true; 195 } 196 197 if (entriesFullyIterated) { 198 if (loadExceptionEncountered) { 199 // generate a message saying reconcilation check did not continue 200 LOG.error("Reconciliation check failed because some origin entry lines could not be parsed."); 201 Message newMessage = new Message("Reconciliation check failed because some origin entry lines could not be parsed.", Message.TYPE_FATAL); 202 errorMessages.add(newMessage); 203 } 204 else { 205 // see if the rowcount matches 206 if (numEntriesSuccessfullyLoaded != reconBlock.getRowCount()) { 207 Message newMessage = generateRowCountMismatchMessage(reconBlock, numEntriesSuccessfullyLoaded); 208 errorMessages.add(newMessage); 209 } 210 211 // now that we've computed the statistics for all of the origin entries in the iterator, 212 // compare the actual statistics (in the columnSums array) with the stats provided in the 213 // reconciliation file (in the "columns" List attribute reconBlock object). Both of these 214 // array/lists should have the same size 215 for (int i = 0; i < columns.size(); i++) { 216 if (!columnSums[i].equals(columns.get(i).getDollarAmount())) { 217 Message newMessage = generateColumnSumErrorMessage(columns.get(i), columnSums[i]); 218 errorMessages.add(newMessage); 219 } 220 } 221 } 222 } 223 } 224 225 /** 226 * Generates the error message for the sum of column(s) not matching the reconciliation value 227 * 228 * @param column the column reconciliation data (recall that this "column" can be the sum of several columns) 229 * @param actualValue the value of the column(s) 230 * @return the message 231 */ 232 protected Message generateColumnSumErrorMessage(ColumnReconciliation column, KualiDecimal actualValue) { 233 // TODO: if the kualiConfiguration service were to implement message params from ApplicationResources.properties, this would 234 // be ideal for that 235 StringBuilder buf = new StringBuilder(); 236 buf.append("Reconciliation failed for field value(s) \""); 237 buf.append(column.getFieldName()); 238 buf.append("\", expected "); 239 buf.append(column.getDollarAmount()); 240 buf.append(", found value "); 241 buf.append(actualValue); 242 buf.append("."); 243 244 Message newMessage = new Message(buf.toString(), Message.TYPE_FATAL); 245 return newMessage; 246 } 247 248 /** 249 * Generates the error message for the number of entries reconciled being unequal to the expected value 250 * 251 * @param block The file reconciliation data 252 * @param actualRowCount the number of rows encountered 253 * @return the message 254 */ 255 protected Message generateRowCountMismatchMessage(ReconciliationBlock block, int actualRowCount) { 256 // TODO: if the kualiConfiguration service were to implement message params from ApplicationResources.properties, this would 257 // be ideal for that 258 StringBuilder buf = new StringBuilder(); 259 buf.append("Reconciliation failed because an incorrect number of origin entry rows were successfully parsed. Expected "); 260 buf.append(block.getRowCount()); 261 buf.append(" row(s), parsed "); 262 buf.append(actualRowCount); 263 buf.append(" row(s)."); 264 265 Message newMessage = new Message(buf.toString(), Message.TYPE_FATAL); 266 return newMessage; 267 } 268 269 /** 270 * Performs basic checking to ensure that values are set up so that reconciliation can proceed 271 * 272 * @param columns the columns generated by the {@link org.kuali.ole.gl.batch.service.ReconciliationParserService} 273 * @param javaAttributeNames the java attribute names corresponding to each field in columns. (see 274 * {@link #resolveJavaAttributeNames(List)}) 275 * @param columnSums a list of KualiDecimals used to store column sums as reconciliation iterates through the origin entries 276 * @param errorMessages a list to which error messages will be appended. 277 * @return true if there are no problems, false otherwise 278 */ 279 protected boolean performSanityChecks(List<ColumnReconciliation> columns, List<JavaAttributeAugmentedColumnReconciliation> javaAttributeNames, KualiDecimal[] columnSums, List<Message> errorMessages) { 280 boolean success = true; 281 282 if (javaAttributeNames.size() != columnSums.length || javaAttributeNames.size() != columns.size()) { 283 // sanity check 284 errorMessages.add(new Message("Reconciliation error: Sizes of lists do not match", Message.TYPE_FATAL)); 285 success = false; 286 } 287 for (int i = 0; i < columns.size(); i++) { 288 if (columns.get(i).getTokenizedFieldNames().length != javaAttributeNames.get(i).size()) { 289 errorMessages.add(new Message("Reconciliation error: Error tokenizing column elements. The number of database fields and java fields do not match.", Message.TYPE_FATAL)); 290 success = false; 291 } 292 for (int fieldIdx = 0; fieldIdx < javaAttributeNames.get(i).size(); i++) { 293 if (StringUtils.isBlank(javaAttributeNames.get(i).getJavaAttributeName(fieldIdx))) { 294 errorMessages.add(new Message("Reconciliation error: javaAttributeName is blank for DB column: " + columns.get(i).getTokenizedFieldNames()[fieldIdx], Message.TYPE_FATAL)); 295 success = false; 296 } 297 } 298 } 299 return success; 300 } 301 302 /** 303 * Creates an array of {@link KualiDecimal}s of a given size, and initializes all elements to {@link KualiDecimal#ZERO} 304 * 305 * @param size the size of the constructed array 306 * @return the array, all initialized to {@link KualiDecimal#ZERO} 307 */ 308 protected KualiDecimal[] createColumnSumsArray(int size) { 309 KualiDecimal[] array = new KualiDecimal[size]; 310 for (int i = 0; i < array.length; i++) { 311 array[i] = KualiDecimal.ZERO; 312 } 313 return array; 314 } 315 316 /** 317 * Resolves a mapping between the database columns and the java attribute name (i.e. bean property names) 318 * 319 * @param columns columns parsed by the {@link org.kuali.ole.gl.batch.service.ReconciliationParserService} 320 * @return a list of {@link JavaAttributeAugmentedColumnReconciliation} (see class description) objects. The returned list will 321 * have the same size as the parameter, and each element in one list corresponds to the element at the same position in 322 * the other list 323 */ 324 protected List<JavaAttributeAugmentedColumnReconciliation> resolveJavaAttributeNames(List<ColumnReconciliation> columns) { 325 List<JavaAttributeAugmentedColumnReconciliation> attributes = new ArrayList<JavaAttributeAugmentedColumnReconciliation>(); 326 for (ColumnReconciliation column : columns) { 327 JavaAttributeAugmentedColumnReconciliation c = new JavaAttributeAugmentedColumnReconciliation(); 328 c.setColumnReconciliation(column); 329 c.setJavaAttributeNames(reconciliationDao.convertDBColumnNamesToJavaName(getOriginEntryClass(), column.getTokenizedFieldNames(), true)); 330 attributes.add(c); 331 } 332 return attributes; 333 } 334 335 /** 336 * Gets the reconciliationDao attribute. 337 * 338 * @return Returns the reconciliationDao. 339 */ 340 protected ReconciliationDao getReconciliationDao() { 341 return reconciliationDao; 342 } 343 344 /** 345 * Sets the reconciliationDao attribute value. 346 * 347 * @param reconciliationDao The reconciliationDao to set. 348 */ 349 public void setReconciliationDao(ReconciliationDao reconciliationDao) { 350 this.reconciliationDao = reconciliationDao; 351 } 352 353 /** 354 * Gets the originEntryClass attribute. 355 * 356 * @return Returns the originEntryClass. 357 */ 358 protected Class<? extends OriginEntryFull> getOriginEntryClass() { 359 return originEntryClass; 360 } 361 362 /** 363 * Sets the originEntryClass attribute value. 364 * 365 * @param originEntryClass The originEntryClass to set. 366 */ 367 public void setOriginEntryClass(Class<? extends OriginEntryFull> originEntryClass) { 368 this.originEntryClass = originEntryClass; 369 } 370}