/*
* The Kuali Financial System, a comprehensive financial management system for higher education.
*
* Copyright 2005-2014 The Kuali Foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kuali.kfs.sys.batch;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.kuali.kfs.sys.exception.ParseException;
import au.com.bytecode.opencsv.CSVReader;
/**
* Base class for BatchInputFileType implementations that validate using an Enum class (use as the CSV file header)
* and parse using CSV comma delimited
*/
public abstract class CsvBatchInputFileTypeBase<CSVEnum extends Enum<CSVEnum>> extends BatchInputFileTypeBase {
private static final Logger LOG = Logger.getLogger(CsvBatchInputFileTypeBase.class);
private Class<?> csvEnumClass;
/**
* Constructs a BatchInputFileTypeBase.java.
*/
public CsvBatchInputFileTypeBase() {
super();
}
public void setCsvEnumClass(Class<?> csvEnumClass) {
this.csvEnumClass = csvEnumClass;
}
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#process(java.lang.String, java.lang.Object)
*/
public void process(String fileName, Object parsedFileContents) {
// default impl does nothing
}
/**
* @see org.kuali.kfs.sys.batch.BatchInputFileType#parse(byte[])
*
* @return parsed object in structure - List<Map<String, String>>
*/
public Object parse(byte[] fileByteContent) throws ParseException {
// handle null objects and zero byte contents
String errorMessage = fileByteContent == null? "an invalid(null) argument was given" :
fileByteContent.length == 0? "an invalid argument was given, empty input stream" : "";
if (!errorMessage.isEmpty()){
LOG.error(errorMessage);
throw new IllegalArgumentException(errorMessage);
}
List<String> headerList = getCsvHeaderList();
Object parsedContents = null;
try {
// validate csv header
ByteArrayInputStream validateFileContents = new ByteArrayInputStream(fileByteContent);
validateCSVFileInput(headerList, validateFileContents);
//use csv reader to parse the csv content
CSVReader csvReader = new CSVReader(new InputStreamReader(new ByteArrayInputStream(fileByteContent)));
List<String[]>dataList = csvReader.readAll();
//remove first header line
dataList.remove(0);
//parse and create List of Maps base on enum value names as map keys
List<Map<String, String>> dataMapList = new ArrayList<Map<String, String>>();
Map<String, String>rowMap;
int index = 0;
for (String[] row : dataList){
rowMap = new LinkedHashMap<String, String>();
// reset index
index = 0;
for (String header : headerList){
rowMap.put(header, row[index++]);
}
dataMapList.add(rowMap);
}
parsedContents = dataMapList;
}catch (IOException ex) {
LOG.error(ex.getMessage(), ex);
throw new ParseException(ex.getMessage(), ex);
}
return parsedContents;
}
/**
* Validates the CSV file content against the CSVEnum items as header
* 1. content header must match CSVEnum order/value
* 2. each data row should have same size as the header
*
* @param expectedHeaderList expected CSV header String list
* @param fileContents contents to validate
*
* @throws IOException
*/
private void validateCSVFileInput(final List<String> expectedHeaderList, InputStream fileContents) throws IOException {
//use csv reader to parse the csv content
CSVReader csvReader = new CSVReader(new InputStreamReader(fileContents));
List<String> inputHeaderList = Arrays.asList(csvReader.readNext());
String errorMessage = null;
// validate
if (!CollectionUtils.isEqualCollection(expectedHeaderList, inputHeaderList)){
errorMessage = "CSV Batch Input File contains incorrect number of headers";
//collection has same elements, now check the exact content orders by looking at the toString comparisons
}else if (!expectedHeaderList.equals(inputHeaderList)){
errorMessage = "CSV Batch Input File headers are different";
}else {
//check the content size as well if headers are validated
int line = 1;
List<String> inputDataList = null;
while ((inputDataList = Arrays.asList(csvReader.readNext())) != null && errorMessage != null){
//if the data list size does not match header list (its missing data)
if (inputDataList.size() != expectedHeaderList.size()){
errorMessage = "line " + line + " layout does not match the header";
}
line++;
}
}
if (errorMessage != null){
LOG.error(errorMessage);
throw new RuntimeException(errorMessage);
}
}
/**
* build the csv header list base on provided csv enum class
*
* @return
*/
@SuppressWarnings("rawtypes")
protected List<String> getCsvHeaderList(){
List<String> headerList = new ArrayList<String>();
EnumSet<CSVEnum> enums = EnumSet.allOf((Class)csvEnumClass);
for (Enum<CSVEnum> e : enums ){
headerList.add(e.name());
}
return headerList;
}
/**
* convert the parsed content into VOs
*
* @param parsedContent
* @return
*/
abstract protected Object convertParsedObjectToVO(Object parsedContent);
}