/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/calendar/trunk/calendar-impl/impl/src/java/org/sakaiproject/calendar/impl/readers/CSVReader.java $
* $Id: CSVReader.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.opensource.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
**********************************************************************************/
package org.sakaiproject.calendar.impl.readers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.sakaiproject.calendar.impl.GenericCalendarImporter;
import org.sakaiproject.exception.ImportException;
import org.sakaiproject.time.api.TimeBreakdown;
import org.sakaiproject.util.ResourceLoader;
/**
* This class parses a comma (or other separator other than a double-quote) delimited
* file.
*/
public class CSVReader extends Reader
{
private ResourceLoader rb = new ResourceLoader("calendar");
private static final String COMMENT_LINE_PREFIX = "//";
/**
* This regular expression will split separate separated values, with optionally quoted columns.
* Quote characters not used to group columns must be escaped with a backslash. The __DELIMITER__
* token is replaced with the actual character in the form of \x2c where the "2c" part is the
* character value, in this case a comma.
*/
private static final String CSV_REGEX =
"__DELIMITER__(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))";
private static final String DELIMITER_TOKEN_IN_REGEX = "__DELIMITER__";
/** Defaults to comma */
private String columnDelimiter = ",";
/**
* Default constructor
*/
public CSVReader()
{
super();
}
/**
* Import a CSV file from a stream and callback on each row.
* @param stream Stream of CSV (or other delimited data)
* @param handler Callback for each row.
*/
public void importStreamFromDelimitedFile(
InputStream stream,
ReaderImportRowHandler handler) throws ImportException
{
BufferedReader bufferedReader = getReader(stream);
ColumnHeader columnDescriptionArray[] = null;
int lineNumber = 1;
boolean readDone = false;
while (!readDone)
{
try
{
// Prepare the column map on the first line.
String lineBuffer = bufferedReader.readLine();
// See if we have exhausted the input
if (lineBuffer == null)
{
break;
}
// Skip comment or empty lines, but keep track of the line number.
if (lineBuffer.startsWith(COMMENT_LINE_PREFIX)
|| lineBuffer.trim().length() == 0)
{
lineNumber++;
continue;
}
if (columnDescriptionArray == null)
{
columnDescriptionArray =
buildColumnDescriptionArray(
parseLineFromDelimitedFile(lineBuffer));
lineNumber++;
// Immediately start the next loop, don't do any more
// processing or increment the line counter.
continue;
}
else
{
handler.handleRow(
processLine(
columnDescriptionArray,
lineNumber,
parseLineFromDelimitedFile(lineBuffer)));
}
}
catch (IOException e)
{
// We'll get an exception when we've exhauster
readDone = true;
}
// If we get this far, increment the line counter.
lineNumber++;
}
}
/**
* Form the hex string for the delimiter character(s)
*/
private String getHexStringForDelimiter()
{
StringBuilder delimiter = new StringBuilder();
for ( int i=0; i < columnDelimiter.length(); i++)
{
delimiter.append( "\\" + "x");
delimiter.append( Integer.toHexString(this.columnDelimiter.charAt(i)) );
}
return delimiter.toString().replaceAll("\\\\","\\\\\\\\");
}
/**
* Break a line's columns up into a String array. (One element for each column.)
* @param line
*/
protected String[] parseLineFromDelimitedFile(String line)
{
String[] columnsReadFromFile;
String pattern = CSV_REGEX;
pattern =
pattern.replaceAll(
DELIMITER_TOKEN_IN_REGEX,
getHexStringForDelimiter());
columnsReadFromFile = line.trim().split(pattern);
trimLeadingTrailingQuotes(columnsReadFromFile);
return columnsReadFromFile;
}
/**
* Set the delimiter
* @param string
*/
public void setColumnDelimiter(String columnDelimiter)
{
this.columnDelimiter = columnDelimiter;
}
/**
* Get the default column map for CSV files.
*/
public Map getDefaultColumnMap()
{
Map columnMap = new HashMap();
columnMap.put(GenericCalendarImporter.LOCATION_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.LOCATION_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.ITEM_TYPE_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.ITEM_TYPE_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.FREQUENCY_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.FREQUENCY_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.DURATION_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.DURATION_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.START_TIME_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.START_TIME_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.DATE_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.DATE_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.DESCRIPTION_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.DESCRIPTION_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.TITLE_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.TITLE_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.INTERVAL_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.INTERVAL_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.ENDS_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.ENDS_PROPERTY_NAME);
columnMap.put(GenericCalendarImporter.REPEAT_DEFAULT_COLUMN_HEADER, GenericCalendarImporter.REPEAT_PROPERTY_NAME);
return columnMap;
}
/* (non-Javadoc)
* @see org.sakaiproject.tool.calendar.schedimportreaders.Reader#filterEvents(java.util.List, java.lang.String[])
*/
public List filterEvents(List events, String[] customFieldNames) throws ImportException
{
setColumnDelimiter(",");
Map augmentedMapping = getDefaultColumnMap();
// Add custom fields.
if ( customFieldNames != null )
{
for ( int i=0; i < customFieldNames.length; i++ )
{
augmentedMapping.put(customFieldNames[i], customFieldNames[i]);
}
}
// Use the default mappings
setColumnHeaderToAtributeMapping(augmentedMapping);
Iterator it = events.iterator();
int lineNumber = 2; // The headers are (or should be) on line 1.
//
// Convert the date/time fields as they appear in the Outlook import to
// be a synthesized start/end timerange.
//
while ( it.hasNext() )
{
Map eventProperties = (Map)it.next();
Date startTime = (Date) eventProperties.get(GenericCalendarImporter.START_TIME_PROPERTY_NAME);
TimeBreakdown startTimeBreakdown = null;
if ( startTime != null )
{
// if the source time zone were known, this would be
// a good place to set it: startCal.setTimeZone()
GregorianCalendar startCal = new GregorianCalendar();
startCal.setTimeInMillis( startTime.getTime() );
startTimeBreakdown =
getTimeService().newTimeBreakdown( 0, 0, 0,
startCal.get(Calendar.HOUR_OF_DAY),
startCal.get(Calendar.MINUTE),
startCal.get(Calendar.SECOND),
0 );
}
Integer durationInMinutes = (Integer)eventProperties.get(GenericCalendarImporter.DURATION_PROPERTY_NAME);
if ( durationInMinutes == null )
{
Integer line = Integer.valueOf(lineNumber);
String msg = (String)rb.getFormattedMessage("err_no_dur",
new Object[]{line});
throw new ImportException( msg );
}
Date endTime =
new Date(
startTime.getTime()
+ (durationInMinutes.longValue() * 60 * 1000));
TimeBreakdown endTimeBreakdown = null;
if ( endTime != null )
{
// if the source time zone were known, this would be
// a good place to set it: endCal.setTimeZone()
GregorianCalendar endCal = new GregorianCalendar();
endCal.setTimeInMillis( endTime.getTime() );
endTimeBreakdown =
getTimeService().newTimeBreakdown( 0, 0, 0,
endCal.get(Calendar.HOUR_OF_DAY),
endCal.get(Calendar.MINUTE),
endCal.get(Calendar.SECOND),
0 );
}
Date startDate = (Date) eventProperties.get(GenericCalendarImporter.DATE_PROPERTY_NAME);
TimeBreakdown startDateBreakdown = null;
if ( startDate != null )
{
// if the source time zone were known, this would be
// a good place to set it: endCal.setTimeZone()
GregorianCalendar startCal = new GregorianCalendar();
startCal.setTimeInMillis( startDate.getTime() );
startTimeBreakdown.setYear( startCal.get(Calendar.YEAR) );
startTimeBreakdown.setMonth( startCal.get(Calendar.MONTH)+1 );
startTimeBreakdown.setDay( startCal.get(Calendar.DAY_OF_MONTH) );
endTimeBreakdown.setYear( startCal.get(Calendar.YEAR) );
endTimeBreakdown.setMonth( startCal.get(Calendar.MONTH)+1 );
endTimeBreakdown.setDay( startCal.get(Calendar.DAY_OF_MONTH) );
}
else
{
Integer line = Integer.valueOf(lineNumber);
String msg = (String)rb.getFormattedMessage("err_no_start",
new Object[]{line});
throw new ImportException( msg );
}
eventProperties.put(
GenericCalendarImporter.ACTUAL_TIMERANGE,
getTimeService().newTimeRange(
getTimeService().newTimeLocal(startTimeBreakdown),
getTimeService().newTimeLocal(endTimeBreakdown),
true,
false));
lineNumber++;
}
return events;
}
}