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