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 }