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