/********************************************************************************** * $URL$ * $Id$ *********************************************************************************** * * Copyright (c) 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.InputStream; import java.text.DateFormat; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.model.Component; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.property.DateProperty; import net.fortuna.ical4j.util.CompatibilityHints; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; 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 an import file from iCalendar. */ public class IcalendarReader extends Reader { private ResourceLoader rb = new ResourceLoader("calendar"); private static Log M_log = LogFactory.getLog(IcalendarReader.class); private static final String CONTACT_SECTION_HEADER = "Contacts"; private static final String TODO_SECTION_HEADER = "Todos"; private static final String EVENT_SECTION_HEADER = "Events"; public static final String TITLE_HEADER = "Summary"; public static final String LOCATION_HEADER = "Location"; public static final String DATE_HEADER = "Start Date"; public static final String START_TIME_HEADER = "Start Time"; public static final String DURATION_HEADER = "Duration"; public static final String ITEM_HEADER = "Type"; public static final String DESCRIPTION_HEADER = "Description"; /** * Default constructor */ public IcalendarReader() { super(); } /* (non-Javadoc) * @see org.sakaiproject.tool.calendar.ImportReader#importStreamFromDelimitedFile(java.io.InputStream, org.sakaiproject.tool.calendar.ImportReader.ReaderImportRowHandler) */ public void importStreamFromDelimitedFile( InputStream stream, ReaderImportRowHandler handler) throws ImportException//, IOException, ParserException { try { ColumnHeader columnDescriptionArray[] = null; String descriptionColumns[] = {"Summary","Description","Start Date","Start Time","Duration","Location"}; int lineNumber = 1; String durationformat =""; String requireValues = ""; // column map stuff trimLeadingTrailingQuotes(descriptionColumns); columnDescriptionArray = buildColumnDescriptionArray(descriptionColumns); // enable "relaxed parsing"; read file using LF instead of CRLF CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_UNFOLDING, true); CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_PARSING, true); CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_OUTLOOK_COMPATIBILITY, true); CompatibilityHints.setHintEnabled(CompatibilityHints.KEY_RELAXED_VALIDATION, true); CalendarBuilder builder = new CalendarBuilder(); net.fortuna.ical4j.model.Calendar calendar = builder.build(stream); for (Iterator i = calendar.getComponents("VEVENT").iterator(); i.hasNext();) { Component component = (Component) i.next(); // Find event duration DateProperty dtstartdate; DateProperty dtenddate; if(component instanceof VEvent){ VEvent vEvent = (VEvent) component; dtstartdate = vEvent.getStartDate(); dtenddate = vEvent.getEndDate(true); }else{ dtstartdate = (DateProperty) component.getProperty("DTSTART"); dtenddate = (DateProperty) component.getProperty("DTEND"); } if ( component.getProperty("SUMMARY") == null ) { M_log.warn("IcalendarReader: SUMMARY is required; event not imported"); continue; } String summary = component.getProperty("SUMMARY").getValue(); if ( component.getProperty("RRULE") != null ) { M_log.warn("IcalendarReader: Re-occuring events not supported: " + summary ); continue; } else if (dtstartdate == null || dtenddate == null ) { M_log.warn("IcalendarReader: DTSTART/DTEND required: " + summary ); continue; } int durationsecs = (int) ((dtenddate.getDate().getTime() - dtstartdate.getDate().getTime()) / 1000); int durationminutes = (durationsecs/60) % 60; int durationhours = (durationsecs/(60*60)) % 24; // Put duration in proper format (hh:mm or mm) if less than 1 hour if (durationminutes < 10) { durationformat = "0"+durationminutes; } else { durationformat = ""+durationminutes; } if (durationhours != 0) { durationformat = durationhours+":"+durationformat; } String description = ""; if ( component.getProperty("DESCRIPTION") != null ) description = component.getProperty("DESCRIPTION").getValue(); String location = ""; if (component.getProperty("LOCATION") != null) location = component.getProperty("LOCATION").getValue(); String columns[] = {component.getProperty("SUMMARY").getValue(), description, DateFormat.getDateInstance(DateFormat.SHORT, rb.getLocale()).format(dtstartdate.getDate()), DateFormat.getTimeInstance(DateFormat.SHORT, rb.getLocale()).format(dtstartdate.getDate()), durationformat, location}; // Remove trailing/leading quotes from all columns. //trimLeadingTrailingQuotes(columns); handler.handleRow( processLine( columnDescriptionArray, lineNumber, columns)); lineNumber++; } // end for } catch (Exception e) { M_log.warn(".importSteamFromDelimitedFile(): ", e); } } // end importStreamFromDelimitedFile /* (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 { Iterator it = events.iterator(); int lineNumber = 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 ); } else { Integer line = Integer.valueOf(lineNumber); String msg = (String)rb.getFormattedMessage("err_no_stime_on", new Object[]{line}); throw new ImportException( msg ); } Integer durationInMinutes = (Integer)eventProperties.get(GenericCalendarImporter.DURATION_PROPERTY_NAME); if ( durationInMinutes == null ) { Integer line = Integer.valueOf(lineNumber); String msg = (String)rb.getFormattedMessage("err_no_dtime_on", 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); // if the source time zone were known, this would be // a good place to set it: startCal.setTimeZone() GregorianCalendar startCal = new GregorianCalendar(); if ( startDate != null ) 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) ); eventProperties.put( GenericCalendarImporter.ACTUAL_TIMERANGE, getTimeService().newTimeRange( getTimeService().newTimeLocal(startTimeBreakdown), getTimeService().newTimeLocal(endTimeBreakdown), true, false)); lineNumber++; } return events; } /* (non-Javadoc) * @see org.sakaiproject.tool.calendar.schedimportreaders.Reader#getDefaultColumnMap() */ public Map getDefaultColumnMap() { Map columnHeaderMap = new HashMap(); columnHeaderMap.put(TITLE_HEADER, GenericCalendarImporter.TITLE_PROPERTY_NAME); columnHeaderMap.put(DESCRIPTION_HEADER, GenericCalendarImporter.DESCRIPTION_PROPERTY_NAME); columnHeaderMap.put(DATE_HEADER, GenericCalendarImporter.DATE_PROPERTY_NAME); columnHeaderMap.put(START_TIME_HEADER, GenericCalendarImporter.START_TIME_PROPERTY_NAME); columnHeaderMap.put(DURATION_HEADER, GenericCalendarImporter.DURATION_PROPERTY_NAME); //columnHeaderMap.put(ITEM_HEADER, GenericCalendarImporter.ITEM_TYPE_PROPERTY_NAME); columnHeaderMap.put(LOCATION_HEADER, GenericCalendarImporter.LOCATION_PROPERTY_NAME); return columnHeaderMap; } }