001    /*
002     * Copyright 2011 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     */
016    package org.kuali.kfs.gl.document;
017    
018    import java.math.BigDecimal;
019    import java.text.SimpleDateFormat;
020    import java.util.Collection;
021    import java.util.Date;
022    import java.util.List;
023    
024    import org.apache.commons.lang.StringUtils;
025    import org.kuali.kfs.gl.businessobject.CorrectionChange;
026    import org.kuali.kfs.gl.businessobject.CorrectionChangeGroup;
027    import org.kuali.kfs.gl.businessobject.CorrectionCriteria;
028    import org.kuali.kfs.gl.businessobject.OriginEntryFull;
029    import org.kuali.kfs.gl.businessobject.OriginEntryStatistics;
030    import org.kuali.kfs.gl.businessobject.options.OriginEntryFieldFinder;
031    import org.kuali.kfs.sys.KFSConstants;
032    import org.kuali.kfs.sys.context.SpringContext;
033    import org.kuali.rice.kns.service.ParameterService;
034    import org.kuali.rice.kns.util.KualiDecimal;
035    
036    /**
037     * This class provides utility methods for the correction document
038     */
039    public class CorrectionDocumentUtils {
040        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CorrectionDocumentUtils.class);
041        public static final int DEFAULT_RECORD_COUNT_FUNCTIONALITY_LIMIT = 1000;
042    
043        /**
044         * The GLCP document will always be on restricted functionality mode, regardless of input group size
045         */
046        public static final int RECORD_COUNT_FUNCTIONALITY_LIMIT_IS_NONE = 0;
047    
048        /**
049         * The GLCP document will never be on restricted functionality mode, regardless of input group size
050         */
051        public static final int RECORD_COUNT_FUNCTIONALITY_LIMIT_IS_UNLIMITED = -1;
052    
053        public static final int DEFAULT_RECORDS_PER_PAGE = 10;
054    
055        /**
056         * This method returns the limit for record count functionality
057         * 
058         * @return limit for record count functionality
059         */
060        public static int getRecordCountFunctionalityLimit() {
061            String limitString = SpringContext.getBean(ParameterService.class).getParameterValue(GeneralLedgerCorrectionProcessDocument.class, KFSConstants.GeneralLedgerCorrectionProcessApplicationParameterKeys.RECORD_COUNT_FUNCTIONALITY_LIMIT);
062            if (limitString != null) {
063                return Integer.valueOf(limitString);
064            }
065    
066            return DEFAULT_RECORD_COUNT_FUNCTIONALITY_LIMIT;
067        }
068    
069        /**
070         * This method returns the number of records per page
071         * 
072         * @return number of records per page
073         */
074        public static int getRecordsPerPage() {
075            String limitString = SpringContext.getBean(ParameterService.class).getParameterValue(GeneralLedgerCorrectionProcessDocument.class, KFSConstants.GeneralLedgerCorrectionProcessApplicationParameterKeys.RECORDS_PER_PAGE);
076            if (limitString != null) {
077                return Integer.valueOf(limitString);
078            }
079            return DEFAULT_RECORDS_PER_PAGE;
080        }
081    
082        /**
083         * This method returns true if input group size is greater than or equal to record count functionality limit
084         * 
085         * @param inputGroupSize size of input groups
086         * @param recordCountFunctionalityLimit limit for record count functionality
087         * @return true if input group size is greater than or equal to record count functionality limit
088         */
089        public static boolean isRestrictedFunctionalityMode(int inputGroupSize, int recordCountFunctionalityLimit) {
090            return (recordCountFunctionalityLimit != CorrectionDocumentUtils.RECORD_COUNT_FUNCTIONALITY_LIMIT_IS_UNLIMITED && inputGroupSize >= recordCountFunctionalityLimit) || recordCountFunctionalityLimit == CorrectionDocumentUtils.RECORD_COUNT_FUNCTIONALITY_LIMIT_IS_NONE;
091        }
092    
093        /**
094         * When a correction criterion is about to be added to a group, this will check if it is valid, meaning that the field name is
095         * not blank
096         * 
097         * @param correctionCriteria validated correction criteria
098         * @return true if correction criteria is valid for adding
099         */
100        public static boolean validCorrectionCriteriaForAdding(CorrectionCriteria correctionCriteria) {
101            String fieldName = correctionCriteria.getCorrectionFieldName();
102            if (StringUtils.isBlank(fieldName)) {
103                return false;
104            }
105            return true;
106        }
107    
108        /**
109         * When a document is about to be saved, this will check if it is valid, meaning that the field name and value are both blank
110         * 
111         * @param correctionCriteria validated correction criteria
112         * @return true if correction criteria is valid for saving
113         */
114        public static boolean validCorrectionCriteriaForSaving(CorrectionCriteria correctionCriteria) {
115            return correctionCriteria == null || (StringUtils.isBlank(correctionCriteria.getCorrectionFieldName()) && StringUtils.isBlank(correctionCriteria.getCorrectionFieldValue()));
116        }
117    
118        /**
119         * When a correction change is about to be added to a group, this will check if it is valid, meaning that the field name is not
120         * blank
121         * 
122         * @param correctionChange validated correction change
123         * @return true is correction change is valid for adding
124         */
125        public static boolean validCorrectionChangeForAdding(CorrectionChange correctionChange) {
126            String fieldName = correctionChange.getCorrectionFieldName();
127            if (StringUtils.isBlank(fieldName)) {
128                return false;
129            }
130            return true;
131        }
132    
133        /**
134         * When a document is about to be saved, this will check if it is valid, meaning that the field name and value are both blank
135         * 
136         * @param correctionCriteria validated correction criteria
137         * @return true if correction change is valid for saving (i.e. correction change is null or correction field name and field
138         *         value are blank)
139         */
140        public static boolean validCorrectionChangeForSaving(CorrectionChange correctionChange) {
141            return correctionChange == null || (StringUtils.isBlank(correctionChange.getCorrectionFieldName()) && StringUtils.isBlank(correctionChange.getCorrectionFieldValue()));
142        }
143    
144        /**
145         * Sets all origin entries' entry IDs to null within the collection.
146         * 
147         * @param originEntries collection of origin entries
148         */
149        public static void setAllEntryIdsToNull(Collection<OriginEntryFull> originEntries) {
150            for (OriginEntryFull entry : originEntries) {
151                entry.setEntryId(null);
152            }
153        }
154    
155        /**
156         * Sets all origin entries' entry IDs to be sequential starting from 0 in the collection
157         * 
158         * @param originEntries collection of origin entries
159         */
160        public static void setSequentialEntryIds(Collection<OriginEntryFull> originEntries) {
161            int index = 0;
162            for (OriginEntryFull entry : originEntries) {
163                entry.setEntryId(new Integer(index));
164                index++;
165            }
166        }
167    
168        /**
169         * Returns whether an origin entry matches the passed in criteria. If both the criteria and actual value are both String types
170         * and are empty, null, or whitespace only, then they will match.
171         * 
172         * @param cc correction criteria to test against origin entry
173         * @param oe origin entry to test
174         * @return true if origin entry matches the passed in criteria
175         */
176        public static boolean entryMatchesCriteria(CorrectionCriteria cc, OriginEntryFull oe) {
177            OriginEntryFieldFinder oeff = new OriginEntryFieldFinder();
178            Object fieldActualValue = oe.getFieldValue(cc.getCorrectionFieldName());
179            String fieldTestValue = StringUtils.isBlank(cc.getCorrectionFieldValue()) ? "" : cc.getCorrectionFieldValue();
180            String fieldType = oeff.getFieldType(cc.getCorrectionFieldName());
181    
182            String fieldActualValueString = convertToString(fieldActualValue, fieldType);
183    
184            if ("String".equals(fieldType) || "sw".equals(cc.getCorrectionOperatorCode()) || "ew".equals(cc.getCorrectionOperatorCode()) || "ct".equals(cc.getCorrectionOperatorCode())) {
185                return compareStringData(cc, fieldTestValue, fieldActualValueString);
186            }
187            int compareTo = 0;
188            try {
189                if (fieldActualValue == null) {
190                    return false;
191                }
192                if ("Integer".equals(fieldType)) {
193                    compareTo = ((Integer) fieldActualValue).compareTo(Integer.parseInt(fieldTestValue));
194                }
195                if ("KualiDecimal".equals(fieldType)) {
196                    compareTo = ((KualiDecimal) fieldActualValue).compareTo(new KualiDecimal(Double.parseDouble(fieldTestValue)));
197                }
198                if ("BigDecimal".equals(fieldType)) {
199                    compareTo = ((BigDecimal) fieldActualValue).compareTo(new BigDecimal(Double.parseDouble(fieldTestValue)));
200    
201                }
202                if ("Date".equals(fieldType)) {
203                    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
204                    compareTo = ((Date) fieldActualValue).compareTo(df.parse(fieldTestValue));
205                }
206            }
207            catch (Exception e) {
208                // any exception while parsing data return false
209                return false;
210            }
211            return compareTo(compareTo, cc.getCorrectionOperatorCode());
212        }
213    
214    
215        /**
216         * Compares string data
217         * 
218         * @param cc criteria
219         * @param fieldTestValue test value
220         * @param fieldActualValueString actual value
221         * @return flag true if matches with criteria
222         */
223        public static boolean compareStringData(CorrectionCriteria cc, String fieldTestValue, String fieldActualValueString) {
224            if ("eq".equals(cc.getCorrectionOperatorCode())) {
225                return fieldActualValueString.equals(fieldTestValue);
226            }
227            else if ("ne".equals(cc.getCorrectionOperatorCode())) {
228                return (!fieldActualValueString.equals(fieldTestValue));
229            }
230            else if ("sw".equals(cc.getCorrectionOperatorCode())) {
231                return fieldActualValueString.startsWith(fieldTestValue);
232            }
233            else if ("ew".equals(cc.getCorrectionOperatorCode())) {
234                return fieldActualValueString.endsWith(fieldTestValue);
235            }
236            else if ("ct".equals(cc.getCorrectionOperatorCode())) {
237                return (fieldActualValueString.indexOf(fieldTestValue) > -1);
238            }
239            else if ("lt".equals(cc.getCorrectionOperatorCode())) {
240                return (fieldActualValueString.compareTo(fieldTestValue) < 0);
241            }
242            else if ("le".equals(cc.getCorrectionOperatorCode())) {
243                return (fieldActualValueString.compareTo(fieldTestValue) <= 0);
244            }
245            else if ("gt".equals(cc.getCorrectionOperatorCode())) {
246                return (fieldActualValueString.compareTo(fieldTestValue) > 0);
247            }
248            else if ("ge".equals(cc.getCorrectionOperatorCode())) {
249                return (fieldActualValueString.compareTo(fieldTestValue) >= 0);
250            }
251            throw new IllegalArgumentException("Unknown operator: " + cc.getCorrectionOperatorCode());
252        }
253    
254        /**
255         * Returns true is compared indicator matches
256         * 
257         * @param compareTo
258         * @param operatorCode
259         * @return
260         */
261        public static boolean compareTo(int compareTo, String operatorCode) {
262            if ("eq".equals(operatorCode)) {
263                return (compareTo == 0);
264            }
265            else if ("ne".equals(operatorCode)) {
266                return (compareTo != 0);
267            }
268            else if ("lt".equals(operatorCode)) {
269                return (compareTo < 0);
270            }
271            else if ("le".equals(operatorCode)) {
272                return (compareTo <= 0);
273            }
274            else if ("gt".equals(operatorCode)) {
275                return (compareTo > 0);
276            }
277            else if ("ge".equals(operatorCode)) {
278                return (compareTo >= 0);
279            }
280            throw new IllegalArgumentException("Unknown operator: " + operatorCode);
281        }
282    
283        /**
284         * Converts the value into a string, with the appropriate formatting
285         * 
286         * @param fieldActualValue actual field value
287         * @param fieldType field type (i.e. "String", "Integer", "Date")
288         * @return String object value as a string
289         */
290        public static String convertToString(Object fieldActualValue, String fieldType) {
291            if (fieldActualValue == null) {
292                return "";
293            }
294            if ("String".equals(fieldType)) {
295                return (String) fieldActualValue;
296            }
297            else if ("Integer".equals(fieldType)) {
298                Integer i = (Integer) fieldActualValue;
299                return i.toString();
300            }
301            else if ("KualiDecimal".equals(fieldType)) {
302                KualiDecimal kd = (KualiDecimal) fieldActualValue;
303                return kd.toString();
304            }
305            else if ("BigDecimal".equals(fieldType)) {
306                BigDecimal bd = (BigDecimal) fieldActualValue;
307                return bd.toString();
308            }
309            else if ("Date".equals(fieldType)) {
310                Date d = (Date) fieldActualValue;
311                SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
312                return df.format(d);
313            }
314            return "";
315        }
316    
317        /**
318         * Applies a list of change criteria groups to an origin entry. Note that the returned value, if not null, is a reference to the
319         * same instance as the origin entry passed in (i.e. intentional side effect)
320         * 
321         * @param entry origin entry
322         * @param matchCriteriaOnly if true and no criteria match, then this method will return null
323         * @param changeCriteriaGroups list of change criteria groups to apply
324         * @return the passed in entry instance, or null (see above)
325         */
326        public static OriginEntryFull applyCriteriaToEntry(OriginEntryFull entry, boolean matchCriteriaOnly, List<CorrectionChangeGroup> changeCriteriaGroups) {
327            if (matchCriteriaOnly && !doesEntryMatchAnyCriteriaGroups(entry, changeCriteriaGroups)) {
328                return null;
329            }
330    
331            for (CorrectionChangeGroup ccg : changeCriteriaGroups) {
332                int matches = 0;
333                for (CorrectionCriteria cc : ccg.getCorrectionCriteria()) {
334                    if (entryMatchesCriteria(cc, entry)) {
335                        matches++;
336                    }
337                }
338    
339                // If they all match, change it
340                if (matches == ccg.getCorrectionCriteria().size()) {
341                    for (CorrectionChange change : ccg.getCorrectionChange()) {
342                        // Change the row
343                        entry.setFieldValue(change.getCorrectionFieldName(), change.getCorrectionFieldValue());
344                    }
345                }
346            }
347            return entry;
348        }
349    
350        /**
351         * Returns whether the entry matches any of the criteria groups
352         * 
353         * @param entry origin entry
354         * @param groups collection of correction change group
355         * @return true if origin entry matches any of the criteria groups
356         */
357        public static boolean doesEntryMatchAnyCriteriaGroups(OriginEntryFull entry, Collection<CorrectionChangeGroup> groups) {
358            boolean anyGroupMatch = false;
359            for (CorrectionChangeGroup ccg : groups) {
360                int matches = 0;
361                for (CorrectionCriteria cc : ccg.getCorrectionCriteria()) {
362                    if (CorrectionDocumentUtils.entryMatchesCriteria(cc, entry)) {
363                        matches++;
364                    }
365                }
366    
367                // If they all match, change it
368                if (matches == ccg.getCorrectionCriteria().size()) {
369                    anyGroupMatch = true;
370                    break;
371                }
372            }
373            return anyGroupMatch;
374        }
375    
376        /**
377         * Computes the statistics (credit amount, debit amount, row count) of a collection of origin entries.
378         * 
379         * @param entries list of orgin entry entries
380         * @return {@link OriginEntryStatistics} statistics (credit amount, debit amount, row count) of a collection of origin entries.
381         */
382        public static OriginEntryStatistics getStatistics(Collection<OriginEntryFull> entries) {
383            OriginEntryStatistics oes = new OriginEntryStatistics();
384    
385            for (OriginEntryFull oe : entries) {
386                updateStatisticsWithEntry(oe, oes);
387            }
388            return oes;
389        }
390    
391        /**
392         * Returns whether the origin entry represents a debit
393         * 
394         * @param oe origin entry
395         * @return true if origin entry represents a debit
396         */
397        public static boolean isDebit(OriginEntryFull oe) {
398            return (KFSConstants.GL_DEBIT_CODE.equals(oe.getTransactionDebitCreditCode()));
399        }
400    
401        /**
402         * Returns whether the origin entry represents a budget
403         * 
404         * @param oe origin entry
405         * @return true if origin entry represents a budget
406         */
407        public static boolean isBudget(OriginEntryFull oe) {
408            return KFSConstants.GL_BUDGET_CODE.equals(oe.getTransactionDebitCreditCode());
409        }
410    
411        /**
412         * Returns whether the origin entry represents a credit
413         * 
414         * @param oe origin entry
415         * @return true if origin entry represents a credit
416         */
417        public static boolean isCredit(OriginEntryFull oe) {
418            return KFSConstants.GL_CREDIT_CODE.equals(oe.getTransactionDebitCreditCode());
419        }
420    
421        /**
422         * Given an instance of statistics, it adds information from the passed in entry to the statistics
423         * 
424         * @param entry origin entry
425         * @param statistics adds statistics from the passed in origin entry to the passed in statistics
426         */
427        public static void updateStatisticsWithEntry(OriginEntryFull entry, OriginEntryStatistics statistics) {
428            statistics.incrementCount();
429            if (isDebit(entry)) {
430                statistics.addDebit(entry.getTransactionLedgerEntryAmount());
431            }
432            else if (isCredit(entry)) {
433                statistics.addCredit(entry.getTransactionLedgerEntryAmount());
434            }
435            else {
436                statistics.addBudget(entry.getTransactionLedgerEntryAmount());
437            }
438        }
439    
440        /**
441         * Sets document with the statistics data
442         * 
443         * @param statistics origin entry statistics that are being used to set document
444         * @param document document with statistic information being set
445         */
446        public static void copyStatisticsToDocument(OriginEntryStatistics statistics, GeneralLedgerCorrectionProcessDocument document) {
447            document.setCorrectionCreditTotalAmount(statistics.getCreditTotalAmount());
448            document.setCorrectionDebitTotalAmount(statistics.getDebitTotalAmount());
449            document.setCorrectionBudgetTotalAmount(statistics.getBudgetTotalAmount());
450            document.setCorrectionRowCount(statistics.getRowCount());
451        }
452    }