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 017 package org.kuali.kfs.sys.businessobject; 018 019 import static org.kuali.kfs.sys.KFSKeyConstants.AccountingLineParser.ERROR_INVALID_FILE_FORMAT; 020 import static org.kuali.kfs.sys.KFSKeyConstants.AccountingLineParser.ERROR_INVALID_PROPERTY_VALUE; 021 import static org.kuali.kfs.sys.KFSPropertyConstants.ACCOUNT_NUMBER; 022 import static org.kuali.kfs.sys.KFSPropertyConstants.AMOUNT; 023 import static org.kuali.kfs.sys.KFSPropertyConstants.CHART_OF_ACCOUNTS_CODE; 024 import static org.kuali.kfs.sys.KFSPropertyConstants.FINANCIAL_OBJECT_CODE; 025 import static org.kuali.kfs.sys.KFSPropertyConstants.FINANCIAL_SUB_OBJECT_CODE; 026 import static org.kuali.kfs.sys.KFSPropertyConstants.ORGANIZATION_REFERENCE_ID; 027 import static org.kuali.kfs.sys.KFSPropertyConstants.OVERRIDE_CODE; 028 import static org.kuali.kfs.sys.KFSPropertyConstants.POSTING_YEAR; 029 import static org.kuali.kfs.sys.KFSPropertyConstants.PROJECT_CODE; 030 import static org.kuali.kfs.sys.KFSPropertyConstants.SEQUENCE_NUMBER; 031 import static org.kuali.kfs.sys.KFSPropertyConstants.SUB_ACCOUNT_NUMBER; 032 033 import java.io.BufferedReader; 034 import java.io.IOException; 035 import java.io.InputStream; 036 import java.io.InputStreamReader; 037 import java.lang.reflect.InvocationTargetException; 038 import java.util.ArrayList; 039 import java.util.HashMap; 040 import java.util.List; 041 import java.util.Map; 042 import java.util.Map.Entry; 043 044 import org.apache.commons.lang.StringUtils; 045 import org.kuali.kfs.coa.service.AccountService; 046 import org.kuali.kfs.sys.KFSConstants; 047 import org.kuali.kfs.sys.KFSKeyConstants; 048 import org.kuali.kfs.sys.KFSPropertyConstants; 049 import org.kuali.kfs.sys.context.SpringContext; 050 import org.kuali.kfs.sys.document.AccountingDocument; 051 import org.kuali.kfs.sys.exception.AccountingLineParserException; 052 import org.kuali.rice.kns.exception.InfrastructureException; 053 import org.kuali.rice.kns.service.BusinessObjectDictionaryService; 054 import org.kuali.rice.kns.service.DataDictionaryService; 055 import org.kuali.rice.kns.util.GlobalVariables; 056 import org.kuali.rice.kns.util.ObjectUtils; 057 import org.kuali.rice.kns.web.format.FormatException; 058 059 /** 060 * Base class for parsing serialized <code>AccountingLine</code>s for <code>TransactionalDocument</code>s 061 */ 062 public class AccountingLineParserBase implements AccountingLineParser { 063 protected static final String[] DEFAULT_FORMAT = { CHART_OF_ACCOUNTS_CODE, ACCOUNT_NUMBER, SUB_ACCOUNT_NUMBER, FINANCIAL_OBJECT_CODE, FINANCIAL_SUB_OBJECT_CODE, PROJECT_CODE, ORGANIZATION_REFERENCE_ID, AMOUNT }; 064 private String fileName; 065 private Integer lineNo = 0; 066 067 /** 068 * @see org.kuali.rice.kns.bo.AccountingLineParser#getSourceAccountingLineFormat() 069 */ 070 public String[] getSourceAccountingLineFormat() { 071 return removeChartFromFormatIfNeeded(DEFAULT_FORMAT); 072 } 073 074 /** 075 * @see org.kuali.rice.kns.bo.AccountingLineParser#getTargetAccountingLineFormat() 076 */ 077 public String[] getTargetAccountingLineFormat() { 078 return removeChartFromFormatIfNeeded(DEFAULT_FORMAT); 079 } 080 081 /** 082 * If accounts can cross charts, returns the given format; 083 * otherwise returns the format with ChartOfAccountsCode field removed. 084 */ 085 public String[] removeChartFromFormatIfNeeded(String[] format) { 086 if (SpringContext.getBean(AccountService.class).accountsCanCrossCharts()) { 087 return format; 088 } 089 090 // if accounts can't cross charts, exclude ChartOfAccountsCode field from the format 091 String[] formatNoChart = new String[format.length-1]; 092 int idx = 0; 093 for (int i=0; i<format.length; i++) { 094 if (format[i].equals(CHART_OF_ACCOUNTS_CODE)) 095 continue; 096 else { 097 formatNoChart[idx] = format[i]; 098 idx++; 099 } 100 } 101 return formatNoChart; 102 } 103 104 /** 105 * @see org.kuali.rice.kns.bo.AccountingLineParser#getExpectedAccountingLineFormatAsString(java.lang.Class) 106 */ 107 public String getExpectedAccountingLineFormatAsString(Class<? extends AccountingLine> accountingLineClass) { 108 StringBuffer sb = new StringBuffer(); 109 boolean first = true; 110 for (String attributeName : chooseFormat(accountingLineClass)) { 111 if (!first) { 112 sb.append(","); 113 } 114 else { 115 first = false; 116 } 117 sb.append(retrieveAttributeLabel(accountingLineClass, attributeName)); 118 } 119 return sb.toString(); 120 } 121 122 /** 123 * @see org.kuali.rice.kns.bo.AccountingLineParser#parseSourceAccountingLine(org.kuali.rice.kns.document.TransactionalDocument, 124 * java.lang.String) 125 */ 126 public SourceAccountingLine parseSourceAccountingLine(AccountingDocument transactionalDocument, String sourceAccountingLineString) { 127 Class sourceAccountingLineClass = getSourceAccountingLineClass(transactionalDocument); 128 SourceAccountingLine sourceAccountingLine = (SourceAccountingLine) populateAccountingLine(transactionalDocument, sourceAccountingLineClass, sourceAccountingLineString, parseAccountingLine(sourceAccountingLineClass, sourceAccountingLineString), transactionalDocument.getNextSourceLineNumber()); 129 return sourceAccountingLine; 130 } 131 132 /** 133 * Given a document, determines what class the source lines of that document uses 134 * @param accountingDocument the document to find the class of the source lines for 135 * @return the class of the source lines 136 */ 137 protected Class getSourceAccountingLineClass(final AccountingDocument accountingDocument) { 138 return accountingDocument.getSourceAccountingLineClass(); 139 } 140 141 /** 142 * @see org.kuali.rice.kns.bo.AccountingLineParser#parseTargetAccountingLine(org.kuali.rice.kns.document.TransactionalDocument, 143 * java.lang.String) 144 */ 145 public TargetAccountingLine parseTargetAccountingLine(AccountingDocument transactionalDocument, String targetAccountingLineString) { 146 Class targetAccountingLineClass = getTargetAccountingLineClass(transactionalDocument); 147 TargetAccountingLine targetAccountingLine = (TargetAccountingLine) populateAccountingLine(transactionalDocument, targetAccountingLineClass, targetAccountingLineString, parseAccountingLine(targetAccountingLineClass, targetAccountingLineString), transactionalDocument.getNextTargetLineNumber()); 148 return targetAccountingLine; 149 } 150 151 /** 152 * Given a document, determines what class that document uses for target accounting lines 153 * @param accountingDocument the document to determine the target accounting line class for 154 * @return the class of the target lines for the given document 155 */ 156 protected Class getTargetAccountingLineClass(final AccountingDocument accountingDocument) { 157 return accountingDocument.getTargetAccountingLineClass(); 158 } 159 160 /** 161 * Populates a source/target line with values 162 * 163 * @param transactionalDocument 164 * @param accountingLineClass 165 * @param accountingLineAsString 166 * @param attributeValueMap 167 * @param sequenceNumber 168 * @return AccountingLine 169 */ 170 protected AccountingLine populateAccountingLine(AccountingDocument transactionalDocument, Class<? extends AccountingLine> accountingLineClass, String accountingLineAsString, Map<String, String> attributeValueMap, Integer sequenceNumber) { 171 172 putCommonAttributesInMap(attributeValueMap, transactionalDocument, sequenceNumber); 173 174 // create line and populate fields 175 AccountingLine accountingLine; 176 177 try { 178 accountingLine = (AccountingLine) accountingLineClass.newInstance(); 179 180 // perform custom line population 181 if (SourceAccountingLine.class.isAssignableFrom(accountingLineClass)) { 182 performCustomSourceAccountingLinePopulation(attributeValueMap, (SourceAccountingLine) accountingLine, accountingLineAsString); 183 } 184 else if (TargetAccountingLine.class.isAssignableFrom(accountingLineClass)) { 185 performCustomTargetAccountingLinePopulation(attributeValueMap, (TargetAccountingLine) accountingLine, accountingLineAsString); 186 } 187 else { 188 throw new IllegalArgumentException("invalid (unknown) accounting line type: " + accountingLineClass); 189 } 190 191 for (Entry<String, String> entry : attributeValueMap.entrySet()) { 192 try { 193 try { 194 Class entryType = ObjectUtils.easyGetPropertyType(accountingLine, entry.getKey()); 195 if (String.class.isAssignableFrom(entryType)) { 196 entry.setValue(entry.getValue().toUpperCase()); 197 } 198 ObjectUtils.setObjectProperty(accountingLine, entry.getKey(), entryType, entry.getValue()); 199 } 200 catch (IllegalArgumentException e) { 201 throw new InfrastructureException("unable to complete accounting line population.", e); 202 } 203 } 204 catch (FormatException e) { 205 String[] errorParameters = { entry.getValue().toString(), retrieveAttributeLabel(accountingLine.getClass(), entry.getKey()), accountingLineAsString }; 206 // KULLAB-408 207 GlobalVariables.getMessageMap().putError(KFSConstants.ACCOUNTING_LINE_ERRORS, ERROR_INVALID_PROPERTY_VALUE, entry.getValue().toString(), entry.getKey(), accountingLineAsString + " : Line Number " + lineNo.toString()); 208 throw new AccountingLineParserException("invalid '" + entry.getKey() + "=" + entry.getValue() + "for " + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters); 209 } 210 } 211 212 // override chart code if accounts can't cross charts 213 SpringContext.getBean(AccountService.class).populateAccountingLineChartIfNeeded(accountingLine); 214 } 215 catch (SecurityException e) { 216 throw new InfrastructureException("unable to complete accounting line population.", e); 217 } 218 catch (NoSuchMethodException e) { 219 throw new InfrastructureException("unable to complete accounting line population.", e); 220 } 221 catch (IllegalAccessException e) { 222 throw new InfrastructureException("unable to complete accounting line population.", e); 223 } 224 catch (InvocationTargetException e) { 225 throw new InfrastructureException("unable to complete accounting line population.", e); 226 } 227 catch (InstantiationException e) { 228 throw new InfrastructureException("unable to complete accounting line population.", e); 229 } 230 231 // force input to uppercase 232 SpringContext.getBean(BusinessObjectDictionaryService.class).performForceUppercase(accountingLine); 233 accountingLine.refresh(); 234 235 return accountingLine; 236 } 237 238 /** 239 * Places fields common to both source/target accounting lines in the attribute map 240 * 241 * @param attributeValueMap 242 * @param document 243 * @param sequenceNumber 244 */ 245 protected void putCommonAttributesInMap(Map<String, String> attributeValueMap, AccountingDocument document, Integer sequenceNumber) { 246 attributeValueMap.put(KFSPropertyConstants.DOCUMENT_NUMBER, document.getDocumentNumber()); 247 attributeValueMap.put(POSTING_YEAR, document.getPostingYear().toString()); 248 attributeValueMap.put(SEQUENCE_NUMBER, sequenceNumber.toString()); 249 } 250 251 /** 252 * Parses the csv line 253 * 254 * @param accountingLineClass 255 * @param lineToParse 256 * @return Map containing accounting line attribute,value pairs 257 */ 258 protected Map<String, String> parseAccountingLine(Class<? extends AccountingLine> accountingLineClass, String lineToParse) { 259 if (StringUtils.isNotBlank(fileName) && !StringUtils.lowerCase(fileName).endsWith(".csv")) { 260 throw new AccountingLineParserException("unsupported file format: " + fileName, ERROR_INVALID_FILE_FORMAT, fileName); 261 } 262 String[] attributes = chooseFormat(accountingLineClass); 263 String[] attributeValues = StringUtils.splitPreserveAllTokens(lineToParse, ","); 264 265 Map<String, String> attributeValueMap = new HashMap<String, String>(); 266 267 for (int i = 0; i < Math.min(attributeValues.length, attributes.length); i++) { 268 attributeValueMap.put(attributes[i], attributeValues[i]); 269 } 270 271 return attributeValueMap; 272 } 273 274 /** 275 * Should be voerriden by documents to perform any additional <code>SourceAccountingLine</code> population 276 * 277 * @param attributeValueMap 278 * @param sourceAccountingLine 279 * @param accountingLineAsString 280 */ 281 protected void performCustomSourceAccountingLinePopulation(Map<String, String> attributeValueMap, SourceAccountingLine sourceAccountingLine, String accountingLineAsString) { 282 } 283 284 /** 285 * Should be overridden by documents to perform any additional <code>TargetAccountingLine</code> attribute population 286 * 287 * @param attributeValueMap 288 * @param targetAccountingLine 289 * @param accountingLineAsString 290 */ 291 protected void performCustomTargetAccountingLinePopulation(Map<String, String> attributeValueMap, TargetAccountingLine targetAccountingLine, String accountingLineAsString) { 292 } 293 294 /** 295 * Calls the appropriate parseAccountingLine method 296 * 297 * @param stream 298 * @param transactionalDocument 299 * @param isSource 300 * @return List 301 */ 302 protected List<AccountingLine> importAccountingLines(String fileName, InputStream stream, AccountingDocument transactionalDocument, boolean isSource) { 303 List<AccountingLine> importedAccountingLines = new ArrayList<AccountingLine>(); 304 this.fileName = fileName; 305 BufferedReader br = new BufferedReader(new InputStreamReader(stream)); 306 307 try { 308 String accountingLineAsString = null; 309 lineNo = 0; 310 while ((accountingLineAsString = br.readLine()) != null) { 311 lineNo++; 312 AccountingLine accountingLine = null; 313 314 try { 315 if (isSource) { 316 accountingLine = parseSourceAccountingLine(transactionalDocument, accountingLineAsString); 317 } 318 else { 319 accountingLine = parseTargetAccountingLine(transactionalDocument, accountingLineAsString); 320 } 321 322 validateImportedAccountingLine(accountingLine, accountingLineAsString); 323 importedAccountingLines.add(accountingLine); 324 } 325 catch (AccountingLineParserException e) { 326 GlobalVariables.getMessageMap().putError((isSource ? "sourceAccountingLines" : "targetAccountingLines"), KFSKeyConstants.ERROR_ACCOUNTING_DOCUMENT_ACCOUNTING_LINE_IMPORT_GENERAL, new String[] { e.getMessage() }); 327 } 328 } 329 } 330 catch (IOException e) { 331 throw new InfrastructureException("unable to readLine from bufferReader in accountingLineParserBase", e); 332 } 333 finally { 334 try { 335 br.close(); 336 } 337 catch (IOException e) { 338 throw new InfrastructureException("unable to close bufferReader in accountingLineParserBase", e); 339 } 340 } 341 342 return importedAccountingLines; 343 } 344 345 /** 346 * @see org.kuali.rice.kns.bo.AccountingLineParser#importSourceAccountingLines(java.io.InputStream, 347 * org.kuali.rice.kns.document.TransactionalDocument) 348 */ 349 public final List importSourceAccountingLines(String fileName, InputStream stream, AccountingDocument document) { 350 return importAccountingLines(fileName, stream, document, true); 351 } 352 353 /** 354 * @see org.kuali.rice.kns.bo.AccountingLineParser#importTargetAccountingLines(java.io.InputStream, 355 * org.kuali.rice.kns.document.TransactionalDocument) 356 */ 357 public final List importTargetAccountingLines(String fileName, InputStream stream, AccountingDocument document) { 358 return importAccountingLines(fileName, stream, document, false); 359 } 360 361 /** 362 * performs any additional accounting line validation 363 * 364 * @param line 365 * @param accountingLineAsString 366 * @throws AccountingLineParserException 367 */ 368 protected void validateImportedAccountingLine(AccountingLine line, String accountingLineAsString) throws AccountingLineParserException { 369 // This check isn't done for the web UI because the code is never input from the user and doesn't correspond to a displayed 370 // property that could be error highlighted. Throwing an exception here follows the convention of TooFewFieldsException 371 // and the unchecked NumberFormatException, altho todo: reconsider design, e.g., KULFDBCK-478 372 String overrideCode = line.getOverrideCode(); 373 if (!AccountingLineOverride.isValidCode(overrideCode)) { 374 String[] errorParameters = { overrideCode, retrieveAttributeLabel(line.getClass(), OVERRIDE_CODE), accountingLineAsString }; 375 throw new AccountingLineParserException("invalid overrride code '" + overrideCode + "' for:" + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters); 376 } 377 } 378 379 protected String retrieveAttributeLabel(Class clazz, String attributeName) { 380 String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(clazz, attributeName); 381 if (StringUtils.isBlank(label)) { 382 label = attributeName; 383 } 384 return label; 385 } 386 387 protected String[] chooseFormat(Class<? extends AccountingLine> accountingLineClass) { 388 String[] format = null; 389 if (SourceAccountingLine.class.isAssignableFrom(accountingLineClass)) { 390 format = getSourceAccountingLineFormat(); 391 } 392 else if (TargetAccountingLine.class.isAssignableFrom(accountingLineClass)) { 393 format = getTargetAccountingLineFormat(); 394 } 395 else { 396 throw new IllegalStateException("unknow accounting line class: " + accountingLineClass); 397 } 398 return format; 399 } 400 }