001/*
002 * Copyright 2007 The Kuali Foundation
003 * 
004 * Licensed under the Educational Community License, Version 2.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/ecl2.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 */
016package org.kuali.ole.gl.batch.service.impl;
017
018import java.util.ArrayList;
019import java.util.Iterator;
020import java.util.List;
021
022import org.apache.commons.lang.StringUtils;
023import org.kuali.ole.gl.batch.dataaccess.ReconciliationDao;
024import org.kuali.ole.gl.batch.service.ReconciliationService;
025import org.kuali.ole.gl.businessobject.OriginEntryFull;
026import org.kuali.ole.gl.exception.LoadException;
027import org.kuali.ole.sys.Message;
028import org.kuali.rice.core.api.util.type.KualiDecimal;
029import org.kuali.rice.core.api.util.type.TypeUtils;
030import org.springframework.transaction.annotation.Transactional;
031
032/**
033 * Default implementation of ReconciliationService
034 */
035@Transactional
036public class ReconciliationServiceImpl implements ReconciliationService {
037    private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReconciliationServiceImpl.class);
038
039    private ReconciliationDao reconciliationDao;
040    private Class<? extends OriginEntryFull> originEntryClass;
041
042    /**
043     * A wrapper around {@link ColumnReconciliation} objects to provide it with information specific to the java beans representing
044     * each BO. <br/><br/> In the default implementation of {@link org.kuali.ole.gl.batch.service.ReconciliationParserService}, each
045     * {@link ColumnReconciliation} object may actually represent the sum of multiple fields across all origin entries (i.e.
046     * ColumnReconciliation.getTokenizedFieldNames().length may be > 1). <br/><br/> Furthermore, the parser service returns
047     * database field names as the identifier. This service requires the use of java bean names, so this class is used to maintain a
048     * mapping between the DB names (in columnReconciliation.getTokenizedFieldNames()) and the java bean names (in
049     * javaAttributeNames). These lists/arrays are the same size, and each element at the same position in both lists are mapped to
050     * each other.
051     */
052    protected class JavaAttributeAugmentedColumnReconciliation {
053        protected ColumnReconciliation columnReconciliation;
054        protected List<String> javaAttributeNames;
055
056        protected JavaAttributeAugmentedColumnReconciliation() {
057            columnReconciliation = null;
058            javaAttributeNames = null;
059        }
060
061        /**
062         * Gets the columnReconciliation attribute.
063         * 
064         * @return Returns the columnReconciliation.
065         */
066        protected ColumnReconciliation getColumnReconciliation() {
067            return columnReconciliation;
068        }
069
070        /**
071         * Sets the columnReconciliation attribute value.
072         * 
073         * @param columnReconciliation The columnReconciliation to set.
074         */
075        protected void setColumnReconciliation(ColumnReconciliation columnReconciliation) {
076            this.columnReconciliation = columnReconciliation;
077        }
078
079        /**
080         * Sets the javaAttributeNames attribute value.
081         * 
082         * @param javaAttributeNames The javaAttributeNames to set.
083         */
084        protected void setJavaAttributeNames(List<String> javaAttributeNames) {
085            this.javaAttributeNames = javaAttributeNames;
086        }
087
088        protected String getJavaAttributeName(int index) {
089            return javaAttributeNames.get(index);
090        }
091
092        /**
093         * Returns the number of attributes this object is holing
094         * 
095         * @return the count of attributes this holding
096         */
097        protected int size() {
098            return javaAttributeNames.size();
099        }
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}