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 }