View Javadoc
1   /*
2    * Copyright 2007 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl2.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.kuali.ole.sys.businessobject;
18  
19  import static org.kuali.ole.sys.OLEKeyConstants.AccountingLineParser.ERROR_INVALID_FILE_FORMAT;
20  import static org.kuali.ole.sys.OLEKeyConstants.AccountingLineParser.ERROR_INVALID_PROPERTY_VALUE;
21  import static org.kuali.ole.sys.OLEPropertyConstants.ACCOUNT_NUMBER;
22  import static org.kuali.ole.sys.OLEPropertyConstants.AMOUNT;
23  import static org.kuali.ole.sys.OLEPropertyConstants.CHART_OF_ACCOUNTS_CODE;
24  import static org.kuali.ole.sys.OLEPropertyConstants.FINANCIAL_OBJECT_CODE;
25  import static org.kuali.ole.sys.OLEPropertyConstants.FINANCIAL_SUB_OBJECT_CODE;
26  import static org.kuali.ole.sys.OLEPropertyConstants.ORGANIZATION_REFERENCE_ID;
27  import static org.kuali.ole.sys.OLEPropertyConstants.OVERRIDE_CODE;
28  import static org.kuali.ole.sys.OLEPropertyConstants.POSTING_YEAR;
29  import static org.kuali.ole.sys.OLEPropertyConstants.PROJECT_CODE;
30  import static org.kuali.ole.sys.OLEPropertyConstants.SEQUENCE_NUMBER;
31  import static org.kuali.ole.sys.OLEPropertyConstants.SUB_ACCOUNT_NUMBER;
32  
33  import java.io.BufferedReader;
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.io.InputStreamReader;
37  import java.lang.reflect.InvocationTargetException;
38  import java.util.ArrayList;
39  import java.util.HashMap;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Map.Entry;
43  
44  import org.apache.commons.lang.StringUtils;
45  import org.kuali.ole.coa.service.AccountService;
46  import org.kuali.ole.sys.OLEConstants;
47  import org.kuali.ole.sys.OLEKeyConstants;
48  import org.kuali.ole.sys.OLEPropertyConstants;
49  import org.kuali.ole.sys.context.SpringContext;
50  import org.kuali.ole.sys.document.AccountingDocument;
51  import org.kuali.ole.sys.exception.AccountingLineParserException;
52  import org.kuali.rice.core.web.format.FormatException;
53  import org.kuali.rice.kns.service.BusinessObjectDictionaryService;
54  import org.kuali.rice.kns.service.DataDictionaryService;
55  import org.kuali.rice.krad.util.GlobalVariables;
56  import org.kuali.rice.krad.util.ObjectUtils;
57  
58  /**
59   * Base class for parsing serialized <code>AccountingLine</code>s for <code>TransactionalDocument</code>s
60   */
61  public class AccountingLineParserBase implements AccountingLineParser {
62      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 };
63      private String fileName;
64      private Integer lineNo = 0;
65  
66      /**
67       * @see org.kuali.rice.krad.bo.AccountingLineParser#getSourceAccountingLineFormat()
68       */
69      public String[] getSourceAccountingLineFormat() {
70          return removeChartFromFormatIfNeeded(DEFAULT_FORMAT);
71      }
72  
73      /**
74       * @see org.kuali.rice.krad.bo.AccountingLineParser#getTargetAccountingLineFormat()
75       */
76      public String[] getTargetAccountingLineFormat() {
77          return removeChartFromFormatIfNeeded(DEFAULT_FORMAT);
78      }
79  
80      /**
81       * If accounts can cross charts, returns the given format; 
82       * otherwise returns the format with ChartOfAccountsCode field removed.
83       */
84      public String[] removeChartFromFormatIfNeeded(String[] format) {
85          if (SpringContext.getBean(AccountService.class).accountsCanCrossCharts()) {
86              return format;
87          }
88          
89          // if accounts can't cross charts, exclude ChartOfAccountsCode field from the format
90          String[] formatNoChart = new String[format.length-1];
91          int idx = 0;
92          for (int i=0; i<format.length; i++) {
93              if (format[i].equals(CHART_OF_ACCOUNTS_CODE)) 
94                  continue;
95              else {
96                  formatNoChart[idx] = format[i];
97                  idx++;
98              }
99          }
100         return formatNoChart;
101     }
102     
103     /**
104      * @see org.kuali.rice.krad.bo.AccountingLineParser#getExpectedAccountingLineFormatAsString(java.lang.Class)
105      */
106     public String getExpectedAccountingLineFormatAsString(Class<? extends AccountingLine> accountingLineClass) {
107         StringBuffer sb = new StringBuffer();
108         boolean first = true;
109         for (String attributeName : chooseFormat(accountingLineClass)) {
110             if (!first) {
111                 sb.append(",");
112             }
113             else {
114                 first = false;
115             }
116             sb.append(retrieveAttributeLabel(accountingLineClass, attributeName));
117         }
118         return sb.toString();
119     }
120 
121     /**
122      * @see org.kuali.rice.krad.bo.AccountingLineParser#parseSourceAccountingLine(org.kuali.rice.krad.document.TransactionalDocument,
123      *      java.lang.String)
124      */
125     public SourceAccountingLine parseSourceAccountingLine(AccountingDocument transactionalDocument, String sourceAccountingLineString) {
126         Class sourceAccountingLineClass = getSourceAccountingLineClass(transactionalDocument);
127         SourceAccountingLine sourceAccountingLine = (SourceAccountingLine) populateAccountingLine(transactionalDocument, sourceAccountingLineClass, sourceAccountingLineString, parseAccountingLine(sourceAccountingLineClass, sourceAccountingLineString), transactionalDocument.getNextSourceLineNumber());
128         return sourceAccountingLine;
129     }
130     
131     /**
132      * Given a document, determines what class the source lines of that document uses
133      * @param accountingDocument the document to find the class of the source lines for
134      * @return the class of the source lines
135      */
136     protected Class getSourceAccountingLineClass(final AccountingDocument accountingDocument) {
137         return accountingDocument.getSourceAccountingLineClass();
138     }
139 
140     /**
141      * @see org.kuali.rice.krad.bo.AccountingLineParser#parseTargetAccountingLine(org.kuali.rice.krad.document.TransactionalDocument,
142      *      java.lang.String)
143      */
144     public TargetAccountingLine parseTargetAccountingLine(AccountingDocument transactionalDocument, String targetAccountingLineString) {
145         Class targetAccountingLineClass = getTargetAccountingLineClass(transactionalDocument);
146         TargetAccountingLine targetAccountingLine = (TargetAccountingLine) populateAccountingLine(transactionalDocument, targetAccountingLineClass, targetAccountingLineString, parseAccountingLine(targetAccountingLineClass, targetAccountingLineString), transactionalDocument.getNextTargetLineNumber());
147         return targetAccountingLine;
148     }
149     
150     /**
151      * Given a document, determines what class that document uses for target accounting lines
152      * @param accountingDocument the document to determine the target accounting line class for
153      * @return the class of the target lines for the given document
154      */
155     protected Class getTargetAccountingLineClass(final AccountingDocument accountingDocument) {
156         return accountingDocument.getTargetAccountingLineClass();
157     }
158 
159     /**
160      * Populates a source/target line with values
161      * 
162      * @param transactionalDocument
163      * @param accountingLineClass
164      * @param accountingLineAsString
165      * @param attributeValueMap
166      * @param sequenceNumber
167      * @return AccountingLine
168      */
169     protected AccountingLine populateAccountingLine(AccountingDocument transactionalDocument, Class<? extends AccountingLine> accountingLineClass, String accountingLineAsString, Map<String, String> attributeValueMap, Integer sequenceNumber) {
170 
171         putCommonAttributesInMap(attributeValueMap, transactionalDocument, sequenceNumber);
172 
173         // create line and populate fields
174         AccountingLine accountingLine;
175 
176         try {
177             accountingLine = (AccountingLine) accountingLineClass.newInstance();
178             
179             // perform custom line population
180             if (SourceAccountingLine.class.isAssignableFrom(accountingLineClass)) {
181                 performCustomSourceAccountingLinePopulation(attributeValueMap, (SourceAccountingLine) accountingLine, accountingLineAsString);
182             }
183             else if (TargetAccountingLine.class.isAssignableFrom(accountingLineClass)) {
184                 performCustomTargetAccountingLinePopulation(attributeValueMap, (TargetAccountingLine) accountingLine, accountingLineAsString);
185             }
186             else {
187                 throw new IllegalArgumentException("invalid (unknown) accounting line type: " + accountingLineClass);
188             }
189             
190             for (Entry<String, String> entry : attributeValueMap.entrySet()) {
191                 try {
192                     try {
193                         Class entryType = ObjectUtils.easyGetPropertyType(accountingLine, entry.getKey());
194                         if (String.class.isAssignableFrom(entryType)) {
195                             entry.setValue(entry.getValue().toUpperCase());
196                         }
197                         ObjectUtils.setObjectProperty(accountingLine, entry.getKey(), entryType, entry.getValue());
198                     }
199                     catch (IllegalArgumentException e) {
200                         throw new IllegalArgumentException("unable to complete accounting line population.", e);
201                     }
202                 }
203                 catch (FormatException e) {
204                     String[] errorParameters = { entry.getValue().toString(), retrieveAttributeLabel(accountingLine.getClass(), entry.getKey()), accountingLineAsString };
205                     // KULLAB-408
206                     GlobalVariables.getMessageMap().putError(OLEConstants.ACCOUNTING_LINE_ERRORS, ERROR_INVALID_PROPERTY_VALUE, entry.getValue().toString(), entry.getKey(), accountingLineAsString + "  : Line Number " + lineNo.toString());
207                     throw new AccountingLineParserException("invalid '" + entry.getKey() + "=" + entry.getValue() + "for " + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters);
208                 }
209             }
210             
211             // override chart code if accounts can't cross charts
212             SpringContext.getBean(AccountService.class).populateAccountingLineChartIfNeeded(accountingLine);            
213         }
214         catch (SecurityException e) {
215             throw new IllegalArgumentException("unable to complete accounting line population.", e);
216         }
217         catch (NoSuchMethodException e) {
218             throw new IllegalArgumentException("unable to complete accounting line population.", e);
219         }
220         catch (IllegalAccessException e) {
221             throw new IllegalArgumentException("unable to complete accounting line population.", e);
222         }
223         catch (InvocationTargetException e) {
224             throw new IllegalArgumentException("unable to complete accounting line population.", e);
225         }
226         catch (InstantiationException e) {
227             throw new IllegalArgumentException("unable to complete accounting line population.", e);
228         }
229 
230         // force input to uppercase
231         SpringContext.getBean(BusinessObjectDictionaryService.class).performForceUppercase(accountingLine);
232         accountingLine.refresh();
233 
234         return accountingLine;
235     }
236 
237     /**
238      * Places fields common to both source/target accounting lines in the attribute map
239      * 
240      * @param attributeValueMap
241      * @param document
242      * @param sequenceNumber
243      */
244     protected void putCommonAttributesInMap(Map<String, String> attributeValueMap, AccountingDocument document, Integer sequenceNumber) {
245         attributeValueMap.put(OLEPropertyConstants.DOCUMENT_NUMBER, document.getDocumentNumber());
246         attributeValueMap.put(POSTING_YEAR, document.getPostingYear().toString());
247         attributeValueMap.put(SEQUENCE_NUMBER, sequenceNumber.toString());
248     }
249 
250     /**
251      * Parses the csv line
252      * 
253      * @param accountingLineClass
254      * @param lineToParse
255      * @return Map containing accounting line attribute,value pairs
256      */
257     protected Map<String, String> parseAccountingLine(Class<? extends AccountingLine> accountingLineClass, String lineToParse) {
258         if (StringUtils.isNotBlank(fileName) && !StringUtils.lowerCase(fileName).endsWith(".csv")) {
259             throw new AccountingLineParserException("unsupported file format: " + fileName, ERROR_INVALID_FILE_FORMAT, fileName);
260         }
261         String[] attributes = chooseFormat(accountingLineClass);
262         String[] attributeValues = StringUtils.splitPreserveAllTokens(lineToParse, ",");
263 
264         Map<String, String> attributeValueMap = new HashMap<String, String>();
265 
266         for (int i = 0; i < Math.min(attributeValues.length, attributes.length); i++) {
267             attributeValueMap.put(attributes[i], attributeValues[i]);
268         }
269 
270         return attributeValueMap;
271     }
272 
273     /**
274      * Should be voerriden by documents to perform any additional <code>SourceAccountingLine</code> population
275      * 
276      * @param attributeValueMap
277      * @param sourceAccountingLine
278      * @param accountingLineAsString
279      */
280     protected void performCustomSourceAccountingLinePopulation(Map<String, String> attributeValueMap, SourceAccountingLine sourceAccountingLine, String accountingLineAsString) {
281     }
282 
283     /**
284      * Should be overridden by documents to perform any additional <code>TargetAccountingLine</code> attribute population
285      * 
286      * @param attributeValueMap
287      * @param targetAccountingLine
288      * @param accountingLineAsString
289      */
290     protected void performCustomTargetAccountingLinePopulation(Map<String, String> attributeValueMap, TargetAccountingLine targetAccountingLine, String accountingLineAsString) {
291     }
292 
293     /**
294      * Calls the appropriate parseAccountingLine method
295      * 
296      * @param stream
297      * @param transactionalDocument
298      * @param isSource
299      * @return List
300      */
301     protected List<AccountingLine> importAccountingLines(String fileName, InputStream stream, AccountingDocument transactionalDocument, boolean isSource) {
302         List<AccountingLine> importedAccountingLines = new ArrayList<AccountingLine>();
303         this.fileName = fileName;
304         BufferedReader br = new BufferedReader(new InputStreamReader(stream));
305 
306         try {
307             String accountingLineAsString = null;
308             lineNo = 0;
309             while ((accountingLineAsString = br.readLine()) != null) {
310                 lineNo++;
311                 
312                 if (StringUtils.isBlank(StringUtils.remove(StringUtils.deleteWhitespace(accountingLineAsString),OLEConstants.COMMA))) {
313                     continue;
314                 }
315                 
316                 AccountingLine accountingLine = null;
317 
318                 try {
319                     if (isSource) {
320                         accountingLine = parseSourceAccountingLine(transactionalDocument, accountingLineAsString);
321                     }
322                     else {
323                         accountingLine = parseTargetAccountingLine(transactionalDocument, accountingLineAsString);
324                     }
325 
326                     validateImportedAccountingLine(accountingLine, accountingLineAsString);
327                     importedAccountingLines.add(accountingLine);
328                 }
329                 catch (AccountingLineParserException e) {
330                     GlobalVariables.getMessageMap().putError((isSource ? "sourceAccountingLines" : "targetAccountingLines"), OLEKeyConstants.ERROR_ACCOUNTING_DOCUMENT_ACCOUNTING_LINE_IMPORT_GENERAL, new String[] { e.getMessage() });
331                 }
332             }
333         }
334         catch (IOException e) {
335             throw new IllegalArgumentException("unable to readLine from bufferReader in accountingLineParserBase", e);
336         }
337         finally {
338             try {
339                 br.close();
340             }
341             catch (IOException e) {
342                 throw new IllegalArgumentException("unable to close bufferReader in accountingLineParserBase", e);
343             }
344         }
345 
346         return importedAccountingLines;
347     }
348 
349     /**
350      * @see org.kuali.rice.krad.bo.AccountingLineParser#importSourceAccountingLines(java.io.InputStream,
351      *      org.kuali.rice.krad.document.TransactionalDocument)
352      */
353     public final List importSourceAccountingLines(String fileName, InputStream stream, AccountingDocument document) {
354         return importAccountingLines(fileName, stream, document, true);
355     }
356 
357     /**
358      * @see org.kuali.rice.krad.bo.AccountingLineParser#importTargetAccountingLines(java.io.InputStream,
359      *      org.kuali.rice.krad.document.TransactionalDocument)
360      */
361     public final List importTargetAccountingLines(String fileName, InputStream stream, AccountingDocument document) {
362         return importAccountingLines(fileName, stream, document, false);
363     }
364 
365     /**
366      * performs any additional accounting line validation
367      * 
368      * @param line
369      * @param accountingLineAsString
370      * @throws AccountingLineParserException
371      */
372     protected void validateImportedAccountingLine(AccountingLine line, String accountingLineAsString) throws AccountingLineParserException {
373         // 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
374         // property that could be error highlighted. Throwing an exception here follows the convention of TooFewFieldsException
375         // and the unchecked NumberFormatException, altho todo: reconsider design, e.g., KULFDBCK-478
376         String overrideCode = line.getOverrideCode();
377         if (!AccountingLineOverride.isValidCode(overrideCode)) {
378             String[] errorParameters = { overrideCode, retrieveAttributeLabel(line.getClass(), OVERRIDE_CODE), accountingLineAsString };
379             throw new AccountingLineParserException("invalid overrride code '" + overrideCode + "' for:" + accountingLineAsString, ERROR_INVALID_PROPERTY_VALUE, errorParameters);
380         }
381     }
382 
383     protected String retrieveAttributeLabel(Class clazz, String attributeName) {
384         String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(clazz, attributeName);
385         if (StringUtils.isBlank(label)) {
386             label = attributeName;
387         }
388         return label;
389     }
390 
391     protected String[] chooseFormat(Class<? extends AccountingLine> accountingLineClass) {
392         String[] format = null;
393         if (SourceAccountingLine.class.isAssignableFrom(accountingLineClass)) {
394             format = getSourceAccountingLineFormat();
395         }
396         else if (TargetAccountingLine.class.isAssignableFrom(accountingLineClass)) {
397             format = getTargetAccountingLineFormat();
398         }
399         else {
400             throw new IllegalStateException("unknow accounting line class: " + accountingLineClass);
401         }
402         return format;
403     }
404 }