View Javadoc
1   /*
2    * The Kuali Financial System, a comprehensive financial management system for higher education.
3    * 
4    * Copyright 2005-2014 The Kuali Foundation
5    * 
6    * This program is free software: you can redistribute it and/or modify
7    * it under the terms of the GNU Affero General Public License as
8    * published by the Free Software Foundation, either version 3 of the
9    * License, or (at your option) any later version.
10   * 
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14   * GNU Affero General Public License for more details.
15   * 
16   * You should have received a copy of the GNU Affero General Public License
17   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18   */
19  package org.kuali.kfs.module.ld.batch.service.impl;
20  
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.List;
24  
25  import org.apache.commons.lang.StringUtils;
26  import org.kuali.kfs.gl.batch.dataaccess.ReconciliationDao;
27  import org.kuali.kfs.gl.batch.service.impl.ColumnReconciliation;
28  import org.kuali.kfs.gl.batch.service.impl.ReconciliationBlock;
29  import org.kuali.kfs.gl.businessobject.OriginEntryFull;
30  import org.kuali.kfs.gl.exception.LoadException;
31  import org.kuali.kfs.module.ld.batch.service.ReconciliationService;
32  import org.kuali.kfs.module.ld.businessobject.LaborOriginEntry;
33  import org.kuali.kfs.sys.Message;
34  import org.kuali.rice.core.api.util.type.KualiDecimal;
35  import org.kuali.rice.core.api.util.type.TypeUtils;
36  import org.springframework.transaction.annotation.Transactional;
37  
38  /**
39   * Default implementation of ReconciliationService
40   */
41  @Transactional
42  public class ReconciliationServiceImpl implements ReconciliationService {
43      private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReconciliationServiceImpl.class);
44  
45      private ReconciliationDao reconciliationDao;
46      private Class<? extends OriginEntryFull> originEntryClass;
47  
48      /**
49       * A wrapper around {@link ColumnReconciliation} objects to provide it with information specific to the java beans representing
50       * each BO. <br/><br/> In the default implementation of {@link org.kuali.kfs.gl.batch.service.ReconciliationParserService}, each
51       * {@link ColumnReconciliation} object may actually represent the sum of multiple fields across all origin entries (i.e.
52       * ColumnReconciliation.getTokenizedFieldNames().length may be > 1). <br/><br/> Furthermore, the parser service returns
53       * database field names as the identifier. This service requires the use of java bean names, so this class is used to maintain a
54       * mapping between the DB names (in columnReconciliation.getTokenizedFieldNames()) and the java bean names (in
55       * javaAttributeNames). These lists/arrays are the same size, and each element at the same position in both lists are mapped to
56       * each other.
57       */
58      protected class JavaAttributeAugmentedColumnReconciliation {
59          protected ColumnReconciliation columnReconciliation;
60          protected List<String> javaAttributeNames;
61  
62          protected JavaAttributeAugmentedColumnReconciliation() {
63              columnReconciliation = null;
64              javaAttributeNames = null;
65          }
66  
67          /**
68           * Gets the columnReconciliation attribute.
69           * 
70           * @return Returns the columnReconciliation.
71           */
72          protected ColumnReconciliation getColumnReconciliation() {
73              return columnReconciliation;
74          }
75  
76          /**
77           * Sets the columnReconciliation attribute value.
78           * 
79           * @param columnReconciliation The columnReconciliation to set.
80           */
81          protected void setColumnReconciliation(ColumnReconciliation columnReconciliation) {
82              this.columnReconciliation = columnReconciliation;
83          }
84  
85          /**
86           * Sets the javaAttributeNames attribute value.
87           * 
88           * @param javaAttributeNames The javaAttributeNames to set.
89           */
90          protected void setJavaAttributeNames(List<String> javaAttributeNames) {
91              this.javaAttributeNames = javaAttributeNames;
92          }
93  
94          protected String getJavaAttributeName(int index) {
95              return javaAttributeNames.get(index);
96          }
97  
98          /**
99           * Returns the number of attributes this object is holing
100          * 
101          * @return the count of attributes this holding
102          */
103         protected int size() {
104             return javaAttributeNames.size();
105         }
106     }
107 
108     /**
109      * Performs the reconciliation on origin entries using the data from the {@link ReconciliationBlock} parameter
110      * 
111      * @param entries origin entries
112      * @param reconBlock reconciliation data
113      * @param errorMessages a non-null list onto which error messages will be appended. This list will be modified by reference.
114      * @see org.kuali.kfs.gl.batch.service.ReconciliationService#reconcile(java.util.Iterator,
115      *      org.kuali.kfs.gl.batch.service.impl.ReconciliationBlock, java.util.List)
116      */
117     public void reconcile(Iterator<LaborOriginEntry> entries, ReconciliationBlock reconBlock, List<Message> errorMessages) {
118         List<ColumnReconciliation> columns = reconBlock.getColumns();
119 
120         int numEntriesSuccessfullyLoaded = 0;
121 
122         // this value gets incremented every time the hasNext method of the iterator is called
123         int numEntriesAttemptedToLoad = 1;
124 
125         // precompute the DB -> java name mappings so that we don't have to recompute them once for each row
126         List<JavaAttributeAugmentedColumnReconciliation> javaAttributeNames = resolveJavaAttributeNames(columns);
127         KualiDecimal[] columnSums = createColumnSumsArray(columns.size());
128 
129         // because of the way the OriginEntryFileIterator works (which is likely to be the type passed in as a parameter),
130         // there are 2 primary causes of exceptions to be thrown by the Iterator.hasNext method:
131         // 
132         // - Underlying IO exception, this is a fatal error (i.e. we no longer attempt to continue parsing the file)
133         // - Misformatted origin entry line, which is not fatal (i.e. continue parsing the file and report further misformatted
134         // lines), but if it occurs, we don't want to do the final reconciliation step after this loop
135 
136         // operator short-circuiting is utilized to ensure that if there's a fatal error then we don't try to keep reading
137 
138         boolean entriesFullyIterated = false;
139 
140         // set to true if there's a problem parsing origin entry line(s)
141         boolean loadExceptionEncountered = false;
142 
143         while (!entriesFullyIterated) {
144             try {
145                 while (entries.hasNext()) {
146                     numEntriesAttemptedToLoad++;
147                     OriginEntryFull entry = entries.next();
148                     for (int c = 0; c < columns.size(); c++) {
149                         // this is for each definition of the "S" line in the reconciliation file
150                         KualiDecimal columnValue = KualiDecimal.ZERO;
151 
152                         for (int f = 0; f < javaAttributeNames.get(c).size(); f++) {
153                             String javaAttributeName = javaAttributeNames.get(c).getJavaAttributeName(f);
154                             Object fieldValue = entry.getFieldValue(javaAttributeName);
155 
156                             if (fieldValue == null) {
157                                 // what to do about nulls?
158                             }
159                             else {
160                                 if (TypeUtils.isIntegralClass(fieldValue.getClass()) || TypeUtils.isDecimalClass(fieldValue.getClass())) {
161                                     KualiDecimal castValue;
162                                     if (fieldValue instanceof KualiDecimal) {
163                                         castValue = (KualiDecimal) fieldValue;
164                                     }
165                                     else {
166                                         castValue = new KualiDecimal(fieldValue.toString());
167                                     }
168                                     columnValue = columnValue.add(castValue);
169                                 }
170                                 else {
171                                     throw new LoadException("The value for " + columns.get(c).getTokenizedFieldNames()[f] + " is not a numeric value.");
172                                 }
173                             }
174                         }
175                         columnSums[c] = columnSums[c].add(columnValue);
176                     }
177                     numEntriesSuccessfullyLoaded++;
178                 }
179             }
180             catch (LoadException e) {
181                 loadExceptionEncountered = true;
182                 LOG.error("Line " + numEntriesAttemptedToLoad + " parse error: " + e.getMessage(), e);
183                 Message newMessage = new Message("Line " + numEntriesAttemptedToLoad + " parse error: " + e.getMessage(), Message.TYPE_FATAL);
184                 errorMessages.add(newMessage);
185 
186                 numEntriesAttemptedToLoad++;
187                 continue;
188             }
189             catch (Exception e) {
190                 // entriesFullyIterated will stay false when we break out
191 
192                 // encountered a potentially serious problem, abort reading of the data
193                 LOG.error("Error encountered trying to iterate through origin entry iterator", e);
194 
195                 Message newMessage = new Message(e.getMessage(), Message.TYPE_FATAL);
196                 errorMessages.add(newMessage);
197 
198                 break;
199             }
200             entriesFullyIterated = true;
201         }
202 
203         if (entriesFullyIterated) {
204             if (loadExceptionEncountered) {
205                 // generate a message saying reconcilation check did not continue
206                 LOG.error("Reconciliation check failed because some origin entry lines could not be parsed.");
207                 Message newMessage = new Message("Reconciliation check failed because some origin entry lines could not be parsed.", Message.TYPE_FATAL);
208                 errorMessages.add(newMessage);
209             }
210             else {
211                 // see if the rowcount matches
212                 if (numEntriesSuccessfullyLoaded != reconBlock.getRowCount()) {
213                     Message newMessage = generateRowCountMismatchMessage(reconBlock, numEntriesSuccessfullyLoaded);
214                     errorMessages.add(newMessage);
215                 }
216 
217                 // now that we've computed the statistics for all of the origin entries in the iterator,
218                 // compare the actual statistics (in the columnSums array) with the stats provided in the
219                 // reconciliation file (in the "columns" List attribute reconBlock object). Both of these
220                 // array/lists should have the same size
221                 for (int i = 0; i < columns.size(); i++) {
222                     if (!columnSums[i].equals(columns.get(i).getDollarAmount())) {
223                         Message newMessage = generateColumnSumErrorMessage(columns.get(i), columnSums[i]);
224                         errorMessages.add(newMessage);
225                     }
226                 }
227             }
228         }
229     }
230 
231     /**
232      * Generates the error message for the sum of column(s) not matching the reconciliation value
233      * 
234      * @param column the column reconciliation data (recall that this "column" can be the sum of several columns)
235      * @param actualValue the value of the column(s)
236      * @return the message
237      */
238     protected Message generateColumnSumErrorMessage(ColumnReconciliation column, KualiDecimal actualValue) {
239         // TODO: if the kualiConfiguration service were to implement message params from ApplicationResources.properties, this would
240         // be ideal for that
241         StringBuilder buf = new StringBuilder();
242         buf.append("Reconciliation failed for field value(s) \"");
243         buf.append(column.getFieldName());
244         buf.append("\", expected ");
245         buf.append(column.getDollarAmount());
246         buf.append(", found value ");
247         buf.append(actualValue);
248         buf.append(".");
249 
250         Message newMessage = new Message(buf.toString(), Message.TYPE_FATAL);
251         return newMessage;
252     }
253 
254     /**
255      * Generates the error message for the number of entries reconciled being unequal to the expected value
256      * 
257      * @param block The file reconciliation data
258      * @param actualRowCount the number of rows encountered
259      * @return the message
260      */
261     protected Message generateRowCountMismatchMessage(ReconciliationBlock block, int actualRowCount) {
262         // TODO: if the kualiConfiguration service were to implement message params from ApplicationResources.properties, this would
263         // be ideal for that
264         StringBuilder buf = new StringBuilder();
265         buf.append("Reconciliation failed because an incorrect number of origin entry rows were successfully parsed.  Expected ");
266         buf.append(block.getRowCount());
267         buf.append(" row(s), parsed ");
268         buf.append(actualRowCount);
269         buf.append(" row(s).");
270 
271         Message newMessage = new Message(buf.toString(), Message.TYPE_FATAL);
272         return newMessage;
273     }
274 
275     /**
276      * Performs basic checking to ensure that values are set up so that reconciliation can proceed
277      * 
278      * @param columns the columns generated by the {@link org.kuali.kfs.gl.batch.service.ReconciliationParserService}
279      * @param javaAttributeNames the java attribute names corresponding to each field in columns. (see
280      *        {@link #resolveJavaAttributeNames(List)})
281      * @param columnSums a list of KualiDecimals used to store column sums as reconciliation iterates through the origin entries
282      * @param errorMessages a list to which error messages will be appended.
283      * @return true if there are no problems, false otherwise
284      */
285     protected boolean performSanityChecks(List<ColumnReconciliation> columns, List<JavaAttributeAugmentedColumnReconciliation> javaAttributeNames, KualiDecimal[] columnSums, List<Message> errorMessages) {
286         boolean success = true;
287 
288         if (javaAttributeNames.size() != columnSums.length || javaAttributeNames.size() != columns.size()) {
289             // sanity check
290             errorMessages.add(new Message("Reconciliation error: Sizes of lists do not match", Message.TYPE_FATAL));
291             success = false;
292         }
293         for (int i = 0; i < columns.size(); i++) {
294             if (columns.get(i).getTokenizedFieldNames().length != javaAttributeNames.get(i).size()) {
295                 errorMessages.add(new Message("Reconciliation error: Error tokenizing column elements.  The number of database fields and java fields do not match.", Message.TYPE_FATAL));
296                 success = false;
297             }
298             for (int fieldIdx = 0; fieldIdx < javaAttributeNames.get(i).size(); i++) {
299                 if (StringUtils.isBlank(javaAttributeNames.get(i).getJavaAttributeName(fieldIdx))) {
300                     errorMessages.add(new Message("Reconciliation error: javaAttributeName is blank for DB column: " + columns.get(i).getTokenizedFieldNames()[fieldIdx], Message.TYPE_FATAL));
301                     success = false;
302                 }
303             }
304         }
305         return success;
306     }
307 
308     /**
309      * Creates an array of {@link KualiDecimal}s of a given size, and initializes all elements to {@link KualiDecimal#ZERO}
310      * 
311      * @param size the size of the constructed array
312      * @return the array, all initialized to {@link KualiDecimal#ZERO}
313      */
314     protected KualiDecimal[] createColumnSumsArray(int size) {
315         KualiDecimal[] array = new KualiDecimal[size];
316         for (int i = 0; i < array.length; i++) {
317             array[i] = KualiDecimal.ZERO;
318         }
319         return array;
320     }
321 
322     /**
323      * Resolves a mapping between the database columns and the java attribute name (i.e. bean property names)
324      * 
325      * @param columns columns parsed by the {@link org.kuali.kfs.gl.batch.service.ReconciliationParserService}
326      * @return a list of {@link JavaAttributeAugmentedColumnReconciliation} (see class description) objects. The returned list will
327      *         have the same size as the parameter, and each element in one list corresponds to the element at the same position in
328      *         the other list
329      */
330     protected List<JavaAttributeAugmentedColumnReconciliation> resolveJavaAttributeNames(List<ColumnReconciliation> columns) {
331         List<JavaAttributeAugmentedColumnReconciliation> attributes = new ArrayList<JavaAttributeAugmentedColumnReconciliation>();
332         for (ColumnReconciliation column : columns) {
333             JavaAttributeAugmentedColumnReconciliation c = new JavaAttributeAugmentedColumnReconciliation();
334             c.setColumnReconciliation(column);
335             c.setJavaAttributeNames(reconciliationDao.convertDBColumnNamesToJavaName(getOriginEntryClass(), column.getTokenizedFieldNames(), true));
336             attributes.add(c);
337         }
338         return attributes;
339     }
340 
341     /**
342      * Gets the reconciliationDao attribute.
343      * 
344      * @return Returns the reconciliationDao.
345      */
346     protected ReconciliationDao getReconciliationDao() {
347         return reconciliationDao;
348     }
349 
350     /**
351      * Sets the reconciliationDao attribute value.
352      * 
353      * @param reconciliationDao The reconciliationDao to set.
354      */
355     public void setReconciliationDao(ReconciliationDao reconciliationDao) {
356         this.reconciliationDao = reconciliationDao;
357     }
358 
359     /**
360      * Gets the originEntryClass attribute.
361      * 
362      * @return Returns the originEntryClass.
363      */
364     protected Class<? extends OriginEntryFull> getOriginEntryClass() {
365         return originEntryClass;
366     }
367 
368     /**
369      * Sets the originEntryClass attribute value.
370      * 
371      * @param originEntryClass The originEntryClass to set.
372      */
373     public void setOriginEntryClass(Class<? extends OriginEntryFull> originEntryClass) {
374         this.originEntryClass = originEntryClass;
375     }
376 }