View Javadoc
1   /*
2    * Copyright 2007 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl2.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.ole.gl.batch.service.impl;
17  
18  import java.util.ArrayList;
19  import java.util.Iterator;
20  import java.util.List;
21  
22  import org.apache.commons.lang.StringUtils;
23  import org.kuali.ole.gl.batch.dataaccess.ReconciliationDao;
24  import org.kuali.ole.gl.batch.service.ReconciliationService;
25  import org.kuali.ole.gl.businessobject.OriginEntryFull;
26  import org.kuali.ole.gl.exception.LoadException;
27  import org.kuali.ole.sys.Message;
28  import org.kuali.rice.core.api.util.type.KualiDecimal;
29  import org.kuali.rice.core.api.util.type.TypeUtils;
30  import org.springframework.transaction.annotation.Transactional;
31  
32  /**
33   * Default implementation of ReconciliationService
34   */
35  @Transactional
36  public class ReconciliationServiceImpl implements ReconciliationService {
37      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReconciliationServiceImpl.class);
38  
39      private ReconciliationDao reconciliationDao;
40      private Class<? extends OriginEntryFull> originEntryClass;
41  
42      /**
43       * A wrapper around {@link ColumnReconciliation} objects to provide it with information specific to the java beans representing
44       * each BO. <br/><br/> In the default implementation of {@link org.kuali.ole.gl.batch.service.ReconciliationParserService}, each
45       * {@link ColumnReconciliation} object may actually represent the sum of multiple fields across all origin entries (i.e.
46       * ColumnReconciliation.getTokenizedFieldNames().length may be > 1). <br/><br/> Furthermore, the parser service returns
47       * database field names as the identifier. This service requires the use of java bean names, so this class is used to maintain a
48       * mapping between the DB names (in columnReconciliation.getTokenizedFieldNames()) and the java bean names (in
49       * javaAttributeNames). These lists/arrays are the same size, and each element at the same position in both lists are mapped to
50       * each other.
51       */
52      protected class JavaAttributeAugmentedColumnReconciliation {
53          protected ColumnReconciliation columnReconciliation;
54          protected List<String> javaAttributeNames;
55  
56          protected JavaAttributeAugmentedColumnReconciliation() {
57              columnReconciliation = null;
58              javaAttributeNames = null;
59          }
60  
61          /**
62           * Gets the columnReconciliation attribute.
63           * 
64           * @return Returns the columnReconciliation.
65           */
66          protected ColumnReconciliation getColumnReconciliation() {
67              return columnReconciliation;
68          }
69  
70          /**
71           * Sets the columnReconciliation attribute value.
72           * 
73           * @param columnReconciliation The columnReconciliation to set.
74           */
75          protected void setColumnReconciliation(ColumnReconciliation columnReconciliation) {
76              this.columnReconciliation = columnReconciliation;
77          }
78  
79          /**
80           * Sets the javaAttributeNames attribute value.
81           * 
82           * @param javaAttributeNames The javaAttributeNames to set.
83           */
84          protected void setJavaAttributeNames(List<String> javaAttributeNames) {
85              this.javaAttributeNames = javaAttributeNames;
86          }
87  
88          protected String getJavaAttributeName(int index) {
89              return javaAttributeNames.get(index);
90          }
91  
92          /**
93           * Returns the number of attributes this object is holing
94           * 
95           * @return the count of attributes this holding
96           */
97          protected int size() {
98              return javaAttributeNames.size();
99          }
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 }