001: /*
002: * Copyright 2007 The Kuali Foundation.
003: *
004: * Licensed under the Educational Community License, Version 1.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/ecl1.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.module.gl.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.core.util.KualiDecimal;
024: import org.kuali.core.util.TypeUtils;
025: import org.kuali.module.gl.bo.OriginEntryFull;
026: import org.kuali.module.gl.dao.ReconciliationDao;
027: import org.kuali.module.gl.exception.LoadException;
028: import org.kuali.module.gl.service.ReconciliationService;
029: import org.kuali.module.gl.util.ColumnReconciliation;
030: import org.kuali.module.gl.util.Message;
031: import org.kuali.module.gl.util.ReconciliationBlock;
032: import org.springframework.transaction.annotation.Transactional;
033:
034: /**
035: * Default implementation of ReconciliationService
036: */
037: @Transactional
038: public class ReconciliationServiceImpl implements ReconciliationService {
039: private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger
040: .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.module.gl.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(
079: ColumnReconciliation columnReconciliation) {
080: this .columnReconciliation = columnReconciliation;
081: }
082:
083: /**
084: * Sets the javaAttributeNames attribute value.
085: *
086: * @param javaAttributeNames The javaAttributeNames to set.
087: */
088: protected void setJavaAttributeNames(
089: List<String> javaAttributeNames) {
090: this .javaAttributeNames = javaAttributeNames;
091: }
092:
093: protected String getJavaAttributeName(int index) {
094: return javaAttributeNames.get(index);
095: }
096:
097: /**
098: * Returns the number of attributes this object is holing
099: *
100: * @return the count of attributes this holding
101: */
102: protected int size() {
103: return javaAttributeNames.size();
104: }
105: }
106:
107: /**
108: * Performs the reconciliation on origin entries using the data from the {@link ReconciliationBlock} parameter
109: *
110: * @param entries origin entries
111: * @param reconBlock reconciliation data
112: * @param errorMessages a non-null list onto which error messages will be appended. This list will be modified by reference.
113: * @see org.kuali.module.gl.service.ReconciliationService#reconcile(java.util.Iterator,
114: * org.kuali.module.gl.util.ReconciliationBlock, java.util.List)
115: */
116: public void reconcile(Iterator<OriginEntryFull> entries,
117: 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
128: .size());
129:
130: // because of the way the OriginEntryFileIterator works (which is likely to be the type passed in as a parameter),
131: // there are 2 primary causes of exceptions to be thrown by the Iterator.hasNext method:
132: //
133: // - Underlying IO exception, this is a fatal error (i.e. we no longer attempt to continue parsing the file)
134: // - Misformatted origin entry line, which is not fatal (i.e. continue parsing the file and report further misformatted
135: // lines), but if it occurs, we don't want to do the final reconciliation step after this loop
136:
137: // operator short-circuiting is utilized to ensure that if there's a fatal error then we don't try to keep reading
138:
139: boolean entriesFullyIterated = false;
140:
141: // set to true if there's a problem parsing origin entry line(s)
142: boolean loadExceptionEncountered = false;
143:
144: while (!entriesFullyIterated) {
145: try {
146: while (entries.hasNext()) {
147: numEntriesAttemptedToLoad++;
148: OriginEntryFull entry = entries.next();
149: for (int c = 0; c < columns.size(); c++) {
150: // this is for each definition of the "S" line in the reconciliation file
151: KualiDecimal columnValue = KualiDecimal.ZERO;
152:
153: for (int f = 0; f < javaAttributeNames.get(c)
154: .size(); f++) {
155: String javaAttributeName = javaAttributeNames
156: .get(c).getJavaAttributeName(f);
157: Object fieldValue = entry
158: .getFieldValue(javaAttributeName);
159:
160: if (fieldValue == null) {
161: // what to do about nulls?
162: } else {
163: if (TypeUtils
164: .isIntegralClass(fieldValue
165: .getClass())
166: || TypeUtils
167: .isDecimalClass(fieldValue
168: .getClass())) {
169: KualiDecimal castValue;
170: if (fieldValue instanceof KualiDecimal) {
171: castValue = (KualiDecimal) fieldValue;
172: } else {
173: castValue = new KualiDecimal(
174: fieldValue.toString());
175: }
176: columnValue = columnValue
177: .add(castValue);
178: } else {
179: throw new LoadException(
180: "The value for "
181: + columns
182: .get(c)
183: .getTokenizedFieldNames()[f]
184: + " is not a numeric value.");
185: }
186: }
187: }
188: columnSums[c] = columnSums[c].add(columnValue);
189: }
190: numEntriesSuccessfullyLoaded++;
191: }
192: } catch (LoadException e) {
193: loadExceptionEncountered = true;
194:
195: Message newMessage = new Message("Line "
196: + numEntriesAttemptedToLoad + " parse error: "
197: + e.getMessage(), Message.TYPE_FATAL);
198: errorMessages.add(newMessage);
199:
200: numEntriesAttemptedToLoad++;
201: continue;
202: } catch (Exception e) {
203: // entriesFullyIterated will stay false when we break out
204:
205: // encountered a potentially serious problem, abort reading of the data
206: LOG
207: .error(
208: "Error encountered trying to iterate through origin entry iterator",
209: e);
210:
211: Message newMessage = new Message(e.getMessage(),
212: Message.TYPE_FATAL);
213: errorMessages.add(newMessage);
214:
215: break;
216: }
217: entriesFullyIterated = true;
218: }
219:
220: if (entriesFullyIterated) {
221: if (loadExceptionEncountered) {
222: // generate a message saying reconcilation check did not continue
223: Message newMessage = new Message(
224: "Reconciliation check failed because some origin entry lines could not be parsed.",
225: Message.TYPE_FATAL);
226: errorMessages.add(newMessage);
227: } else {
228: // see if the rowcount matches
229: if (numEntriesSuccessfullyLoaded != reconBlock
230: .getRowCount()) {
231: Message newMessage = generateRowCountMismatchMessage(
232: reconBlock, numEntriesSuccessfullyLoaded);
233: errorMessages.add(newMessage);
234: }
235:
236: // now that we've computed the statistics for all of the origin entries in the iterator,
237: // compare the actual statistics (in the columnSums array) with the stats provided in the
238: // reconciliation file (in the "columns" List attribute reconBlock object). Both of these
239: // array/lists should have the same size
240: for (int i = 0; i < columns.size(); i++) {
241: if (!columnSums[i].equals(columns.get(i)
242: .getDollarAmount())) {
243: Message newMessage = generateColumnSumErrorMessage(
244: columns.get(i), columnSums[i]);
245: errorMessages.add(newMessage);
246: }
247: }
248: }
249: }
250: }
251:
252: /**
253: * Generates the error message for the sum of column(s) not matching the reconciliation value
254: *
255: * @param column the column reconciliation data (recall that this "column" can be the sum of several columns)
256: * @param actualValue the value of the column(s)
257: * @return the message
258: */
259: protected Message generateColumnSumErrorMessage(
260: ColumnReconciliation column, KualiDecimal actualValue) {
261: // TODO: if the kualiConfiguration service were to implement message params from ApplicationResources.properties, this would
262: // be ideal for that
263: StringBuilder buf = new StringBuilder();
264: buf.append("Reconciliation failed for field value(s) \"");
265: buf.append(column.getFieldName());
266: buf.append("\", expected ");
267: buf.append(column.getDollarAmount());
268: buf.append(", found value ");
269: buf.append(actualValue);
270: buf.append(".");
271:
272: Message newMessage = new Message(buf.toString(),
273: Message.TYPE_FATAL);
274: return newMessage;
275: }
276:
277: /**
278: * Generates the error message for the number of entries reconciled being unequal to the expected value
279: *
280: * @param block The file reconciliation data
281: * @param actualRowCount the number of rows encountered
282: * @return the message
283: */
284: protected Message generateRowCountMismatchMessage(
285: ReconciliationBlock block, int actualRowCount) {
286: // TODO: if the kualiConfiguration service were to implement message params from ApplicationResources.properties, this would
287: // be ideal for that
288: StringBuilder buf = new StringBuilder();
289: buf
290: .append("Reconciliation failed because an incorrect number of origin entry rows were successfully parsed. Expected ");
291: buf.append(block.getRowCount());
292: buf.append(" row(s), parsed ");
293: buf.append(actualRowCount);
294: buf.append(" row(s).");
295:
296: Message newMessage = new Message(buf.toString(),
297: Message.TYPE_FATAL);
298: return newMessage;
299: }
300:
301: /**
302: * Performs basic checking to ensure that values are set up so that reconciliation can proceed
303: *
304: * @param columns the columns generated by the {@link org.kuali.module.gl.service.ReconciliationParserService}
305: * @param javaAttributeNames the java attribute names corresponding to each field in columns. (see
306: * {@link #resolveJavaAttributeNames(List)})
307: * @param columnSums a list of KualiDecimals used to store column sums as reconciliation iterates through the origin entries
308: * @param errorMessages a list to which error messages will be appended.
309: * @return true if there are no problems, false otherwise
310: */
311: private boolean performSanityChecks(
312: List<ColumnReconciliation> columns,
313: List<JavaAttributeAugmentedColumnReconciliation> javaAttributeNames,
314: KualiDecimal[] columnSums, List<Message> errorMessages) {
315: boolean success = true;
316:
317: if (javaAttributeNames.size() != columnSums.length
318: || javaAttributeNames.size() != columns.size()) {
319: // sanity check
320: errorMessages
321: .add(new Message(
322: "Reconciliation error: Sizes of lists do not match",
323: Message.TYPE_FATAL));
324: success = false;
325: }
326: for (int i = 0; i < columns.size(); i++) {
327: if (columns.get(i).getTokenizedFieldNames().length != javaAttributeNames
328: .get(i).size()) {
329: errorMessages
330: .add(new Message(
331: "Reconciliation error: Error tokenizing column elements. The number of database fields and java fields do not match.",
332: Message.TYPE_FATAL));
333: success = false;
334: }
335: for (int fieldIdx = 0; fieldIdx < javaAttributeNames.get(i)
336: .size(); i++) {
337: if (StringUtils.isBlank(javaAttributeNames.get(i)
338: .getJavaAttributeName(fieldIdx))) {
339: errorMessages
340: .add(new Message(
341: "Reconciliation error: javaAttributeName is blank for DB column: "
342: + columns
343: .get(i)
344: .getTokenizedFieldNames()[fieldIdx],
345: Message.TYPE_FATAL));
346: success = false;
347: }
348: }
349: }
350: return success;
351: }
352:
353: /**
354: * Creates an array of {@link KualiDecimal}s of a given size, and initializes all elements to {@link KualiDecimal#ZERO}
355: *
356: * @param size the size of the constructed array
357: * @return the array, all initialized to {@link KualiDecimal#ZERO}
358: */
359: private KualiDecimal[] createColumnSumsArray(int size) {
360: KualiDecimal[] array = new KualiDecimal[size];
361: for (int i = 0; i < array.length; i++) {
362: array[i] = KualiDecimal.ZERO;
363: }
364: return array;
365: }
366:
367: /**
368: * Resolves a mapping between the database columns and the java attribute name (i.e. bean property names)
369: *
370: * @param columns columns parsed by the {@link org.kuali.module.gl.service.ReconciliationParserService}
371: * @return a list of {@link JavaAttributeAugmentedColumnReconciliation} (see class description) objects. The returned list will
372: * have the same size as the parameter, and each element in one list corresponds to the element at the same position in
373: * the other list
374: */
375: private List<JavaAttributeAugmentedColumnReconciliation> resolveJavaAttributeNames(
376: List<ColumnReconciliation> columns) {
377: List<JavaAttributeAugmentedColumnReconciliation> attributes = new ArrayList<JavaAttributeAugmentedColumnReconciliation>();
378: for (ColumnReconciliation column : columns) {
379: JavaAttributeAugmentedColumnReconciliation c = new JavaAttributeAugmentedColumnReconciliation();
380: c.setColumnReconciliation(column);
381: c.setJavaAttributeNames(reconciliationDao
382: .convertDBColumnNamesToJavaName(
383: getOriginEntryClass(), column
384: .getTokenizedFieldNames(), true));
385: attributes.add(c);
386: }
387: return attributes;
388: }
389:
390: /**
391: * Gets the reconciliationDao attribute.
392: *
393: * @return Returns the reconciliationDao.
394: */
395: protected ReconciliationDao getReconciliationDao() {
396: return reconciliationDao;
397: }
398:
399: /**
400: * Sets the reconciliationDao attribute value.
401: *
402: * @param reconciliationDao The reconciliationDao to set.
403: */
404: public void setReconciliationDao(ReconciliationDao reconciliationDao) {
405: this .reconciliationDao = reconciliationDao;
406: }
407:
408: /**
409: * Gets the originEntryClass attribute.
410: *
411: * @return Returns the originEntryClass.
412: */
413: protected Class<? extends OriginEntryFull> getOriginEntryClass() {
414: return originEntryClass;
415: }
416:
417: /**
418: * Sets the originEntryClass attribute value.
419: *
420: * @param originEntryClass The originEntryClass to set.
421: */
422: public void setOriginEntryClass(
423: Class<? extends OriginEntryFull> originEntryClass) {
424: this.originEntryClass = originEntryClass;
425: }
426: }
|