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