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.util;
017    
018    import static org.kuali.kfs.module.endow.EndowConstants.TRANSACTION_LINE_ERRORS;
019    import static org.kuali.kfs.module.endow.EndowPropertyConstants.KEMID;
020    import static org.kuali.kfs.module.endow.EndowPropertyConstants.TRANSACTION_LINE_DESCRIPTION;
021    import static org.kuali.kfs.module.endow.EndowPropertyConstants.TRANSACTION_LINE_ENDOWMENT_TRANSACTION_CODE;
022    import static org.kuali.kfs.module.endow.EndowPropertyConstants.TRANSACTION_LINE_IP_INDICATOR_CODE;
023    import static org.kuali.kfs.module.endow.EndowPropertyConstants.TRANSACTION_LINE_TRANSACTION_UNITS;
024    import static org.kuali.kfs.module.endow.EndowPropertyConstants.TRANSACTION_LINE_TRANSACTION_AMOUNT;
025    import static org.kuali.kfs.module.endow.EndowPropertyConstants.TRANSACTION_LINE_TRANSACTION_UNIT_ADJUSTMENT_AMOUNT;
026    
027    import java.io.BufferedReader;
028    import java.io.IOException;
029    import java.io.InputStream;
030    import java.io.InputStreamReader;
031    import java.lang.reflect.InvocationTargetException;
032    import java.util.ArrayList;
033    import java.util.HashMap;
034    import java.util.List;
035    import java.util.Map;
036    import java.util.Map.Entry;
037    
038    import org.apache.commons.lang.StringUtils;
039    import org.apache.struts.upload.FormFile;
040    import org.kuali.kfs.module.endow.EndowKeyConstants;
041    import org.kuali.kfs.module.endow.EndowKeyConstants.EndowmentTransactionDocumentConstants;
042    import org.kuali.kfs.module.endow.businessobject.EndowmentTransactionLine;
043    import org.kuali.kfs.module.endow.businessobject.EndowmentTransactionLineBase;
044    import org.kuali.kfs.module.endow.exception.LineParserException;
045    import org.kuali.kfs.sys.KFSKeyConstants;
046    import org.kuali.kfs.sys.context.SpringContext;
047    import org.kuali.rice.kns.exception.InfrastructureException;
048    import org.kuali.rice.kns.service.DataDictionaryService;
049    import org.kuali.rice.kns.util.GlobalVariables;
050    import org.kuali.rice.kns.util.ObjectUtils;
051    import org.kuali.rice.kns.web.format.FormatException;
052    
053    public class LineParserBase implements LineParser {
054    
055        /**
056         * The default format defines the expected line property names and their order in the import file. Please update this if the
057         * import file format changes (i.e. adding/deleting line properties, changing their order).
058         */
059        protected static final String[] DEFAULT_LINE_FORMAT = { KEMID, TRANSACTION_LINE_ENDOWMENT_TRANSACTION_CODE, TRANSACTION_LINE_DESCRIPTION, TRANSACTION_LINE_IP_INDICATOR_CODE, TRANSACTION_LINE_TRANSACTION_UNITS, TRANSACTION_LINE_TRANSACTION_AMOUNT, TRANSACTION_LINE_TRANSACTION_UNIT_ADJUSTMENT_AMOUNT };
060    
061        private Integer lineNo = 0;
062    
063        /**
064         * @see org.kuali.kfs.module.purap.util.ItemParser#getItemFormat()
065         */
066        public String[] getLineFormat() {
067            return DEFAULT_LINE_FORMAT;
068        }
069    
070    
071        /**
072         * Retrieves the attribute label for the specified attribute.
073         * 
074         * @param clazz the class in which the specified attribute is defined
075         * @param attributeName the name of the specified attribute
076         * @return the attribute label for the specified attribute
077         */
078        protected String getAttributeLabel(Class clazz, String attributeName) {
079            String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(clazz, attributeName);
080            if (StringUtils.isBlank(label)) {
081                label = attributeName;
082            }
083            return label;
084        }
085    
086        /**
087         * Checks whether the specified Line class is a subclass of EndowmentTransactionLine; throws exceptions if not.
088         * 
089         * @param lineClass the specified line class
090         */
091        protected void checkLineClass(Class<? extends EndowmentTransactionLine> lineClass) {
092            if (!EndowmentTransactionLine.class.isAssignableFrom(lineClass))
093                throw new IllegalArgumentException("Unknown Line class: " + lineClass);
094        }
095    
096        /**
097         * Checks whether the specified line import file is not null and of a valid format; throws exceptions if conditions not
098         * satisfied.
099         * 
100         * @param lineClass the specified line import file
101         */
102        protected void checkLineFile(FormFile lineFile) {
103            if (lineFile == null)
104                throw new LineParserException("Invalid (null) Line import file", KFSKeyConstants.ERROR_UPLOADFILE_NULL);
105    
106            if (lineFile.getFileSize() == 0)
107                throw new LineParserException("Invalid (null) Line import file", KFSKeyConstants.ERROR_UPLOADFILE_NULL);
108    
109            String fileName = lineFile.getFileName();
110            if (StringUtils.isNotBlank(fileName) && !StringUtils.lowerCase(fileName).endsWith(".csv"))
111                throw new LineParserException("unsupported Line import file format: " + fileName, KFSKeyConstants.ERROR_LINEPARSER_INVALID_FILE_FORMAT, fileName);
112        }
113    
114        /**
115         * Parses a line of transactions data from a csv file and retrieves the attributes as key-value string pairs into a map.
116         * 
117         * @param line a string read from a line in the line import file
118         * @return a map containing line attribute name-value string pairs
119         */
120        protected Map<String, String> retrieveLineAttributes(String line, Class<? extends EndowmentTransactionLine> lineClass) {
121            String[] attributeNames = getLineFormat();
122            String[] attributeValues = StringUtils.splitPreserveAllTokens(line, ',');
123    
124            if (attributeNames.length != attributeValues.length) {
125                String[] errorParams = { "" + attributeNames.length, "" + attributeValues.length, "" + lineNo };
126                GlobalVariables.getMessageMap().putError(TRANSACTION_LINE_ERRORS, EndowmentTransactionDocumentConstants.ERROR_LINEPARSER_WRONG_PROPERTY_NUMBER, errorParams);
127                throw new LineParserException("Wrong number of Line properties: " + attributeValues.length + " exist, " + attributeNames.length + " expected (line " + lineNo + ")", EndowmentTransactionDocumentConstants.ERROR_LINEPARSER_WRONG_PROPERTY_NUMBER, errorParams);
128            }
129    
130            Map<String, String> lineMap = new HashMap<String, String>();
131            for (int i = 0; i < attributeNames.length; i++) {
132                String attributeName = attributeNames[i];
133                String attributeValue = attributeValues[i];
134                // if the attribute has an forceUpper = true in the data dictionary, convert the value to upper case...
135                if (SpringContext.getBean(DataDictionaryService.class).getAttributeForceUppercase(lineClass, attributeName)) {
136                    attributeValue = attributeValue.toUpperCase();
137                }
138                
139                lineMap.put(attributeName, attributeValue);
140            }
141            return lineMap;
142        }
143    
144        /**
145         * Generates an line instance and populates it with the specified attribute map.
146         * 
147         * @param lineMap the specified attribute map from which attributes are populated
148         * @param lineClass the class of which the new line instance shall be created
149         * @return the populated line
150         */
151        protected EndowmentTransactionLine genLineWithRetrievedAttributes(Map<String, String> lineMap, Class<? extends EndowmentTransactionLine> lineClass) {
152            EndowmentTransactionLine line;
153            try {
154                line = lineClass.newInstance();
155            }
156            catch (IllegalAccessException e) {
157                throw new InfrastructureException("Unable to complete line line population.", e);
158            }
159            catch (InstantiationException e) {
160                throw new InfrastructureException("Unable to complete line line population.", e);
161            }
162    
163            boolean failed = false;
164            for (Entry<String, String> entry : lineMap.entrySet()) {
165                String key = entry.getKey();
166                String value = entry.getValue();
167                try {
168                    try {
169                        ObjectUtils.setObjectProperty(line, key, value);
170                    }
171                    catch (FormatException e) {
172                        String[] errorParams = { value, key, "" + lineNo };
173                        throw new LineParserException("Invalid property value: " + key + " = " + value + " (line " + lineNo + ")", EndowmentTransactionDocumentConstants.ERROR_LINEPARSER_INVALID_PROPERTY_VALUE, errorParams);
174                    }
175                }
176                catch (LineParserException e) {
177                    // Continue to parse the rest of the line properties after the current property fails
178                    GlobalVariables.getMessageMap().putError(TRANSACTION_LINE_ERRORS, e.getErrorKey(), e.getErrorParameters());
179                    failed = true;
180                }
181                catch (IllegalAccessException e) {
182                    throw new InfrastructureException("unable to complete line line population.", e);
183                }
184                catch (NoSuchMethodException e) {
185                    throw new InfrastructureException("unable to complete line line population.", e);
186                }
187                catch (InvocationTargetException e) {
188                    throw new InfrastructureException("unable to complete line line population.", e);
189                }
190            }
191    
192            if (failed) {
193                throw new LineParserException("empty or invalid line properties in line " + lineNo + ")", EndowmentTransactionDocumentConstants.ERROR_TRANSACTION_LINE_PARSE_INVALID, "" + lineNo);
194            }
195            return line;
196        }
197    
198        /**
199         * @see org.kuali.kfs.module.purap.util.ItemParser#parseItem(org.apache.struts.upload.FormFile,java.lang.Class,java.lang.String)
200         */
201        public List<EndowmentTransactionLine> importLines(FormFile lineFile, Class<? extends EndowmentTransactionLine> lineClass, String documentNumber) {
202            InputStream is;
203            BufferedReader br = null;
204    
205            // Open input stream
206            List<EndowmentTransactionLine> importedLines = new ArrayList<EndowmentTransactionLine>();
207    
208            try {
209                // Check input File.
210                checkLineFile(lineFile);
211    
212                is = lineFile.getInputStream();
213                br = new BufferedReader(new InputStreamReader(is));
214    
215                // Parse lines line by line
216                lineNo = 0;
217                boolean failed = false;
218                String tranLine = null;
219                try {
220                    while ((tranLine = br.readLine()) != null) {
221                        lineNo++;
222                        try {
223                            EndowmentTransactionLine line = parseLine(tranLine, lineClass, documentNumber);
224                            importedLines.add(line);
225                        }
226                        catch (RuntimeException e) {
227                            // continue to parse the rest of the lines after the current line fails
228                            // error messages are already dealt with inside parseItem, so no need to do anything here
229                            failed = true;
230                        }
231                    }
232    
233                    if (failed) {
234                        throw new LineParserException("Errors in parsing lines in file " + lineFile.getFileName(), EndowKeyConstants.EndowmentTransactionDocumentConstants.ERROR_TRANSACTION_LINE_PARSE_INVALID, lineFile.getFileName());
235                    }
236                }
237                catch (IOException e) {
238                    throw new InfrastructureException("Unable to read line from BufferReader in LineParserBase", e);
239                }
240            }
241            catch (IOException e) {
242                throw new InfrastructureException("Unable to read line from BufferReader in LineParserBase", e);
243            }
244            finally {
245                try {
246                    if (null != br)
247                        br.close();
248                }
249                catch (IOException e) {
250                    throw new InfrastructureException("Unable to close BufferReader in LineParserBase", e);
251                }
252            }
253    
254            return importedLines;
255        }
256    
257        /**
258         * @see org.kuali.kfs.module.purap.util.ItemParser#parseItem(java.lang.String,java.lang.Class,java.lang.String)
259         */
260        public EndowmentTransactionLine parseLine(String transactionLine, Class<? extends EndowmentTransactionLine> lineClass, String documentNumber) {
261            Map<String, String> lineMap = retrieveLineAttributes(transactionLine, lineClass);
262            EndowmentTransactionLine line = genLineWithRetrievedAttributes(lineMap, lineClass);
263            // populateExtraAttributes( line, documentNumber );
264            line.refresh();
265            return line;
266        }
267    }