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.purap.util;
017    
018    import static org.kuali.kfs.module.purap.PurapKeyConstants.ERROR_ITEMPARSER_INVALID_FILE_FORMAT;
019    import static org.kuali.kfs.module.purap.PurapKeyConstants.ERROR_ITEMPARSER_INVALID_NUMERIC_VALUE;
020    import static org.kuali.kfs.module.purap.PurapKeyConstants.ERROR_ITEMPARSER_ITEM_LINE;
021    import static org.kuali.kfs.module.purap.PurapKeyConstants.ERROR_ITEMPARSER_ITEM_PROPERTY;
022    import static org.kuali.kfs.module.purap.PurapKeyConstants.ERROR_ITEMPARSER_WRONG_PROPERTY_NUMBER;
023    import static org.kuali.kfs.module.purap.PurapPropertyConstants.ITEM_CATALOG_NUMBER;
024    import static org.kuali.kfs.module.purap.PurapPropertyConstants.ITEM_COMMODITY_CODE;
025    import static org.kuali.kfs.module.purap.PurapPropertyConstants.ITEM_DESCRIPTION;
026    import static org.kuali.kfs.module.purap.PurapPropertyConstants.ITEM_QUANTITY;
027    import static org.kuali.kfs.module.purap.PurapPropertyConstants.ITEM_UNIT_PRICE;
028    
029    import java.io.BufferedReader;
030    import java.io.IOException;
031    import java.io.InputStream;
032    import java.io.InputStreamReader;
033    import java.lang.reflect.InvocationTargetException;
034    import java.util.ArrayList;
035    import java.util.HashMap;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.Map.Entry;
039    
040    import org.apache.commons.lang.StringUtils;
041    import org.apache.struts.upload.FormFile;
042    import org.kuali.kfs.module.purap.PurapConstants;
043    import org.kuali.kfs.module.purap.PurapParameterConstants;
044    import org.kuali.kfs.module.purap.businessobject.PurApItem;
045    import org.kuali.kfs.module.purap.businessobject.PurchaseOrderItem;
046    import org.kuali.kfs.module.purap.businessobject.RequisitionItem;
047    import org.kuali.kfs.module.purap.exception.ItemParserException;
048    import org.kuali.kfs.sys.KFSKeyConstants;
049    import org.kuali.kfs.sys.KFSPropertyConstants;
050    import org.kuali.kfs.sys.context.SpringContext;
051    import org.kuali.kfs.sys.service.impl.KfsParameterConstants;
052    import org.kuali.rice.kns.exception.InfrastructureException;
053    import org.kuali.rice.kns.service.DataDictionaryService;
054    import org.kuali.rice.kns.service.KualiConfigurationService;
055    import org.kuali.rice.kns.service.ParameterService;
056    import org.kuali.rice.kns.util.GlobalVariables;
057    import org.kuali.rice.kns.util.ObjectUtils;
058    import org.kuali.rice.kns.web.format.FormatException;
059    
060    public class ItemParserBase implements ItemParser {
061    
062        /**
063         * The default format defines the expected item property names and their order in the import file.
064         * Please update this if the import file format changes (i.e. adding/deleting item properties, changing their order).
065         */
066        protected static final String[] DEFAULT_FORMAT = {ITEM_QUANTITY, KFSPropertyConstants.ITEM_UNIT_OF_MEASURE_CODE, ITEM_CATALOG_NUMBER, ITEM_COMMODITY_CODE, ITEM_DESCRIPTION, ITEM_UNIT_PRICE};
067        protected static final String[] COMMODITY_CODE_DISABLED_FORMAT = {ITEM_QUANTITY, KFSPropertyConstants.ITEM_UNIT_OF_MEASURE_CODE, ITEM_CATALOG_NUMBER, ITEM_DESCRIPTION, ITEM_UNIT_PRICE};
068        
069        private Integer lineNo = 0;
070    
071        /**
072         * @see org.kuali.kfs.module.purap.util.ItemParser#getItemFormat()
073         */
074        public String[] getItemFormat() {
075            //Check the ENABLE_COMMODITY_CODE_IND system parameter. If it's Y then 
076            //we should return the DEFAULT_FORMAT, otherwise
077            //we should return the COMMODITY_CODE_DISABLED_FORMAT
078            boolean enableCommodityCode = SpringContext.getBean(ParameterService.class).getIndicatorParameter(KfsParameterConstants.PURCHASING_DOCUMENT.class, PurapParameterConstants.ENABLE_COMMODITY_CODE_IND);
079            if (enableCommodityCode) {
080                return DEFAULT_FORMAT;
081            }
082            return COMMODITY_CODE_DISABLED_FORMAT;
083        }
084    
085        /**
086         * @see org.kuali.kfs.module.purap.util.ItemParser#getExpectedItemFormatAsString(java.lang.Class)
087         */
088        public String getExpectedItemFormatAsString( Class<? extends PurApItem> itemClass ) {
089            checkItemClass( itemClass );
090            StringBuffer sb = new StringBuffer();
091            boolean first = true;
092            for (String attributeName : getItemFormat()) {
093                if (!first) {
094                    sb.append(",");
095                }
096                else {
097                    first = false;
098                }
099                sb.append( getAttributeLabel( itemClass, attributeName ) );
100            }
101            return sb.toString();
102        }
103    
104        /**
105         * Retrieves the attribute label for the specified attribute.
106         * 
107         * @param clazz the class in which the specified attribute is defined
108         * @param attributeName the name of the specified attribute
109         * @return the attribute label for the specified attribute
110         */
111        @SuppressWarnings("rawtypes")
112        protected String getAttributeLabel( Class clazz, String attributeName ) {
113            String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(clazz, attributeName);
114            if (StringUtils.isBlank(label)) {
115                label = attributeName;
116            }
117            return label;
118        }
119    
120        /**
121         * Checks whether the specified item class is a subclass of PurApItem;
122         * throws exceptions if not.
123         * 
124         * @param itemClass the specified item class
125         */
126        protected void checkItemClass(Class<? extends PurApItem> itemClass) {
127            if (!PurApItem.class.isAssignableFrom(itemClass)) {
128                throw new IllegalArgumentException("unknown item class: " + itemClass);
129            }
130        }
131        
132        /**
133         * Checks whether the specified item import file is not null and of a valid format;
134         * throws exceptions if conditions not satisfied.
135         * 
136         * @param itemClass the specified item import file
137         */
138        protected void checkItemFile(FormFile itemFile) {
139            if (itemFile == null) {
140                throw new ItemParserException("invalid (null) item import file", KFSKeyConstants.ERROR_UPLOADFILE_NULL);
141            }
142            String fileName = itemFile.getFileName();
143            if (StringUtils.isNotBlank(fileName) && !StringUtils.lowerCase(fileName).endsWith(".csv") && !StringUtils.lowerCase(fileName).endsWith(".xls")) {
144                throw new ItemParserException("unsupported item import file format: " + fileName, ERROR_ITEMPARSER_INVALID_FILE_FORMAT, fileName);
145            }
146        }
147    
148        /**
149         * Parses a line of item data from a csv file and retrieves the attributes as key-value string pairs into a map.
150         * 
151         * @param itemLine a string read from a line in the item import file
152         * @return a map containing item attribute name-value string pairs
153         */
154        protected Map<String, String> retrieveItemAttributes( String itemLine ) {
155            String[] attributeNames = getItemFormat();
156            String[] attributeValues = StringUtils.splitPreserveAllTokens(itemLine, ',');
157            if ( attributeNames.length != attributeValues.length ) {
158                String[] errorParams = { "" + attributeNames.length, "" + attributeValues.length, "" + lineNo };
159                GlobalVariables.getMessageMap().putError( PurapConstants.ITEM_TAB_ERRORS, ERROR_ITEMPARSER_WRONG_PROPERTY_NUMBER, errorParams );
160                throw new ItemParserException("wrong number of item properties: " + attributeValues.length + " exist, " + attributeNames.length + " expected (line " + lineNo + ")", ERROR_ITEMPARSER_WRONG_PROPERTY_NUMBER, errorParams); 
161            }
162    
163            Map<String, String> itemMap = new HashMap<String, String>();
164            for (int i=0; i < attributeNames.length; i++) {
165                itemMap.put( attributeNames[i], attributeValues[i] );
166            }
167            return itemMap;
168        }
169        
170        /**
171         * Generates an item instance and populates it with the specified attribute map.
172         * 
173         * @param itemMap the specified attribute map from which attributes are populated
174         * @param itemClass the class of which the new item instance shall be created
175         * @return the populated item
176         */
177        protected PurApItem genItemWithRetrievedAttributes( Map<String, String> itemMap, Class<? extends PurApItem> itemClass ) {
178            PurApItem item;
179            try {
180                item = itemClass.newInstance();
181            }
182            catch (IllegalAccessException e) {
183                throw new InfrastructureException("unable to complete item line population.", e);
184            }
185            catch (InstantiationException e) {
186                throw new InfrastructureException("unable to complete item line population.", e);
187            }
188            
189            boolean failed = false;
190            for (Entry<String, String> entry : itemMap.entrySet()) {
191                String key = entry.getKey();
192                String value = entry.getValue();          
193                try {
194                    /* removing this part as the checking are done in rule class later
195                    if ((key.equals(ITEM_DESCRIPTION) || key.equals(ITEM_UNIT_PRICE)) && value.equals("")) {
196                        String[] errorParams = { key, "" + lineNo };
197                        throw new ItemParserException("empty property value for " + key + " (line " + lineNo + ")", ERROR_ITEMPARSER_EMPTY_PROPERTY_VALUE, errorParams);                    
198                    }
199                    else */
200                    if (key.equals(KFSPropertyConstants.ITEM_UNIT_OF_MEASURE_CODE)) {
201                        value = value.toUpperCase(); // force UOM code to uppercase
202                    }
203                    try {
204                        ObjectUtils.setObjectProperty(item, key, value);
205                    }
206                    catch (FormatException e) {
207                        String[] errorParams = { value, key, "" + lineNo };
208                        throw new ItemParserException("invalid numeric property value: " + key + " = " + value + " (line " + lineNo + ")", ERROR_ITEMPARSER_INVALID_NUMERIC_VALUE, errorParams);
209                    }
210                }
211                catch (ItemParserException e) {
212                    // continue to parse the rest of the item properties after the current property fails
213                    GlobalVariables.getMessageMap().putError( PurapConstants.ITEM_TAB_ERRORS, e.getErrorKey(), e.getErrorParameters() );
214                    failed = true;
215                }
216                catch (IllegalAccessException e) {
217                    throw new InfrastructureException("unable to complete item line population.", e);
218                }
219                catch (NoSuchMethodException e) {
220                    throw new InfrastructureException("unable to complete item line population.", e);
221                }
222                catch (InvocationTargetException e) {
223                    throw new InfrastructureException("unable to complete item line population.", e);
224                }
225            }
226    
227            if (failed) {
228                throw new ItemParserException("empty or invalid item properties in line " + lineNo + ")", ERROR_ITEMPARSER_ITEM_PROPERTY, ""+lineNo);             
229            }
230            return item;
231        }
232    
233        /**
234         * Populates extra item attributes not contained in the imported item data to default values.
235         * 
236         * @param item the item to be populated
237         * @param documentNumber the number of the docment that contains the item
238         */
239        protected void populateExtraAttributes( PurApItem item, String documentNumber ) {     
240            if (item.getItemQuantity() != null) {
241                String paramName = PurapParameterConstants.DEFAULT_QUANTITY_ITEM_TYPE;
242                String itemTypeCode = SpringContext.getBean(KualiConfigurationService.class).getParameterValue(PurapConstants.PURAP_NAMESPACE, "Document", paramName);            
243                item.setItemTypeCode(itemTypeCode);
244            }
245            else {
246                String paramName = PurapParameterConstants.DEFAULT_NON_QUANTITY_ITEM_TYPE;
247                String itemTypeCode = SpringContext.getBean(KualiConfigurationService.class).getParameterValue(PurapConstants.PURAP_NAMESPACE, "Document", paramName);
248                item.setItemTypeCode(itemTypeCode);
249            }
250            if (item instanceof RequisitionItem)
251                ((RequisitionItem)item).setItemRestrictedIndicator(false);
252            if (item instanceof PurchaseOrderItem)
253                ((PurchaseOrderItem)item).setDocumentNumber(documentNumber);
254        }
255        
256        /**
257         * @see org.kuali.kfs.module.purap.util.ItemParser#parseItem(java.lang.String,java.lang.Class,java.lang.String)
258         */
259        public PurApItem parseItem( String itemLine, Class<? extends PurApItem> itemClass, String documentNumber ) {
260            Map<String, String> itemMap = retrieveItemAttributes( itemLine );
261            PurApItem item = genItemWithRetrievedAttributes( itemMap, itemClass );
262            populateExtraAttributes( item, documentNumber );
263            item.refresh();
264            return item;
265        }
266        
267        /**
268         * @see org.kuali.kfs.module.purap.util.ItemParser#parseItem(org.apache.struts.upload.FormFile,java.lang.Class,java.lang.String)
269         */
270        public List<PurApItem> importItems( FormFile itemFile, Class<? extends PurApItem> itemClass, String documentNumber ) {
271            // check input parameters
272            try {
273                checkItemClass( itemClass );
274                checkItemFile( itemFile );
275            }
276            catch (IllegalArgumentException e) {
277                throw new InfrastructureException("unable to import items.", e);
278            }
279    
280            // open input stream
281            List<PurApItem> importedItems = new ArrayList<PurApItem>();
282            InputStream is;
283            BufferedReader br;
284            try {
285                is = itemFile.getInputStream();
286                br = new BufferedReader(new InputStreamReader(is));
287            }
288            catch (IOException e) {
289                throw new InfrastructureException("unable to open import file in ItemParserBase.", e);
290            }
291            
292            // parse items line by line
293            lineNo = 0;
294            boolean failed = false;
295            String itemLine = null;
296            try {
297                while ( (itemLine = br.readLine()) != null ) {
298                    lineNo++;
299                    try {
300                        PurApItem item = parseItem( itemLine, itemClass, documentNumber );
301                        importedItems.add(item);
302                    }
303                    catch (ItemParserException e) {
304                        // continue to parse the rest of the items after the current item fails
305                        // error messages are already dealt with inside parseItem, so no need to do anything here
306                        failed = true;
307                    }                
308                }
309                
310                if (failed) {
311                    throw new ItemParserException("errors in parsing item lines in file " + itemFile.getFileName(), ERROR_ITEMPARSER_ITEM_LINE, itemFile.getFileName());             
312                }
313            }
314            catch (IOException e) {
315                throw new InfrastructureException("unable to read line from BufferReader in ItemParserBase", e);
316            }
317            finally {
318                try {
319                    br.close();
320                }
321                catch (IOException e) {
322                    throw new InfrastructureException("unable to close BufferReader in ItemParserBase", e);
323                }
324            }
325    
326            return importedItems;
327        }
328    
329    }