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 }