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.module.endow.document;
017    
018    import static org.kuali.kfs.module.endow.EndowKeyConstants.EndowmentAccountingLineParser.ERROR_INVALID_FILE_FORMAT;
019    import static org.kuali.kfs.module.endow.EndowKeyConstants.EndowmentAccountingLineParser.ERROR_INVALID_PROPERTY_VALUE;
020    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_ACCT_NBR;
021    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_AMOUNT;
022    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_CHART_CD;
023    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_NBR;
024    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_OBJECT_CD;
025    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_ORG_REF_ID;
026    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_PROJECT_CD;
027    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_SUBACCT_NBR;
028    import static org.kuali.kfs.module.endow.EndowPropertyConstants.ENDOWMENT_ACCOUNTING_LINE_SUBOBJ_CD;
029    
030    import java.io.BufferedReader;
031    import java.io.IOException;
032    import java.io.InputStream;
033    import java.io.InputStreamReader;
034    import java.lang.reflect.InvocationTargetException;
035    import java.util.ArrayList;
036    import java.util.HashMap;
037    import java.util.List;
038    import java.util.Map;
039    import java.util.Map.Entry;
040    
041    import org.apache.commons.lang.StringUtils;
042    import org.kuali.kfs.module.endow.EndowConstants;
043    import org.kuali.kfs.module.endow.EndowKeyConstants;
044    import org.kuali.kfs.module.endow.EndowPropertyConstants;
045    import org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLine;
046    import org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser;
047    import org.kuali.kfs.module.endow.businessobject.SourceEndowmentAccountingLine;
048    import org.kuali.kfs.module.endow.businessobject.TargetEndowmentAccountingLine;
049    import org.kuali.kfs.sys.KFSPropertyConstants;
050    import org.kuali.kfs.sys.context.SpringContext;
051    import org.kuali.kfs.sys.exception.AccountingLineParserException;
052    import org.kuali.rice.kns.exception.InfrastructureException;
053    import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
054    import org.kuali.rice.kns.service.DataDictionaryService;
055    import org.kuali.rice.kns.util.GlobalVariables;
056    import org.kuali.rice.kns.util.ObjectUtils;
057    import org.kuali.rice.kns.web.format.FormatException;
058    
059    public class EndowmentAccountingLineParserBase implements EndowmentAccountingLineParser {
060    
061        protected static final String[] DEFAULT_FORMAT = { ENDOWMENT_ACCOUNTING_LINE_CHART_CD, ENDOWMENT_ACCOUNTING_LINE_ACCT_NBR, ENDOWMENT_ACCOUNTING_LINE_SUBACCT_NBR, ENDOWMENT_ACCOUNTING_LINE_OBJECT_CD, ENDOWMENT_ACCOUNTING_LINE_SUBOBJ_CD, ENDOWMENT_ACCOUNTING_LINE_PROJECT_CD, ENDOWMENT_ACCOUNTING_LINE_ORG_REF_ID, ENDOWMENT_ACCOUNTING_LINE_AMOUNT };
062        private String fileName;
063        private Integer lineNo = 0;
064    
065    
066        /**
067         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#getSourceEndowmentAccountingLineFormat()
068         */
069        public String[] getSourceEndowmentAccountingLineFormat() {
070            return DEFAULT_FORMAT;
071        }
072    
073        /**
074         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#getTargetEndowmentAccountingLineFormat()
075         */
076        public String[] getTargetEndowmentAccountingLineFormat() {
077            return DEFAULT_FORMAT;
078        }
079    
080        /**
081         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#getExpectedEndowmentAccountingLineFormatAsString(java.lang.Class)
082         */
083        public String getExpectedEndowmentAccountingLineFormatAsString(Class<? extends EndowmentAccountingLine> accountingLineClass) {
084            StringBuffer sb = new StringBuffer();
085            boolean first = true;
086            for (String attributeName : chooseFormat(accountingLineClass)) {
087                if (!first) {
088                    sb.append(",");
089                }
090                else {
091                    first = false;
092                }
093                sb.append(retrieveAttributeLabel(accountingLineClass, attributeName));
094            }
095            return sb.toString();
096        }
097    
098        /**
099         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#parseSourceEndowmentAccountingLine(org.kuali.kfs.module.endow.document.EndowmentAccountingLinesDocument,
100         *      java.lang.String)
101         */
102        public SourceEndowmentAccountingLine parseSourceEndowmentAccountingLine(EndowmentAccountingLinesDocument transactionalDocument, String sourceAccountingLineString) {
103            Class sourceAccountingLineClass = getSourceEndowmentAccountingLineClass(transactionalDocument);
104            SourceEndowmentAccountingLine sourceAccountingLine = (SourceEndowmentAccountingLine) populateAccountingLine(transactionalDocument, sourceAccountingLineClass, sourceAccountingLineString, parseAccountingLine(sourceAccountingLineClass, sourceAccountingLineString), transactionalDocument.getNextSourceLineNumber());
105            return sourceAccountingLine;
106        }
107    
108        /**
109         * Given a document, determines what class the source lines of that document uses
110         * 
111         * @param accountingDocument the document to find the class of the source lines for
112         * @return the class of the source lines
113         */
114        protected Class getSourceEndowmentAccountingLineClass(final EndowmentAccountingLinesDocument accountingDocument) {
115            return accountingDocument.getSourceAccountingLineClass();
116        }
117    
118        /**
119         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#parseTargetEndowmentAccountingLine(org.kuali.kfs.module.endow.document.EndowmentAccountingLinesDocument,
120         *      java.lang.String)
121         */
122        public TargetEndowmentAccountingLine parseTargetEndowmentAccountingLine(EndowmentAccountingLinesDocument transactionalDocument, String targetAccountingLineString) {
123            Class targetAccountingLineClass = getTargetEndowmentAccountingLineClass(transactionalDocument);
124            TargetEndowmentAccountingLine targetAccountingLine = (TargetEndowmentAccountingLine) populateAccountingLine(transactionalDocument, targetAccountingLineClass, targetAccountingLineString, parseAccountingLine(targetAccountingLineClass, targetAccountingLineString), transactionalDocument.getNextTargetLineNumber());
125            return targetAccountingLine;
126        }
127    
128        /**
129         * Given a document, determines what class that document uses for target accounting lines
130         * 
131         * @param accountingDocument the document to determine the target accounting line class for
132         * @return the class of the target lines for the given document
133         */
134        protected Class getTargetEndowmentAccountingLineClass(final EndowmentAccountingLinesDocument accountingDocument) {
135            return accountingDocument.getTargetAccountingLineClass();
136        }
137    
138        /**
139         * Populates a source/target line with values
140         * 
141         * @param transactionalDocument
142         * @param accountingLineClass
143         * @param accountingLineAsString
144         * @param attributeValueMap
145         * @param sequenceNumber
146         * @return AccountingLine
147         */
148        protected EndowmentAccountingLine populateAccountingLine(EndowmentAccountingLinesDocument transactionalDocument, Class<? extends EndowmentAccountingLine> accountingLineClass, String accountingLineAsString, Map<String, String> attributeValueMap, Integer sequenceNumber) {
149    
150            putCommonAttributesInMap(attributeValueMap, transactionalDocument, sequenceNumber);
151    
152            // create line and populate fields
153            EndowmentAccountingLine accountingLine;
154    
155            try {
156                accountingLine = (EndowmentAccountingLine) accountingLineClass.newInstance();
157    
158                // perform custom line population
159                if (SourceEndowmentAccountingLine.class.isAssignableFrom(accountingLineClass)) {
160                    performCustomSourceAccountingLinePopulation(attributeValueMap, (SourceEndowmentAccountingLine) accountingLine, accountingLineAsString);
161                }
162                else if (TargetEndowmentAccountingLine.class.isAssignableFrom(accountingLineClass)) {
163                    performCustomTargetAccountingLinePopulation(attributeValueMap, (TargetEndowmentAccountingLine) accountingLine, accountingLineAsString);
164                }
165                else {
166                    throw new IllegalArgumentException("invalid (unknown) endowment accounting line type: " + accountingLineClass);
167                }
168    
169                for (Entry<String, String> entry : attributeValueMap.entrySet()) {
170                    try {
171                        try {
172                            Class entryType = ObjectUtils.easyGetPropertyType(accountingLine, entry.getKey());
173                            if (String.class.isAssignableFrom(entryType)) {
174                                entry.setValue(entry.getValue().toUpperCase());
175                            }
176                            ObjectUtils.setObjectProperty(accountingLine, entry.getKey(), entryType, entry.getValue());
177                        }
178                        catch (IllegalArgumentException e) {
179                            throw new InfrastructureException("unable to complete endowment accounting line population.", e);
180                        }
181                    }
182                    catch (FormatException e) {
183                        String[] errorParameters = { entry.getValue().toString(), retrieveAttributeLabel(accountingLine.getClass(), entry.getKey()), accountingLineAsString };
184                        GlobalVariables.getMessageMap().putError(EndowConstants.ACCOUNTING_LINE_ERRORS, ERROR_INVALID_PROPERTY_VALUE, entry.getValue().toString(), entry.getKey(), accountingLineAsString + "  : Line Number " + lineNo.toString());
185                        throw new AccountingLineParserException("invalid '" + entry.getKey() + "=" + entry.getValue() + " for " + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters);
186                    }
187                }
188    
189                // override chart code if accounts can't cross charts
190                // TODO: check if this is needed
191                // SpringContext.getBean(AccountService.class).populateAccountingLineChartIfNeeded(accountingLine);
192            }
193            catch (SecurityException e) {
194                throw new InfrastructureException("unable to complete endowment accounting line population.", e);
195            }
196            catch (NoSuchMethodException e) {
197                throw new InfrastructureException("unable to complete endowment accounting line population.", e);
198            }
199            catch (IllegalAccessException e) {
200                throw new InfrastructureException("unable to complete endowment accounting line population.", e);
201            }
202            catch (InvocationTargetException e) {
203                throw new InfrastructureException("unable to complete endowment accounting line population.", e);
204            }
205            catch (InstantiationException e) {
206                throw new InfrastructureException("unable to complete endowment accounting line population.", e);
207            }
208    
209            // force input to uppercase
210            SpringContext.getBean(BusinessObjectDictionaryService.class).performForceUppercase(accountingLine);
211            accountingLine.refresh();
212    
213            return accountingLine;
214        }
215    
216        /**
217         * Places fields common to both source/target endowment accounting lines in the attribute map
218         * 
219         * @param attributeValueMap
220         * @param document
221         * @param sequenceNumber
222         */
223        protected void putCommonAttributesInMap(Map<String, String> attributeValueMap, EndowmentAccountingLinesDocument document, Integer sequenceNumber) {
224            attributeValueMap.put(KFSPropertyConstants.DOCUMENT_NUMBER, document.getDocumentNumber());
225            attributeValueMap.put(ENDOWMENT_ACCOUNTING_LINE_NBR, sequenceNumber.toString());
226        }
227    
228        /**
229         * Parses the csv line
230         * 
231         * @param accountingLineClass
232         * @param lineToParse
233         * @return Map containing accounting line attribute,value pairs
234         */
235        protected Map<String, String> parseAccountingLine(Class<? extends EndowmentAccountingLine> accountingLineClass, String lineToParse) {
236            if (StringUtils.isNotBlank(fileName) && !StringUtils.lowerCase(fileName).endsWith(".csv")) {
237                throw new AccountingLineParserException("unsupported file format: " + fileName, ERROR_INVALID_FILE_FORMAT, fileName);
238            }
239            String[] attributes = chooseFormat(accountingLineClass);
240            String[] attributeValues = StringUtils.splitPreserveAllTokens(lineToParse, ",");
241    
242            Map<String, String> attributeValueMap = new HashMap<String, String>();
243    
244            for (int i = 0; i < Math.min(attributeValues.length, attributes.length); i++) {
245                attributeValueMap.put(attributes[i], attributeValues[i]);
246            }
247    
248            return attributeValueMap;
249        }
250    
251        /**
252         * Should be overriden by documents to perform any additional <code>SourceAccountingLine</code> population
253         * 
254         * @param attributeValueMap
255         * @param sourceAccountingLine
256         * @param accountingLineAsString
257         */
258        protected void performCustomSourceAccountingLinePopulation(Map<String, String> attributeValueMap, SourceEndowmentAccountingLine sourceAccountingLine, String accountingLineAsString) {
259        }
260    
261        /**
262         * Should be overridden by documents to perform any additional <code>TargetAccountingLine</code> attribute population
263         * 
264         * @param attributeValueMap
265         * @param targetAccountingLine
266         * @param accountingLineAsString
267         */
268        protected void performCustomTargetAccountingLinePopulation(Map<String, String> attributeValueMap, TargetEndowmentAccountingLine targetAccountingLine, String accountingLineAsString) {
269        }
270    
271        /**
272         * Calls the appropriate parseAccountingLine method
273         * 
274         * @param stream
275         * @param transactionalDocument
276         * @param isSource
277         * @return List
278         */
279        protected List<EndowmentAccountingLine> importAccountingLines(String fileName, InputStream stream, EndowmentAccountingLinesDocument transactionalDocument, boolean isSource) {
280            List<EndowmentAccountingLine> importedAccountingLines = new ArrayList<EndowmentAccountingLine>();
281            this.fileName = fileName;
282            BufferedReader br = new BufferedReader(new InputStreamReader(stream));
283    
284            try {
285                String accountingLineAsString = null;
286                lineNo = 0;
287                while ((accountingLineAsString = br.readLine()) != null) {
288                    lineNo++;
289                    EndowmentAccountingLine accountingLine = null;
290    
291                    try {
292                        if (isSource) {
293                            accountingLine = parseSourceEndowmentAccountingLine(transactionalDocument, accountingLineAsString);
294                        }
295                        else {
296                            accountingLine = parseTargetEndowmentAccountingLine(transactionalDocument, accountingLineAsString);
297                        }
298    
299                        importedAccountingLines.add(accountingLine);
300                    }
301                    catch (AccountingLineParserException e) {
302                        GlobalVariables.getMessageMap().putError((isSource ? EndowPropertyConstants.EXISTING_SOURCE_ACCT_LINE_PREFIX : EndowPropertyConstants.EXISTING_TARGET_ACCT_LINE_PREFIX), EndowKeyConstants.ERROR_ENDOW_ACCOUNTING_LINES_DOCUMENT_ACCOUNTING_LINE_IMPORT_GENERAL, new String[] { e.getMessage() });
303                    }
304                }
305            }
306            catch (IOException e) {
307                throw new InfrastructureException("unable to readLine from bufferReader in endowmentAccountingLineParserBase", e);
308            }
309            finally {
310                try {
311                    br.close();
312                }
313                catch (IOException e) {
314                    throw new InfrastructureException("unable to close bufferReader in endowmentAccountingLineParserBase", e);
315                }
316            }
317    
318            return importedAccountingLines;
319        }
320    
321        /**
322         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#importSourceEndowmentAccountingLines(java.lang.String,
323         *      java.io.InputStream, org.kuali.kfs.module.endow.document.EndowmentAccountingLinesDocument)
324         */
325        public final List importSourceEndowmentAccountingLines(String fileName, InputStream stream, EndowmentAccountingLinesDocument document) {
326            return importAccountingLines(fileName, stream, document, true);
327        }
328    
329        /**
330         * @see org.kuali.kfs.module.endow.businessobject.EndowmentAccountingLineParser#importTargetEndowmentAccountingLines(java.lang.String,
331         *      java.io.InputStream, org.kuali.kfs.module.endow.document.EndowmentAccountingLinesDocument)
332         */
333        public final List importTargetEndowmentAccountingLines(String fileName, InputStream stream, EndowmentAccountingLinesDocument document) {
334            return importAccountingLines(fileName, stream, document, false);
335        }
336    
337    
338        /**
339         * Retrieves label for given attribute.
340         * 
341         * @param clazz
342         * @param attributeName
343         * @return
344         */
345        protected String retrieveAttributeLabel(Class clazz, String attributeName) {
346            String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(clazz, attributeName);
347            if (StringUtils.isBlank(label)) {
348                label = attributeName;
349            }
350            return label;
351        }
352    
353        /**
354         * Gets the accounting line format.
355         * 
356         * @param accountingLineClass
357         * @return
358         */
359        protected String[] chooseFormat(Class<? extends EndowmentAccountingLine> accountingLineClass) {
360            String[] format = null;
361            if (SourceEndowmentAccountingLine.class.isAssignableFrom(accountingLineClass)) {
362                format = getSourceEndowmentAccountingLineFormat();
363            }
364            else if (TargetEndowmentAccountingLine.class.isAssignableFrom(accountingLineClass)) {
365                format = getTargetEndowmentAccountingLineFormat();
366            }
367            else {
368                throw new IllegalStateException("unknow endowment accounting line class: " + accountingLineClass);
369            }
370            return format;
371        }
372    
373    }