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 }