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 }