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 }