/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2006 - 2007 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 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 or write to
* the Free Software Foundation, Inc., 51 Franklin Street
, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package com.funambol.common.pim.model.converter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.funambol.common.pim.model.calendar.CalendarContent;
import com.funambol.common.pim.model.calendar.RecurrencePatternException;
import com.funambol.common.pim.model.calendar.ExceptionToRecurrenceRule;
import com.funambol.common.pim.model.calendar.Event;
import com.funambol.common.pim.model.calendar.RecurrencePattern;
import com.funambol.common.pim.model.calendar.Reminder;
import com.funambol.common.pim.model.calendar.Task;
import com.funambol.common.pim.model.common.Property;
import com.funambol.common.pim.model.common.PropertyWithTimeZone;
import com.funambol.common.pim.model.model.Parameter;
import com.funambol.common.pim.model.model.VAlarm;
import com.funambol.common.pim.model.model.VCalendarContent;
import com.funambol.common.pim.model.model.VEvent;
import com.funambol.common.pim.model.model.VTodo;
import com.funambol.common.pim.model.utility.TimeUtils;
import java.util.GregorianCalendar;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* This object is a converter from CalendarContent (Event or Task) to VCalendar
* and from VCalendar to CalendarContent.
*
* @see Converter
* @version $Id: VCalendarContentConverter.java,v 1.23 2008-08-29 12:21:02 mauro Exp $
*/
public class VCalendarContentConverter extends VCalendarConverter {
private TimeZone dtStartTimeZone;
private TimeZone dtEndTimeZone;
private TimeZone reminderTimeZone;
//---------------------------------------------------------------- Constants
private static final short SENSITIVITY_NORMAL = 0 ;
private static final short SENSITIVITY_PERSONAL = 1 ;
private static final short SENSITIVITY_PRIVATE = 2 ;
private static final short SENSITIVITY_CONFIDENTIAL = 3 ;
private static final String CLASS_PUBLIC = "PUBLIC" ;
private static final String CLASS_PRIVATE = "PRIVATE" ;
private static final String CLASS_CONFIDENTIAL = "CONFIDENTIAL";
private static final String CLASS_CUSTOM = "X-PERSONAL" ;
private static final short BUSYSTATUS_OLFREE = 0 ;
private static final short BUSYSTATUS_OLTENTATIVE = 1 ;
private static final short BUSYSTATUS_OLBUSY = 2 ;
private static final short BUSYSTATUS_OLOOF = 3 ;
private static final String BUSYSTATUS_FREE = "FREE" ;
private static final String BUSYSTATUS_TENTATIVE = "TENTATIVE" ;
private static final String BUSYSTATUS_BUSY = "BUSY" ;
private static final String BUSYSTATUS_OOF = "OOF" ;
private static final String[] WEEK = {
"SU", // NB: Sunday needs be the first day of the week for the mask
"MO", // composition algorithm to work
"TU",
"WE",
"TH",
"FR",
"SA"
};
private static final String DAILY = "DAILY";
private static final String WEEKLY = "WEEKLY";
private static final String MONTHLY = "MONTHLY";
private static final String YEARLY = "YEARLY";
private static final String BYMONTH = "BYMONTH";
private static final String BYDAY = "BYDAY";
private static final String BYMONTHDAY = "BYMONTHDAY";
private static final String INTERVAL = "INTERVAL";
private static final String COUNT = "COUNT";
private static final String UNTIL = "UNTIL";
private static final String BYSETPOS = "BYSETPOS";
private static final String FREQ = "FREQ";
private final static Parameter DATETIME_PARAMETER =
new Parameter("VALUE", "DATE-TIME");
private final static String ZERO = String.valueOf(RecurrencePattern.UNSPECIFIED);
private final static long ONE_YEAR = 31622400000L; // 366 days
private final static long ONE_DAY = 86400000L; // 24 hours
private final long DEFAULT_FROM = TimeZoneHelper.getReferenceTime() - ONE_YEAR;
// 1 year ago
private final long DEFAULT_TO = TimeZoneHelper.getReferenceTime() + (ONE_YEAR * 2);
// 2 years in the future
private final long DEFAULT_TO_UNLIMITED = DEFAULT_TO + (ONE_YEAR * 2);
// 4 years in the future
//-------------------------------------------------------------- Constructor
/**
* This constructor is deprecated because to handle the date is need to know
* timezone but also if the dates must be converted in local time.
*
* @param timezone
* @param charset
* @deprecated forceClientLocalTime argument should also be specified
*/
@Deprecated
public VCalendarContentConverter(TimeZone timezone, String charset) {
super(timezone, charset);
}
/**
*
* @param timezone the timezone to use in the conversion
* @param charset the charset
* @param forceClientLocalTime true if the date must be converted in the
* client local time, false otherwise.
*/
public VCalendarContentConverter(TimeZone timezone, String charset, boolean forceClientLocalTime) {
super(timezone, charset, forceClientLocalTime);
}
//----------------------------------------------------------- Public Methods
public VCalendarContent cc2vcc(CalendarContent cc)
throws ConverterException {
return cc2vcc(cc, false); // default: text/calendar (2.0)
}
/**
* Performs the CalendarContent-to-VCalendarContent conversion.
*
* @param cc the CalendarContent object to be converted
* @param xv true if the VCalendarContent must be in text/x-vcalendar format
* @return a VCalendarContent object
* @throws com.funambol.common.pim.converter.ConverterException
*/
public VCalendarContent cc2vcc(CalendarContent cc, boolean xv)
throws ConverterException {
VCalendarContent vcc;
VAlarm valarm = null;
if (cc instanceof Event) {
vcc = new VEvent();
} else {
vcc = new VTodo();
}
boolean allDay = cc.isAllDay();
List<com.funambol.common.pim.model.model.Property> properties =
new ArrayList<com.funambol.common.pim.model.model.Property>(15);
properties.add(composeField("UID" , cc.getUid() , xv));
// Shouldn't be necessary: the UID is already known at Calendar level
properties.add(composeField("SUMMARY" , cc.getSummary() , xv));
properties.add(composeField("DESCRIPTION", cc.getDescription(), xv));
properties.add(composeField("LOCATION" , cc.getLocation() , xv));
properties.add(composeField("CATEGORIES" , cc.getCategories() , xv));
Property pAC = cc.getAccessClass();
if (pAC != null) {
Object savedPropertyValue = null;
try {
savedPropertyValue = Short.parseShort(Property.stringFrom(pAC));
} catch (NumberFormatException e) {
savedPropertyValue = new Short("0");
}
String accessClass = accessClassFrom03((Short)savedPropertyValue);
pAC.setPropertyValue(accessClass);
properties.add(composeField("CLASS", cc.getAccessClass(), xv));
pAC.setPropertyValue(savedPropertyValue); // Restores the value
}
PropertyWithTimeZone dtStart = cc.getDtStart();
properties.add(composeDateTimeField("DTSTART", dtStart, allDay, xv));
if (cc instanceof Event) {
PropertyWithTimeZone pE = cc.getDtEnd();
Object savedPropertyValue = null;
if (pE != null) {
savedPropertyValue = pE.getPropertyValue();
String end = pE.getPropertyValueAsString();
if (TimeUtils.isInAllDayFormat(end)) {
end = TimeUtils.rollOneDay(end, true); // Rolls on
pE.setPropertyValue(end);
}
// FIX for Android //////////////////////
if (pE.getPropertyValue() == null) {
Property duration = cc.getDuration();
if (duration != null) {
String d = duration.getPropertyValueAsString();
if (d != null) {
end = TimeUtils.getDTEnd(dtStart.getPropertyValueAsString(), d, null, null);
if (end != null) {
pE.setPropertyValue(end);
}
// Inherit timezone from dtstart if null
if(pE.getTimeZone() == null && dtStart.getTimeZone() != null) {
pE.setTimeZone(dtStart.getTimeZone());
}
}
}
}
/////////////////////////////////////////
}
properties.add(composeDateTimeField("DTEND", pE, allDay, xv));
if (savedPropertyValue != null) {
pE.setPropertyValue(savedPropertyValue); // Restores the value
}
} else {
properties.add(composeDateTimeField("DUE", cc.getDtEnd(), allDay, xv));
}
// NB: We decided not to store the duration but only Start and End (Due)
properties.add(composeField("PRIORITY" , cc.getPriority() , xv));
properties.add(composeField("CONTACT" , cc.getContact() , xv));
properties.add(composeField("URL" , cc.getUrl() , xv));
properties.add(composeField("SEQUENCE" , cc.getSequence() , xv));
properties.add(composeField("PALARM" , cc.getPAlarm() , xv));
properties.add(composeField("DALARM" , cc.getDAlarm() , xv));
properties.add(composeField("ORGANIZER" , cc.getOrganizer(), xv));
properties.add(composeDateTimeField("DTSTAMP", cc.getDtStamp(), false, xv));
if (cc instanceof Event) {
properties.add(composeField("TRANSP", ((Event) cc).getTransp(), xv));
properties.add(composeField("STATUS", cc.getStatus() , xv));
} else if (cc instanceof Task) {
properties.add(composeField("PERCENT-COMPLETE",
((Task) cc).getPercentComplete(), xv));
properties.add(composeDateTimeField("COMPLETED",
((Task) cc).getDateCompleted(), false, xv));
CalendarStatus calendarStatus = CalendarStatus.mapServerStatus(cc.getStatus());
properties.add(composeField("STATUS",
new Property((calendarStatus != null ? calendarStatus.getVCalICalValue(xv) : null)), xv));
}
properties.add(composeField("LAST-MODIFIED", cc.getLastModified(), xv));
properties.add(composeDateTimeField("DCREATED", cc.getCreated(), false, xv));
Reminder reminder = cc.getReminder();
if (reminder != null && reminder.isActive()) {
if (xv) {
Object savedPropertyValue = reminder.getPropertyValue(); // null?
reminder.setPropertyValue(extractAAlarmPropertyValue(dtStart,
reminder)); // Temporarily changes the value
properties.add(composeDateTimeField("AALARM",
reminder, // A Reminder is
// a Property too
allDay ,
xv ,
false ));
reminder.setPropertyValue(savedPropertyValue); // Restores the value
} else {
valarm = new VAlarm();
Object savedPropertyValue = reminder.getPropertyValue(); // null?
reminder.setPropertyValue(extractReminderTime(dtStart, reminder));
com.funambol.common.pim.model.model.Property trigger =
composeDateTimeField("TRIGGER",
reminder,
allDay,
xv,
false);
trigger.setParameter(DATETIME_PARAMETER);
valarm.addProperty(trigger);
reminder.setPropertyValue(savedPropertyValue); // Restores the value
valarm.addProperty("REPEAT",
String.valueOf(reminder.getRepeatCount()));
int interval = reminder.getInterval();
if (interval > 0) {
valarm.addProperty("DURATION" ,
TimeUtils.getAlarmInterval(interval));
}
String soundFile = reminder.getSoundFile();
if ((soundFile != null) && (soundFile.length() != 0)) {
valarm.addProperty("ACTION", "AUDIO");
valarm.addProperty("ATTACH", soundFile);
}
}
}
RecurrencePattern rp = cc.getRecurrencePattern();
if (rp != null) {
Object savedPropertyValue = rp.getPropertyValue(); // null?
rp.setPropertyValue( // Temporarily changes the value
extractRRulePropertyValue(rp, xv));
properties.add(composeDateTimeField(
"RRULE" ,
rp , // A RecurrencePattern is also a Property
allDay ,
xv ,
true )); // has a special behaviour
rp.setPropertyValue(savedPropertyValue); // Restores the value
properties.add(composeDateTimeField(
"EXDATE",
new PropertyWithTimeZone(extractExDatePropertyValue(rp, xv),
rp.getTimeZone() ),
allDay ,
xv ,
true )); // has a special behaviour
properties.add(composeDateTimeField(
"RDATE" ,
new PropertyWithTimeZone(extractRDatePropertyValue(rp, xv),
rp.getTimeZone() ),
allDay ,
xv ,
true )); // has a special behaviour
}
if ((cc.getLatitude() != null) && (cc.getLongitude() != null)) {
if ((cc.getLatitude().getPropertyValueAsString() != null) &&
(cc.getLongitude().getPropertyValueAsString() != null)) {
String geo = cc.getLatitude().getPropertyValueAsString() + ";"
+ cc.getLongitude().getPropertyValueAsString();
if (geo.length() > 1) { // If it's not just a semicolon
Property tmp = cc.getLatitude();
Object savedPropertyValue = tmp.getPropertyValue();
tmp.setPropertyValue(geo); // Temporarily changes the value
properties.add(composeField("GEO", tmp, xv));
tmp.setPropertyValue(savedPropertyValue); // Restores the
} // value
}
}
properties.add(composeField("X-FUNAMBOL-FOLDER", cc.getFolder(), xv));
for (int i = 0; i < properties.size(); i++) {
if (properties.get(i) != null) {
vcc.addProperty(properties.get(i));
}
}
if (!xv && (valarm != null)) {
vcc.addComponent(valarm);
}
if (xv) {
try {
String priority19 = vcc.getProperty("PRIORITY").getValue();
String priority13 = String.valueOf(
importance19To13(Integer.parseInt(priority19)));
vcc.getProperty("PRIORITY").setValue(priority13);
} catch(Exception e) { //NumberFormatException, NullPointerException
// Goes on
}
}
if (cc.getBusyStatus() != null) {
vcc.addProperty("X-MICROSOFT-CDO-BUSYSTATUS",
busyStatusFrom03(cc.getBusyStatus()));
}
if (cc.isAllDay()) {
vcc.addProperty("X-FUNAMBOL-ALLDAY", "1");
} else {
vcc.addProperty("X-FUNAMBOL-ALLDAY", "0");
}
if (cc.getMeetingStatus() != null) {
vcc.addProperty("PARTSTAT", cc.getMeetingStatus().toString());
}
/*
// @todo
List ccentXTag = cc.getXTags();
for (int i=0; i<ccentXTag.size(); i++){
//vcc.addProperty(composeFieldXTag(ccentXTag));
}
*/
// @todo Add Task-specific properties
return vcc;
}
/**
* Performs the VCalendarContent-to-CalendarContent conversion.
*
* @param vcc the VCalendarContent object to be converted
* @param xv true if the text/x-vcalendar format must be used while
* generating some properties of the VCalendar output object
* @return a CalendarContent object
* @throws com.funambol.common.pim.converter.ConverterException
*/
public CalendarContent vcc2cc(VCalendarContent vcc, boolean xv)
throws ConverterException {
return vcc2cc(vcc, xv, null, null, null);
}
/**
* Performs the VCalendarContent-to-CalendarContent conversion.
*
* @param vcc the VCalendarContent object to be converted
* @param xv true if the text/x-vcalendar format must be used while
* generating some properties of the VCalendar output object
* @param dtStartTimeZone
* @param dtEndTimeZone
* @param reminderTimeZone
* @return a CalendarContent object
* @throws com.funambol.common.pim.converter.ConverterException
*/
protected CalendarContent vcc2cc(VCalendarContent vcc ,
boolean xv ,
TimeZone dtStartTimeZone ,
TimeZone dtEndTimeZone ,
TimeZone reminderTimeZone)
throws ConverterException {
// Sets the three time zones
this.dtStartTimeZone =
(dtStartTimeZone == null ) ?
timezone : dtStartTimeZone ;
this.dtEndTimeZone =
(dtEndTimeZone == null ) ?
timezone : dtEndTimeZone ;
this.reminderTimeZone =
(reminderTimeZone == null) ?
timezone : reminderTimeZone;
CalendarContent cc;
if (vcc instanceof VEvent) {
cc = new Event();
} else {
cc = new Task();
}
cc.setDtStart (decodeDateTimeField(vcc.getProperty("DTSTART" ),
dtStartTimeZone));
cc.setDuration(decodeField(vcc.getProperty("DURATION")));
boolean isAllday = false;
PropertyWithTimeZone pS;
PropertyWithTimeZone pE;
if (vcc instanceof VEvent) {
pS = decodeDateTimeField(vcc.getProperty("DTSTART"), dtStartTimeZone);
pE = decodeDateTimeField(vcc.getProperty("DTEND"), dtEndTimeZone);
//
// An event is an allday when the dtstart is in the format
// PATTERN_YYYYMMDD or PATTERN_YYYY_MM_DD and
// the dtend is null (this means there is the duration)
// or
// the dtend is in allday format
// If the event is already an allday event, it's needed
// to roll back one day from the end date in order to exclude the
// last day like required by iCal specification
//
if (TimeUtils.isInAllDayFormat(pS.getPropertyValueAsString())) {
if (pE.getPropertyValueAsString() == null ||
TimeUtils.isInAllDayFormat(pE.getPropertyValueAsString())) {
isAllday = true;
}
}
} else {
pE = decodeDateTimeField(vcc.getProperty("DUE"), dtEndTimeZone);
}
cc.setDtEnd(pE);
cc.setDtStamp (decodeDateTimeField(vcc.getProperty("DTSTAMP" ),
dtStartTimeZone));
cc.setLastModified(decodeDateTimeField(vcc.getProperty("LAST-MODIFIED"),
dtStartTimeZone));
cc.setCreated (decodeDateTimeField(vcc.getProperty("DCREATED" ),
dtStartTimeZone));
if (cc instanceof Task) {
((Task) cc).setDateCompleted(
decodeDateTimeField(vcc.getProperty("COMPLETED"),
dtStartTimeZone));
}
try {
fixGenericDateProperty(cc.getLastModified());
fixGenericDateProperty(cc.getCreated());
//
// The dates must be fixed before the RRULE and the AALARM
// properties are parsed
//
if (cc instanceof Event) {
fixDates(cc, true);
//
// Only if the event is recognized like an allday event in the
// right allday format the roll back one day it's needed.
//
String start = cc.getDtStart().getPropertyValueAsString();
String end = cc.getDtEnd().getPropertyValueAsString();
if ((end != null) &&
!(end.equals(start)) && // This is for robustness's sake
TimeUtils.isInAllDayFormat(end) &&
isAllday) {
end = TimeUtils.rollOneDay(end, false); // Rolls back
cc.getDtEnd().setPropertyValue(end);
}
} else if (cc instanceof Task) {
fixDates(cc, false);
}
} catch (Exception e) {
throw new ConverterException("Error while fixing the dates", e);
}
cc.setUid(decodeField(vcc.getProperty("UID")));
// Shouldn't be necessary: the UID is already known at Calendar level
if (cc instanceof Event) {
((Event) cc).setTransp(decodeField(vcc.getProperty("TRANSP")));
}
cc.setSummary (decodeField(vcc.getProperty("SUMMARY" )));
cc.setDescription(decodeField(vcc.getProperty("DESCRIPTION")));
cc.setLocation (decodeField(vcc.getProperty("LOCATION" )));
if (cc instanceof Task) {
Property status = new Property();
CalendarStatus calendarStatus=CalendarStatus.mapVcalIcalStatus(decodeField(vcc.getProperty("STATUS")));
status.setPropertyValue((calendarStatus!=null?calendarStatus.getServerValue():null));
cc.setStatus(status);
}else{
cc.setStatus(decodeField(vcc.getProperty("STATUS" )));
}
cc.setCategories (decodeField(vcc.getProperty("CATEGORIES" )));
String accessClass;
com.funambol.common.pim.model.model.Property tmpClass =
vcc.getProperty("CLASS");
if (tmpClass == null) {
accessClass = null;
} else {
accessClass = tmpClass.getValue();
}
Property accessClassProperty = new Property();
accessClassProperty.setPropertyValue(
new Short(accessClassTo03(accessClass)));
cc.setAccessClass(accessClassProperty);
cc.setPriority(decodeField(vcc.getProperty("PRIORITY")));
if (xv) {
try {
String priority13 = cc.getPriority().getPropertyValueAsString();
String priority19 = String.valueOf(
importance13To19(Integer.parseInt(priority13)));
cc.getPriority().setPropertyValue(priority19);
} catch(Exception e) { //NumberFormatException, NullPointerException
// Goes on
}
}
cc.setContact (decodeField(vcc.getProperty("CONTACT" )));
cc.setUrl (decodeField(vcc.getProperty("URL" )));
cc.setSequence (decodeField(vcc.getProperty("SEQUENCE" )));
cc.setPAlarm (decodeDateTimeField(vcc.getProperty("PALARM" ),
reminderTimeZone));
cc.setDAlarm (decodeDateTimeField(vcc.getProperty("DALARM" ),
reminderTimeZone));
cc.setOrganizer(decodeField(vcc.getProperty("ORGANIZER")));
com.funambol.common.pim.model.model.Property msCdoBusyStatus =
vcc.getProperty("X-MICROSOFT-CDO-BUSYSTATUS");
if (msCdoBusyStatus != null) {
String busyStatus = msCdoBusyStatus.getValue();
if (busyStatus != null) {
cc.setBusyStatus(busyStatusTo03(busyStatus));
}
}
Short meetingStatus = decodeShortField(vcc.getProperty("PARTSTAT"));
if (meetingStatus != null) {
cc.setMeetingStatus(meetingStatus);
}
Property geo1 = decodeField(vcc.getProperty("GEO"));
Property geo2 = decodeField(vcc.getProperty("GEO"));
if (geo1 != null) {
StringTokenizer st = new StringTokenizer(
geo1.getPropertyValueAsString(), ";");
if(st.countTokens() == 2) {
geo1.setPropertyValue(st.nextToken());
cc.setLatitude(geo1);
geo2.setPropertyValue(st.nextToken());
cc.setLongitude(geo2);
}
}
//
// If the calendar is an all-day event or task but no device timezone
// is set and there is a start date, the time interval between the start
// date time and the aalarm time is computed and this difference is
// applied to the corrected start time of the all-day event or task
// (ie, midnight UTC) irrespective of any time zone consideration.
// This modified aalarm time is saved in the DB as a UTC time, but
// it's interpreted as a local time when it's retrieved (exactly as
// start and end dates when the all-day flag is on).
//
Reminder reminder = null;
if (xv) { // vCalendar
Property aalarm = decodeField(vcc.getProperty("AALARM"));
if (aalarm != null && aalarm.getPropertyValueAsString() != null) {
Property dtstart = decodeField(vcc.getProperty("DTSTART"));
if (cc.isAllDay() && reminderTimeZone == null && dtstart != null) {
reminder = convertAAlarmToReminderBasedOnMinutes(
dtstart.getPropertyValueAsString(),
cc.getDtStart().getPropertyValueAsString(),
aalarm.getPropertyValueAsString()
);
} else {
reminder = convertAAlarmToReminder(
cc.isAllDay(),
cc.getDtStart(),
aalarm.getPropertyValueAsString()
);
}
}
} else { // iCalendar
VAlarm valarm = (VAlarm) vcc.getComponent("VALARM");
if (valarm != null) {
reminder = convertVAlarmToReminder(valarm ,
cc.isAllDay() ,
cc.getDtStart(),
cc.getDtEnd() );
}
}
if (reminder != null && reminderTimeZone != null) {
reminder.setTimeZone(reminderTimeZone.getID());
}
cc.setReminder(reminder);
Property rrule = decodeField(vcc.getProperty("RRULE"));
if (rrule != null && rrule.getPropertyValueAsString().length() != 0) {
try {
cc.setRecurrencePattern(
getRecurrencePattern(
cc.getDtStart().getPropertyValueAsString(),
cc.getDtEnd().getPropertyValueAsString() ,
rrule.getPropertyValueAsString() ,
dtStartTimeZone ,
xv
)
);
List<com.funambol.common.pim.model.model.Property> rdates =
vcc.getProperties("RDATE");
for (com.funambol.common.pim.model.model.Property rdateProperty : rdates) {
Property rdate = decodeField(rdateProperty);
if (rdate != null) {
cc.getRecurrencePattern()
.getExceptions()
.addAll(getRDates(rdate.getPropertyValueAsString(),
cc.isAllDay() ));
}
}
List<com.funambol.common.pim.model.model.Property> exdates =
vcc.getProperties("EXDATE");
for (com.funambol.common.pim.model.model.Property exdateProperty : exdates) {
Property exdate = decodeField(exdateProperty);
if (exdate != null) {
cc.getRecurrencePattern()
.getExceptions()
.addAll(getExDates(exdate.getPropertyValueAsString(),
cc.isAllDay() ));
}
}
} catch (ConverterException ce) {
cc.setRecurrencePattern(null); // Ignore parsing errors
}
}
/*
// @todo
List ccentXTag = cc.getXTags();
for (int i=0; i<ccentXTag.size(); i++){
//vcc.addProperty(composeFieldXTag(ccentXTag));
}
*/
if (cc instanceof Task) {
((Task) cc).setPercentComplete(
decodeField(vcc.getProperty("PERCENT-COMPLETE"))
);
String status = null;
if ((cc.getStatus() != null)) {
status = cc.getStatus().getPropertyValueAsString();
}
if ((status != null) && (status.length() != 0)) {
if ("COMPLETED".equalsIgnoreCase(status)) {
((Task) cc).setComplete(new Property("1"));
} else {
((Task) cc).setComplete(new Property("0"));
}
}
}
cc.setFolder(decodeField(vcc.getProperty("X-FUNAMBOL-FOLDER")));
return cc;
}
//---------------------------------------------------------- Private Methods
/**
* @return a representation of the field RRULE (version 1.0)
*/
private String composeFieldRrule(RecurrencePattern rrule) {
StringBuffer result = new StringBuffer(60); // Estimate 60 is needed
if (rrule != null) {
addXParams(result, rrule);
result.append(rrule.getTypeDesc()).append(rrule.getInterval());
if (rrule.getInstance() < 0) {
result.append(" " + (-rrule.getInstance()) + "-");
} else if (rrule.getInstance() > 0) {
result.append(" " + rrule.getInstance() + "+");
} // else, it's zero and nothing's to be done
for (String day : rrule.getDayOfWeek()) {
result.append(' ').append(day);
}
if (rrule.getDayOfMonth() != 0 && !"YM".equals(rrule.getTypeDesc())) {
result.append(' ').append(rrule.getDayOfMonth());
}
if (rrule.getMonthOfYear() != 0) {
result.append(' ').append(rrule.getMonthOfYear());
}
if (rrule.getOccurrences() != -1 && rrule.isNoEndDate()) {
result.append(" #").append(rrule.getOccurrences());
} else {
if (rrule.isNoEndDate()) {
result.append(" #0"); //forever
}
}
if (!rrule.isNoEndDate() &&
rrule.getEndDatePattern() != null &&
!rrule.getEndDatePattern().equals("")) {
TimeZone propertyTimeZone;
String timeZoneID = rrule.getTimeZone();
if (timeZoneID == null) {
propertyTimeZone = timezone;
} else {
propertyTimeZone = TimeZone.getTimeZone(timeZoneID);
}
try {
result.append(' ');
String endDatePattern = rrule.getEndDatePattern();
endDatePattern =
handleConversionToLocalDate(endDatePattern, propertyTimeZone);
if (TimeUtils.isInAllDayFormat(endDatePattern)) {
endDatePattern =
TimeUtils.convertDateFromInDayFormat(endDatePattern, "000000");
}
result.append(endDatePattern);
} catch (ParseException e) {
// This should never happen!
} catch (ConverterException ce) {
// This should never happen!
}
}
}
return result.toString();
}
private Reminder convertVAlarmToReminder(VAlarm valarm ,
boolean allDay ,
PropertyWithTimeZone dtStart,
PropertyWithTimeZone dtEnd ) {
Reminder reminder = new Reminder();
try {
com.funambol.common.pim.model.model.Property triggerProperty =
valarm.getProperty("TRIGGER");
String trigger = triggerProperty.getValue();
if (trigger.startsWith("-P") || trigger.startsWith("P")) { // it's an interval
int minutes = -TimeUtils.getAlarmInterval(trigger);
String related = triggerProperty.getParameter("RELATED").value;
PropertyWithTimeZone relatedProperty;
if ("END".equals(related)) {
relatedProperty = dtEnd;
} else {
relatedProperty = dtStart;
reminder.setMinutes(minutes);
}
setAlarmTimeBasedOnMinutes(reminder, minutes, relatedProperty, allDay);
} else {
setAlarmTimeAndMinutes(reminder, trigger, dtStart, allDay);
}
com.funambol.common.pim.model.model.Property repeatProperty =
valarm.getProperty("REPEAT");
if (repeatProperty != null) {
try {
int repeatCount = Integer.parseInt(repeatProperty.getValue());
reminder.setRepeatCount(repeatCount);
} catch (NumberFormatException e) {
// Does nothing
}
}
com.funambol.common.pim.model.model.Property durationProperty =
valarm.getProperty("DURATION");
if (durationProperty != null) {
int interval =
TimeUtils.getAlarmInterval(durationProperty.getValue());
reminder.setInterval(interval);
}
com.funambol.common.pim.model.model.Property actionProperty =
valarm.getProperty("ACTION");
if ((actionProperty != null) &&
("AUDIO".equals(actionProperty.getValue()))) {
com.funambol.common.pim.model.model.Property attachProperty =
valarm.getProperty("ATTACH");
if (attachProperty != null) {
reminder.setSoundFile(attachProperty.getValue());
}
}
return reminder;
} catch (Exception e) {
return null;
}
}
private void setAlarmTimeBasedOnMinutes(Reminder reminder ,
int minutes ,
PropertyWithTimeZone relatedProperty,
boolean allDay )
throws ConverterException {
String relatedTime;
SimpleDateFormat formatter;
if (!allDay) {
String relatedPropertyValue = relatedProperty.getPropertyValueAsString();
if (relatedPropertyValue == null) {
throw new ConverterException("RELATED parameter refers to non-existing property");
}
String relatedTimeZoneID = relatedProperty.getTimeZone();
TimeZone relatedTimeZone = (relatedTimeZoneID == null ?
null :
TimeZone.getTimeZone(relatedTimeZoneID));
relatedTime =
handleConversionToUTCDate(relatedPropertyValue,
relatedTimeZone );
formatter = new SimpleDateFormat(TimeUtils.PATTERN_UTC);
} else {
relatedTime = (relatedProperty.getPropertyValueAsString() + "T000000")
.replaceAll("-", "");
formatter = new SimpleDateFormat(TimeUtils.PATTERN_UTC_WOZ);
}
formatter.setTimeZone(TimeUtils.TIMEZONE_UTC);
Date relatedDate;
try {
relatedDate = formatter.parse(relatedTime);
} catch (ParseException e) {
throw new ConverterException(e);
}
GregorianCalendar greg = new GregorianCalendar();
greg.setTime(relatedDate);
greg.add(GregorianCalendar.MINUTE, -minutes);
reminder.setTime(formatter.format(greg.getTime()));
reminder.setActive(true);
}
private void setAlarmTimeAndMinutes(Reminder reminder,
String time ,
Property dtStart ,
boolean allDay )
throws ConverterException {
String alarmStart;
// If the calendar is an all day event (or task) then
// the aalarm time is considered as a local time
// information consistently with the way start and end
// dates are processed.
if (allDay) {
// Converts aalarm in local date and time to preserve
// the distance from aalarm time to midnigth of the
// start date.
alarmStart =
handleConversionToLocalDate(time, reminderTimeZone);
} else {
// Converts aalarm in UTC date and time to preserve
// the absolute moment of the aalarm.
alarmStart =
handleConversionToUTCDate(time, reminderTimeZone);
}
reminder.setTime(alarmStart);
if (dtStart != null) {
reminder.setMinutes(
TimeUtils.getAlarmMinutes(
dtStart.getPropertyValueAsString(),
reminder.getTime(),
null
)
);
} else {
reminder.setMinutes(0);
}
reminder.setActive(true);
}
/**
* Returns the reminder time calculated using the given dtstart and the
* minutes set in the given Reminder in case of the in this object the time
* is null.
*/
private String extractReminderTime(PropertyWithTimeZone dtStart ,
Reminder reminder)
throws ConverterException {
java.util.Date dateStart = null;
SimpleDateFormat formatter = new SimpleDateFormat();
String dtStartVal=(String)dtStart.getPropertyValue();
String dtAlarmVal = reminder.getTime();
if (dtAlarmVal == null || dtAlarmVal.length() == 0) {
if (dtStartVal == null || dtStartVal.length() == 0) {
return null;
}
try {
TimeZone propertyTimeZone;
String timeZoneID = dtStart.getTimeZone();
if (timeZoneID == null) {
propertyTimeZone = timezone;
} else {
propertyTimeZone = TimeZone.getTimeZone(timeZoneID);
}
dtStartVal =
handleConversionToUTCDate(dtStartVal, propertyTimeZone);
formatter.applyPattern(TimeUtils.getDateFormat(dtStartVal));
dateStart = formatter.parse(dtStartVal);
} catch (Exception e) {
throw new ConverterException("Error while parsing start date "
+ "during reminder time calculation", e);
}
java.util.Calendar calAlarm = java.util.Calendar.getInstance();
calAlarm.setTime(dateStart);
calAlarm.add(java.util.Calendar.MINUTE, -reminder.getMinutes());
Date dtAlarm = calAlarm.getTime();
formatter.applyPattern("yyyyMMdd'T'HHmmss'Z'");
dtAlarmVal = formatter.format(dtAlarm);
}
TimeZone propertyTimeZone;
String timeZoneID = reminder.getTimeZone();
if (timeZoneID == null) {
propertyTimeZone = timezone;
} else {
propertyTimeZone = TimeZone.getTimeZone(timeZoneID);
}
if (forceClientLocalTime) {
dtAlarmVal = handleConversionToLocalDate(dtAlarmVal, propertyTimeZone);
}
return dtAlarmVal;
}
/**
* @return a representation of the event field AALARM
*/
private String extractAAlarmPropertyValue(PropertyWithTimeZone dtStart ,
Reminder reminder)
throws ConverterException {
StringBuffer result = new StringBuffer(60); // 60 has been estimated OK
String dtAlarmVal = extractReminderTime(dtStart, reminder);
result.append(dtAlarmVal).append(';');
if (reminder.getInterval() != 0) {
result.append(TimeUtils.getIso8601Duration(String.valueOf(reminder.getInterval())));
}
result.append(';').append(reminder.getRepeatCount());
result.append(';');
if (reminder.getSoundFile() != null) {
result.append(reminder.getSoundFile());
}
return result.toString();
}
private String extractRRulePropertyValue(RecurrencePattern rp, boolean xv) {
if (xv) {
return composeFieldRrule(rp);
}
StringBuffer result = new StringBuffer(99); // 99 has been estimated OK
String type = null;
switch(rp.getTypeId()) {
case RecurrencePattern.TYPE_DAILY:
type = DAILY;
break;
case RecurrencePattern.TYPE_WEEKLY:
type = WEEKLY;
break;
case RecurrencePattern.TYPE_MONTHLY:
case RecurrencePattern.TYPE_MONTH_NTH:
type = MONTHLY;
break;
case RecurrencePattern.TYPE_YEARLY:
case RecurrencePattern.TYPE_YEAR_NTH:
type = YEARLY;
break;
default:
return null;
}
appendToStringBuffer(result, FREQ, type);
appendToStringBuffer(result, INTERVAL, String.valueOf(rp.getInterval()));
appendToStringBuffer(result, BYMONTH,
String.valueOf(rp.getMonthOfYear()));
appendToStringBuffer(result, BYMONTHDAY,
String.valueOf(rp.getDayOfMonth()));
appendToStringBuffer(result, BYSETPOS,
String.valueOf(rp.getInstance()));
int occurrences = rp.getOccurrences();
if (occurrences != -1) { // means unspecified: see RecurrencePattern
appendToStringBuffer(result, COUNT, String.valueOf(occurrences));
} else {
if (!rp.isNoEndDate()) {
try {
TimeZone propertyTimeZone;
String timeZoneID = rp.getTimeZone();
if (timeZoneID == null) {
propertyTimeZone = timezone;
} else {
propertyTimeZone = TimeZone.getTimeZone(timeZoneID);
}
appendToStringBuffer(result, UNTIL,
handleConversionToLocalDate(
String.valueOf(rp.getEndDatePattern()),
propertyTimeZone
)
);
} catch (ConverterException ce) {
// This should never happen!
}
}
}
String weekDays = "";
for(
short mask = rp.getDayOfWeekMask(), j = 0;
mask > 0; // Until the mask has been eaten up
mask /= 2, j++) { // Shifts the mask, looks for the next weekday
if (mask % 2 == 1) { // Bingo!
weekDays += "," + WEEK[j]; // Adds the correct weekday symbol
}
}
if (weekDays.length() > 0) {
appendToStringBuffer(result, BYDAY,
weekDays.substring(1)); // Discards the first ","
}
return result.toString();
}
/**
* @see extractExceptionsAsString(RecurrencePattern, boolean, boolean)
*/
private String extractExDatePropertyValue(RecurrencePattern rp, boolean xv) {
return extractExceptionsAsString(rp, false, xv);
}
/**
* @see extractExceptionsAsString(RecurrencePattern, boolean, boolean)
*/
private String extractRDatePropertyValue(RecurrencePattern rp, boolean xv) {
return extractExceptionsAsString(rp, true, xv);
}
/**
* Extracts the recurrence exceptions from a given RecurrencePattern object
* and returns them as a string of colon- or semicolon-separated-values.
*
* @param rp the recurrence pattern that contains the exception list
* @param rdate true if only the addition exceptions are to be extracted,
* false if only the deletion exceptions are to be extracted
* @param xv true if the format to be used is vCalendar (1.0),
* false if it is iCalendar (vCalendar 2.0).
* The difference is about the usage of ';' or ',' as a separator
* @return the list in the proper vCalendar/iCalendar format
*/
private String extractExceptionsAsString(RecurrencePattern rp ,
boolean rdate,
boolean xv ) {
List<ExceptionToRecurrenceRule> exceptions = rp.getExceptions();
StringBuilder result = new StringBuilder();
char separator = (xv ? ';' : ','); // as of specifications
TimeZone propertyTimeZone;
String timeZoneID = rp.getTimeZone();
if (timeZoneID == null) {
propertyTimeZone = timezone;
} else {
propertyTimeZone = TimeZone.getTimeZone(timeZoneID);
}
for (ExceptionToRecurrenceRule etrr : exceptions) {
if (etrr.isAddition() == rdate) {
if (result.length() > 0) { // always but the first time
result.append(separator);
}
try {
result.append(
handleConversionToLocalDate(etrr.getDate() ,
propertyTimeZone)
);
} catch (ConverterException ce) {
result.append(etrr.getDate()); // Keeps it as it is
}
}
}
if (result.length() == 0) {
return null;
}
return result.toString();
}
private void appendToStringBuffer(StringBuffer sb ,
String key ,
String value) {
if (value != null && !ZERO.equals(value)) {
if (!key.equals(FREQ)) { // FREQ is the first piece of the RRULE
sb.append(';');
}
sb.append(key).append('=').append(value);
}
}
private static boolean isEndDateOrDuration(String token) { // Just for RRULE 1.0
if (token.startsWith("#")) { // it's a duration
return true;
}
if (token.length() >= 8) { // it's an end date
return true;
}
return false;
}
private static short instanceModifierToInt(String modifier) { // Irrespective of the
if (modifier.indexOf("-") != -1) { // version
return (short) - Short.parseShort(modifier.replaceAll("\\-", ""));
}
return Short.parseShort(modifier.replaceAll("\\+", ""));
}
private static boolean beginsWith1To9(String s) {
switch (s.charAt(0)) {
case '1' :
case '2' :
case '3' :
case '4' :
case '5' :
case '6' :
case '7' :
case '8' :
case '9' :
return true;
default:
return false;
}
}
public static RecurrencePattern getRecurrencePattern(String startDate ,
String endDate ,
String rrule ,
TimeZone recurrenceTZ,
boolean xv )
throws ConverterException {
if (rrule == null || rrule.length() == 0) {
return null;
}
String startDatePattern;
if (startDate == null || startDate.length() == 0) {
if (endDate == null) {
return null;
}
startDatePattern = endDate;
} else {
startDatePattern = startDate;
}
Map<String, List<String>> map =
new HashMap<String, List<String>>(7);
List<String> week = null;
short type = -1;
int occurrences = -1; // means unspecified: see RecurrencePattern
short instance = 0;
int interval;
short dayOfWeekMask = 0;
short dayOfMonth = 0;
short monthOfYear = 0;
String endDatePattern = null;
if (xv) {
try {
StringTokenizer st = new StringTokenizer(rrule, " ");
String frequencyInterval = st.nextToken();
String durationOrEndDate = null;
int c = 2; // Default: frequency is 2-character long
if (frequencyInterval.startsWith("D")) {
type = RecurrencePattern.TYPE_DAILY;
c = 1; // Shorter than default
// No modifier... moves on to end date or duration
} else if (frequencyInterval.startsWith("W")) {
type = RecurrencePattern.TYPE_WEEKLY;
c = 1; // Shorter than default
week = new ArrayList<String>(7); // Big enough!
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (isEndDateOrDuration(token)) {
durationOrEndDate = token;
break;
} else {
week.add(token);
}
}
} else if (frequencyInterval.startsWith("MD")) {
type = RecurrencePattern.TYPE_MONTHLY;
String monthDay = null;
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (isEndDateOrDuration(token)) {
durationOrEndDate = token;
break;
} else if (monthDay == null) { // Just the 1st one found
monthDay = token;
}
}
if (monthDay != null) {
dayOfMonth = instanceModifierToInt(monthDay);
}
} else if (frequencyInterval.startsWith("MP")) {
type = RecurrencePattern.TYPE_MONTH_NTH;
week = new ArrayList<String>(7); // Big enough!
String instanceModifier = "";
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (isEndDateOrDuration(token)) {
durationOrEndDate = token;
break;
} else if (beginsWith1To9(token)) {
instanceModifier = token;
} else {
week.add(new String(instanceModifier + token));
}
}
} else if (frequencyInterval.startsWith("YD")) {
type = RecurrencePattern.TYPE_YEAR_NTH;
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (isEndDateOrDuration(token)) {
durationOrEndDate = token;
break;
}
}
// Month etc. are ignored and set below with rp.fix()
} else if (frequencyInterval.startsWith("YM")) {
type = RecurrencePattern.TYPE_YEARLY;
week = new ArrayList<String>(7); // Big enough!
String instanceModifier = "";
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (isEndDateOrDuration(token)) {
durationOrEndDate = token;
break;
} else if (beginsWith1To9(token)) {
instanceModifier = token;
} else {
week.add(new String(instanceModifier + token));
}
}
} else {
throw new ConverterException("Error while parsing RRULE " +
"(1.0): frequency not recognized");
}
interval = Short.parseShort(
frequencyInterval.substring(c));
if (durationOrEndDate == null) { // If it's not been retricced,
if (!st.hasMoreTokens()) { // yet
durationOrEndDate = "#2"; // Default: repeat twice
} else {
durationOrEndDate = st.nextToken();
}
}
if (durationOrEndDate.startsWith("#")) { // it's a duration
occurrences = Short.parseShort(
durationOrEndDate.substring(1));
if (st.hasMoreTokens()) {
// This is possible in theory. We ignore this case but
// the specification prescribes to use whichever
// requirement is stricter among the duration and the
// end date. We just use the first one we find.
}
} else { // it's an end date
endDatePattern = handleConversionToUTCDate(
durationOrEndDate,
recurrenceTZ);
if (st.hasMoreTokens()) {
// This is possible in theory. We ignore this case but
// the specification prescribes to use whichever
// requirement is stricter among the duration and the
// end date. We just use the first one we find.
}
}
} catch (Exception e) {
throw new ConverterException("Error while parsing RRULE (1.0): "
+ e);
}
} else {
try {
StringTokenizer stSemiColon =
new StringTokenizer(rrule, ";");
while(stSemiColon.hasMoreTokens()) {
StringTokenizer stEquals =
new StringTokenizer(stSemiColon.nextToken(), "=");
String key = stEquals.nextToken();
StringTokenizer stComma =
new StringTokenizer(stEquals.nextToken(), ",");
List<String> list = new ArrayList<String>();
while (stComma.hasMoreTokens()) {
list.add(stComma.nextToken());
}
map.put(key, list);
}
} catch (Exception e) {
throw new ConverterException("Error while parsing RRULE (2.0): "
+ e);
}
interval = Short.parseShort(find(map, INTERVAL, true));
if (interval == 0) {
interval = 1;
}
monthOfYear = Short.parseShort(find(map, BYMONTH, true));
dayOfMonth = Short.parseShort(find(map, BYMONTHDAY, true));
instance = Short.parseShort(find(map, BYSETPOS, true));
occurrences = Short.parseShort(find(map, COUNT, true));
endDatePattern = find(map, UNTIL, false);
try {
endDatePattern = handleConversionToUTCDate(endDatePattern,
recurrenceTZ);
} catch (Exception e) {
throw new ConverterException("Error while parsing RRULE (2.0):"
+ " timezone-based conversion failed.");
}
List<String> obj = map.get(BYDAY);
if (obj != null) {
week = obj;
}
String freq = find(map, FREQ, false);
if (freq == null) {
throw new ConverterException("Error while parsing RRULE (2.0):"
+ " frequency not found");
}
if (freq.equals(DAILY)) {
type = RecurrencePattern.TYPE_DAILY;
} else if (freq.equals(WEEKLY)) {
type = RecurrencePattern.TYPE_WEEKLY;
} else if (freq.equals(MONTHLY)) {
if (week == null) {
type = RecurrencePattern.TYPE_MONTHLY;
} else {
type = RecurrencePattern.TYPE_MONTH_NTH;
}
} else if (freq.equals(YEARLY)) {
if (week == null) {
type = RecurrencePattern.TYPE_YEARLY;
} else {
type = RecurrencePattern.TYPE_YEAR_NTH;
}
}
if (type == -1) { // is the default value
throw new ConverterException("Error while parsing RRULE (2.0):"
+ " frequency not found");
}
}
if (occurrences == 0) {
occurrences = -1; // means unspecified: see RecurrencePattern
}
boolean noEndDate = (endDatePattern == null);
// Duplicates will be counted just once
if (week != null) {
short maskElement = 1;
for (
int j = 0;
j < 7; // 7, because a week has 7 days
j++,
maskElement *= 2 // Sunday = 1, Monday = 2, Tuesday = 4 etc.
) {
for (int i = 0; i < week.size(); i++) {
int pos = week.get(i).indexOf(WEEK[j]);
if (pos != -1) { // Bingo!
dayOfWeekMask += maskElement;
if (pos > 0) { // There's an instance marker, too
if (instance == 0) { // it's not been set yet
try { // Takes just the 1st modifier it finds
instance = instanceModifierToInt(
week.get(i).substring(0, pos));
} catch (Exception e) {
throw new ConverterException("Error while "
+ "parsing RRULE's instance "
+ "modifier.", e);
}
} else {
// Currently, multi-instance recurrence patterns
// are not supported.
}
}
week.remove(i); // It won't be used any more
break; // Time to look for the next week day
}
}
}
}
RecurrencePattern rp = new RecurrencePattern(
type,
interval,
monthOfYear,
dayOfMonth,
dayOfWeekMask,
instance,
startDatePattern,
endDatePattern,
noEndDate,
occurrences);
if (recurrenceTZ != null) {
rp.setTimeZone(recurrenceTZ.getID());
}
try {
rp.fix(); // If it lacks some information, it's extracted from the
// start date
} catch (RecurrencePatternException rpe) {
throw new ConverterException("Error while fixing a newly parsed " +
"recurrence pattern that lacks some information.", rpe);
}
return rp;
}
/**
* @see getExceptionsAsSet(String,boolean)
*/
private SortedSet<ExceptionToRecurrenceRule> getRDates(String rdate, boolean isAllDay) {
return getExceptionsAsSet(rdate, true, isAllDay);
}
/**
* @see getExceptionsAsSet(String,boolean)
*/
private SortedSet<ExceptionToRecurrenceRule> getExDates(String exdate, boolean isAllDay) {
return getExceptionsAsSet(exdate, false, isAllDay);
}
/**
* Parses a string of semicolon- or comma-separated values taken from the
* content of an EXDATE or RDATE vCalendar/iCalendar property and uses the
* parsed values to fill a set of ExceptionToRecurrenceRule istances.
*
* @param scsv the property value, as a String object
* @param rdate true if the string comes from an RDATE property, false if
* it comes from an EXDATE property
* @param isAllDay true if the calendar is an allday
*
* @return a SortedSet of ExceptionToRecurrenceRule objects
*/
private SortedSet<ExceptionToRecurrenceRule> getExceptionsAsSet(String scsv, boolean rdate, boolean isAllDay) {
SortedSet<ExceptionToRecurrenceRule> exceptions =
new TreeSet<ExceptionToRecurrenceRule>();
if (scsv == null || scsv.length() == 0) {
return exceptions;
}
String[] tokens = scsv.split("[;,]");
for (String token : tokens) {
try {
//
// Trimming the given date because some devices send it with a
// space at the beginning
//
if (isAllDay) {
exceptions.add(
new ExceptionToRecurrenceRule(
rdate,
handleConversionToLocalDate(token.trim(), dtStartTimeZone)
)
);
} else {
exceptions.add(
new ExceptionToRecurrenceRule(
rdate,
handleConversionToUTCDate(token.trim(), dtStartTimeZone)
)
);
}
} catch (ConverterException e) {
// Skips this one
} catch (ParseException e) {
// Skips this one
}
}
return exceptions;
}
private static String find(Map where, String what, boolean zeroIfTrouble) {
try {
return (String) ((List) where.get(what)).get(0);
} catch (Exception e) {
if (zeroIfTrouble) {
return ZERO;
}
else {
return null;
}
}
}
/**
* It uses the DTSTART's time zone.
*/
private void fixGenericDateProperty(Property property) throws Exception {
if (property == null ||
property.getPropertyValue() == null ||
"".equals(property.getPropertyValueAsString()) ) {
return ;
}
String value = property.getPropertyValueAsString();
property.setPropertyValue(handleConversionToUTCDate(value, dtStartTimeZone));
}
private void fixDates(CalendarContent cc, boolean isEvent) throws Exception {
if (isEvent) {
fixGenericDateProperty(((Event)cc).getReplyTime());
} else {
fixGenericDateProperty(((Task)cc).getDateCompleted());
}
if (cc.getDtStart() == null) {
cc.setDtStart(new PropertyWithTimeZone());
}
if (cc.getDtEnd() == null) {
cc.setDtEnd(new PropertyWithTimeZone());
}
if (cc.getDuration() == null) {
cc.setDuration(new Property());
}
String dtstart = cc.getDtStart().getPropertyValueAsString() ;
String dtend = cc.getDtEnd().getPropertyValueAsString() ;
String duration = cc.getDuration().getPropertyValueAsString();
try{
cc.setAllDay(Boolean.FALSE);
//
// Check if the event is an AllDay event
// (yyyyMMdd or yyyy-MM-dd)
//
if (TimeUtils.isInAllDayFormat(dtstart)) {
try{
dtstart = TimeUtils.convertDateFromTo(dtstart, TimeUtils.PATTERN_YYYY_MM_DD);
} catch (java.text.ParseException e) {
throw new ConverterException("Error parsing date: " + e.getMessage());
}
cc.getDtStart().setPropertyValue(dtstart);
cc.setAllDay(Boolean.TRUE);
} else {
dtstart = handleConversionToUTCDate(dtstart, dtStartTimeZone);
cc.getDtStart().setPropertyValue(dtstart);
}
//
// Check if the event is an AllDay event
//
if (TimeUtils.isInAllDayFormat(dtend)) {
dtend = TimeUtils.convertDateFromTo(dtend, TimeUtils.PATTERN_YYYY_MM_DD);
cc.setAllDay(Boolean.TRUE);
} else {
dtend = handleConversionToUTCDate(dtend, dtEndTimeZone);
}
//
// Compute End Date by Start Date and Duration
//
dtend = TimeUtils.getDTEnd(dtstart, duration, dtend, null);
cc.getDtEnd().setPropertyValue(dtend);
//
// If the event is an all day check if there is the end date:
// 1) if end date is null then set it with start date value.
// 2) if end date is not into yyyy-MM-dd or yyyyMMdd format then
// normalize it in yyyy-MM-dd format.
//
boolean startAllDay = TimeUtils.isInAllDayFormat(dtstart);
boolean endAllDay = TimeUtils.isInAllDayFormat(dtend);
if (startAllDay) {
if (dtend == null) {
dtend = dtstart;
} else {
if (!endAllDay) {
try{
dtend = TimeUtils.convertDateFromTo(dtend, TimeUtils.PATTERN_YYYY_MM_DD);
} catch (java.text.ParseException e) {
throw new ConverterException("Error parsing date: " + e.getMessage());
}
}
}
}
//
//Note: with task the start date could be null. In this case, the
//task should be handled like an allday. Pay attention because also
//the due date could be null: in this case the task should not be
//handled like an allday.
//
if (dtstart == null && dtend != null) {
if (TimeUtils.isInAllDayFormat(dtend) ||
(dtend.endsWith("T000000" ) || dtend.endsWith("T235900")) ||
(dtend.endsWith("T000000Z") || dtend.endsWith("T235900Z"))) {
cc.setAllDay(Boolean.TRUE);
}
}
//
// We have to check if the dates are not in the DayFormat but are however
// relative to an all day event.
//
if (!cc.isAllDay()) {
boolean isAllDayEvent = false;
//
// Before to check the dates, we have to convert them in local format
// in order to have 00:00:00 as time for the middle night
//
String tmpDateStart = TimeUtils.convertUTCDateToLocal(dtstart, dtStartTimeZone);
String tmpDateEnd = TimeUtils.convertUTCDateToLocal(dtend, dtEndTimeZone);
//Android Sync Client can manage the X-FUNAMBOL-ALL-DAY field,
//so, this check is only performed if this calendar is a vTask
//If problem are encountered during vTask migration
//it is possible that this check is responsible of transforming
//the vTask to all day
isAllDayEvent = !isEvent && TimeUtils.isAllDayEvent(
tmpDateStart,
tmpDateEnd
);
if (isAllDayEvent) {
//
// Convert the dates in DayFormat
//
try{
dtstart =
TimeUtils.convertDateFromTo(tmpDateStart, TimeUtils.PATTERN_YYYY_MM_DD);
dtend =
TimeUtils.convertDateFromTo(tmpDateEnd, TimeUtils.PATTERN_YYYY_MM_DD);
} catch (java.text.ParseException e) {
throw new ParseException("Error parsing date: " + e.getMessage(), e.getErrorOffset());
}
cc.getDtStart().setPropertyValue(dtstart);
cc.getDtEnd().setPropertyValue(dtend);
cc.setAllDay(Boolean.TRUE);
} else {
//Android Sync Client can manage the X-FUNAMBOL-ALL-DAY field,
//so, this check is only performed if this calendar is a vTask
//If problems are encountered during vTask migration
//it is possible that this check is responsible of transforming
//the vTask to all day
if (!isEvent) {
isAllDayCheckingDuration(cc);
} else {
return;
}
}
}
} catch (java.text.ParseException e) {
throw new ConverterException("Error parsing date: " + e.getMessage());
}
}
/**
* Checks if the given dates are of an all day event checking if the duration
* is a multiple of 24 hour.
* The main problem is the end time is something like 23:59:59 so the difference
* is not 24 hour but 24 hour - 1 second.
* Another problem is about the day of the event because the date can be shifted
* because we may not have the timezone of the device. BTW, in order to find
* the day, we don't need the timezone but we need to know just if the timezone
* is with an offset positive or negative. And to know it, we check the time
* of the dtEnd.
* <br/>If an all day event is detected, the properties are set in the given
* calendar.
*
* KNOW ISSUE: this method fails with the timezone with an offset greater than
* 12 hours
*
* @param cc
* @throws Exception if an error occurs
*/
public void isAllDayCheckingDuration(CalendarContent cc) throws Exception {
String dtStart = cc.getDtStart().getPropertyValueAsString() ;
String dtEnd = cc.getDtEnd().getPropertyValueAsString() ;
if (dtStart == null || dtStart.length() == 0) {
cc.setAllDay(Boolean.FALSE);
return;
}
if (dtEnd == null || dtEnd.length() == 0) {
cc.setAllDay(Boolean.FALSE);
return;
}
//
// We replace end time 5900Z with 5959Z in order to check just 5959Z
//
dtEnd = dtEnd.replaceAll("5900Z", "5959Z");
//
// The date start must end with 00Z
//
if (!dtStart.endsWith("00Z")) {
cc.setAllDay(Boolean.FALSE);
return;
}
SimpleDateFormat formatter = new SimpleDateFormat(TimeUtils.PATTERN_UTC);
TimeZone tz = TimeZone.getTimeZone("UTC");
formatter.setLenient(false);
formatter.setTimeZone(tz);
Date dateStart = formatter.parse(dtStart);
Date dateEnd = formatter.parse(dtEnd);
long timeStart = dateStart.getTime();
long timeEnd = dateEnd.getTime();
if (dtEnd.endsWith("5959Z")) {
timeEnd = timeEnd + 1000; // we add a second because
// we'll check
// if the difference is 24H
// (we have already checked if
// the end time finished with
// 5959Z)
}
long diff = timeEnd - timeStart;
long sec = diff / 1000L;
if (sec == 0) {
cc.setAllDay(Boolean.FALSE);
return;
}
if (sec % TimeUtils.SECOND_IN_A_DAY != 0) {
cc.setAllDay(Boolean.FALSE);
return;
}
cc.setAllDay(Boolean.TRUE);
boolean isGMTPositive = false;
java.util.Calendar calStart =
java.util.Calendar.getInstance(TimeUtils.TIMEZONE_UTC);
calStart.setTime(dateStart);
calStart.setTimeZone(TimeUtils.TIMEZONE_UTC);
java.util.Calendar calEnd =
java.util.Calendar.getInstance(TimeUtils.TIMEZONE_UTC);
calEnd.setTime(dateEnd);
calEnd.setTimeZone(TimeUtils.TIMEZONE_UTC);
int hourEnd = calEnd.get(java.util.Calendar.HOUR_OF_DAY);
int minuteEnd = calEnd.get(java.util.Calendar.MINUTE);
int hourMinuteEnd = Integer.parseInt(String.valueOf(hourEnd) +
String.valueOf(minuteEnd));
if (hourMinuteEnd >= 1200 && hourMinuteEnd <= 2350) {
//
// Positive
//
isGMTPositive = true;
} else {
//
// Negative
//
isGMTPositive = false;
}
String allDayStart = null;
String allDayEnd = null;
if (isGMTPositive) {
//
// If the gmt is with an offset positive, the dtStart of the event is
// a day before so we have to add 1 day
//
calStart.add(java.util.Calendar.DATE, 1);
allDayStart = TimeUtils.convertDateTo(calStart.getTime(),
TimeUtils.TIMEZONE_UTC,
TimeUtils.PATTERN_YYYY_MM_DD);
allDayEnd = TimeUtils.convertDateTo(calEnd.getTime(),
TimeUtils.TIMEZONE_UTC,
TimeUtils.PATTERN_YYYY_MM_DD);
} else {
//
// If the gmt is with an offset negative, the end of the event is
// a day after so we have to subtract 1 day
//
calEnd.add(java.util.Calendar.DATE, -1);
//
// We have also to add 1 minute otherwise with the timezones with
// offset = 0 we fail
//
calEnd.add(java.util.Calendar.MINUTE, 1);
allDayStart = TimeUtils.convertDateTo(calStart.getTime(),
TimeUtils.TIMEZONE_UTC,
TimeUtils.PATTERN_YYYY_MM_DD);
allDayEnd = TimeUtils.convertDateTo(calEnd.getTime(),
TimeUtils.TIMEZONE_UTC,
TimeUtils.PATTERN_YYYY_MM_DD);
}
String dtStartTimeZoneID;
if (dtStartTimeZone == null) {
dtStartTimeZoneID = null;
} else {
dtStartTimeZoneID = dtStartTimeZone.getID();
}
cc.setDtStart(new PropertyWithTimeZone(allDayStart, dtStartTimeZoneID));
String dtEndTimeZoneID;
if (dtEndTimeZone == null) {
dtEndTimeZoneID = null;
} else {
dtEndTimeZoneID = dtEndTimeZone.getID();
}
cc.setDtEnd(new PropertyWithTimeZone(allDayEnd, dtEndTimeZoneID));
}
/**
* Converts the importance in the vCalendar scale (one to three, where
* three is the lowest priority) to the iCalendar-like scale (one to nine,
* where nine is the lowest priority) according to RFC 2445.
* NB: 3 is the lowest priority in many implementations of the vCalendar
* standard, that in itself does not prescibe a fixed value for the lowest
* priority level.
*
* @param oneToThree an int being 1 or 2 or 3
* @return an int in the [1; 9] range
* @throws NumberFormatException if the argument is out of range
*/
private int importance13To19(int oneToThree) throws NumberFormatException {
switch (oneToThree) {
case 1:
return 1;
case 2:
return 5;
case 3:
return 9;
default:
throw new NumberFormatException(); // will be caught
}
}
/**
* Converts the importance in the iCalendar scale (one to nine, where
* nine is the lowest priority) to the vCalendar scale (one to three,
* where three is the lowest priority) according to RFC 2445.
* NB: 3 is the lowest priority in many implementations of the vCalendar
* standard, that in itself does not prescibe a fixed value for the lowest
* priority level.
*
* @param oneToNine an int in the [1; 9] range
* @return an int being 1 or 2 or 3
* @throws NumberFormatException if the argument is out of range
*/
private int importance19To13(int oneToNine) throws NumberFormatException {
switch (oneToNine) {
case 1:
case 2:
case 3:
case 4:
return 1;
case 5:
return 2;
case 6:
case 7:
case 8:
case 9:
return 3;
default:
throw new NumberFormatException(); // will be caught
}
}
private Short busyStatusTo03(String busyStatus) {
if (busyStatus == null) {
return null;
}
if (BUSYSTATUS_FREE.equals(busyStatus)) {
return BUSYSTATUS_OLFREE;
}
if (BUSYSTATUS_TENTATIVE.equals(busyStatus)) {
return BUSYSTATUS_OLTENTATIVE;
}
if (BUSYSTATUS_BUSY.equals(busyStatus)) {
return BUSYSTATUS_OLBUSY;
}
if (BUSYSTATUS_OOF.equals(busyStatus)) { // out of office
return BUSYSTATUS_OLOOF;
}
return null;
}
private String busyStatusFrom03(Short zeroToThree) {
if (zeroToThree == null) {
return null; // undefined busy-status
}
switch(zeroToThree.shortValue()) {
case BUSYSTATUS_OLFREE:
return BUSYSTATUS_FREE;
case BUSYSTATUS_OLTENTATIVE:
return BUSYSTATUS_TENTATIVE;
case BUSYSTATUS_OLBUSY:
return BUSYSTATUS_BUSY;
case BUSYSTATUS_OLOOF:
return BUSYSTATUS_OOF;
default:
return null;
}
}
private short accessClassTo03(String accessClass) {
if (accessClass == null) {
return SENSITIVITY_NORMAL; // default
}
if (accessClass.equals(CLASS_PUBLIC)) {
return SENSITIVITY_NORMAL;
}
if (accessClass.equals(CLASS_PRIVATE)) {
return SENSITIVITY_PRIVATE;
}
if (accessClass.equals(CLASS_CONFIDENTIAL)) {
return SENSITIVITY_CONFIDENTIAL;
}
return SENSITIVITY_PERSONAL; // custom value
}
private String accessClassFrom03(Short zeroToThree) {
if (zeroToThree == null) {
return CLASS_PUBLIC;
}
switch(zeroToThree.shortValue()) {
case SENSITIVITY_PRIVATE:
return CLASS_PRIVATE;
case SENSITIVITY_CONFIDENTIAL:
return CLASS_CONFIDENTIAL;
case SENSITIVITY_PERSONAL:
return CLASS_CUSTOM;
case SENSITIVITY_NORMAL:
default:
return CLASS_PUBLIC;
}
}
/**
* Converts the given aalarm string in a Reminder object
* @param dtStart the event's start date
* @param aalarm the aalarm string
* @return the Reminder object built according to the given params
*/
private Reminder convertAAlarmToReminder(boolean isAllday,
Property dtStart,
String aalarm) {
if (aalarm == null) {
return null;
}
Reminder reminder = new Reminder();
reminder.setActive(false);
//
// Splits aalarm considering the eventual spaces before or after ';'
// like part of the token to search: this because some phones send the
// values of the AALARM property with space at the beginning of the
// value.
// For example
// AALARM;TYPE=WAVE;VALUE=URL:20070415T235900; ; ; file:///mmedia/taps.wav
//
String[] values = aalarm.split("( )*;( )*");
int cont = 0;
for (String value: values) {
switch (cont++) {
case 0:
//
// The first token is the date
//
if (value == null || "".equals(value)) {
// The date is empty
break;
}
try {
setAlarmTimeAndMinutes(reminder,
value ,
dtStart ,
isAllday);
} catch (Exception e) {
//
// Something went wrong
//
reminder.setActive(false);
return reminder;
}
break;
case 1:
//
// The second token is the duration
//
if (value == null || "".equals(value)) {
// The duration is empty
break;
}
reminder.setInterval(TimeUtils.getAlarmInterval(value));
break;
case 2:
//
// The third token is the repeat count
//
if (value == null || "".equals(value)) {
// The repeat count is empty
break;
}
reminder.setRepeatCount(Integer.parseInt(value));
break;
case 3:
//
// The fourth token is the sound file
//
if (value == null || "".equals(value)) {
// The sound file is empty
break;
}
reminder.setSoundFile(value);
break;
default:
return reminder;
}
}
return reminder;
}
/**
* Convert AALARM to Reminder object when the calendar is an all day event
* and Timezone is not set and the start date is not null.
*
* @param originalDtstart start date before convertion
* @param convertedDtstart start date after convertion
* @param aalarm the aalarm string
*/
private Reminder convertAAlarmToReminderBasedOnMinutes(
String originalDtstart,
String convertedDtstart,
String aalarm) {
if (aalarm == null) {
return null;
}
Reminder reminder = new Reminder();
reminder.setActive(false);
//
// Splits aalarm considering the eventual spaces before or after ';'
// like part of the token to search: this because some phones send the
// values of the AALARM property with space at the beginning of the
// value.
// For example
// AALARM;TYPE=WAVE;VALUE=URL:20070415T235900; ; ; file:///mmedia/taps.wav
//
String[] values = aalarm.split("( )*;( )*");
int cont = 0;
for (String value: values) {
switch (cont++) {
case 0:
//
// The first token is the date
//
if (value == null || "".equals(value)) {
// The date is empty
break;
}
try {
//
// Uses start date converted to local time to calculate
// minutes
//
reminder.setMinutes(TimeUtils.getAlarmMinutes(
originalDtstart,
value,
null)
);
//
// Uses original start date minus reminder minutes
// (calculated previously) to set the reminder time.
// In this way, the reminder time is calculated like
// relative moment to the timezone.
//
convertedDtstart =
TimeUtils.convertDateFromTo(convertedDtstart, TimeUtils.PATTERN_YYYY_MM_DD);
java.util.Calendar calAlarm =
java.util.Calendar.getInstance();
SimpleDateFormat formatter =
new SimpleDateFormat(TimeUtils.PATTERN_YYYY_MM_DD_HH_MM_SS);
calAlarm.setTime(
formatter.parse(convertedDtstart + " 00:00:00"));
calAlarm.add(java.util.Calendar.MINUTE,
-reminder.getMinutes());
formatter =
new SimpleDateFormat(TimeUtils.PATTERN_UTC_WOZ);
Date dtAlarm = calAlarm.getTime();
reminder.setTime(formatter.format(dtAlarm));
reminder.setActive(true);
} catch (Exception e) {
//
// Something went wrong
//
reminder.setActive(false);
return reminder;
}
break;
case 1:
//
// The second token is the duration
//
if (value == null || "".equals(value)) {
// The duration is empty
break;
}
reminder.setInterval(TimeUtils.getAlarmInterval(value));
break;
case 2:
//
// The third token is the repeat count
//
if (value == null || "".equals(value)) {
// The repeat count is empty
break;
}
reminder.setRepeatCount(Integer.parseInt(value));
break;
case 3:
//
// The fourth token is the sound file
//
if (value == null || "".equals(value)) {
// The sound file is empty
break;
}
reminder.setSoundFile(value);
break;
default:
return reminder;
}
}
return reminder;
}
/**
* Extracts roughly a time interval large enough to contain the whole
* event/task and, in case it's a recurrent one, all its occurrences.
* To be used just with iCalendar (vCalendar 2.0) items.
*
* @param vcc the VCalendarContent object to be quickly parsed
* @return an array of long integers, the first one being the lower end of
* the interval and the other one the upper end
*/
public long[] extractInterval(VCalendarContent vcc) {
String low = null;
if (vcc.getProperty("DTSTART") != null) {
low = vcc.getProperty("DTSTART").getValue();
}
if ((low == null) || ("".equals(low))) {
if (vcc.getProperty("DTEND") != null) {
low = vcc.getProperty("DTEND").getValue();
}
}
if ((low == null) || ("".equals(low))) {
if (vcc.getProperty("DUE") != null) {
low = vcc.getProperty("DUE").getValue();
}
}
long from, to;
if ((low == null) || ("".equals(low))) {
from = DEFAULT_FROM;
} else {
try {
from = TimeUtils.getMidnightTime(low);
} catch (ParseException e) {
from = DEFAULT_FROM;
}
}
String rrule;
if (vcc.getProperty("RRULE") != null) {
rrule = vcc.getProperty("RRULE").getValue();
Pattern pattern = Pattern.compile(
"UNTIL=([0-9]{4}([\\-])?[0-1][0-9]([\\-])?[0-3][0-9])");
Matcher matcher = pattern.matcher(rrule);
if (matcher.find()) { // has an end date
String high = matcher.group(1);
try {
to = TimeUtils.getMidnightTime(high) + ONE_DAY;
} catch (ParseException e) {
to = DEFAULT_TO;
}
} else { // has no end date
pattern = Pattern.compile("COUNT=([0-9]+)");
matcher = pattern.matcher(rrule);
if (matcher.find()) { // has an occurrence limit
int count = Integer.parseInt(matcher.group(1));
pattern = Pattern.compile("FREQ=([A-Z]+LY)");
matcher = pattern.matcher(rrule);
if (matcher.find()) { // has a frequency
String freq = matcher.group(1);
int period = 0;
if (DAILY.equalsIgnoreCase(freq)) {
period = 1;
} else if (WEEKLY.equalsIgnoreCase(freq)) {
period = 7;
} else if (MONTHLY.equalsIgnoreCase(freq)) {
period = 31;
} else if (YEARLY.equalsIgnoreCase(freq)) {
period = 366;
}
if (period != 0) { // frequency was recognized
pattern = Pattern.compile("INTERVAL=([0-9]+)");
matcher = pattern.matcher(rrule);
if (matcher.find()) { // has an interval
period *= Integer.parseInt(matcher.group(1));
} // else, no problem
to = from + (period * count * ONE_DAY);
} else { // frequency was not recognized
to = DEFAULT_TO;
}
} else { // has no frequency
to = DEFAULT_TO;
}
} else { // is unlimited
to = DEFAULT_TO_UNLIMITED;
}
}
List<com.funambol.common.pim.model.model.Property> rdates =
vcc.getProperties("RDATE");
for (com.funambol.common.pim.model.model.Property property : rdates) {
String rdate = property.getValue();
try {
long extra = TimeUtils.getMidnightTime(rdate) + ONE_DAY;
if (extra > to) {
to = extra;
}
} catch (ParseException e) {
// Ignores this RDATE
}
}
} else {
to = DEFAULT_TO;
}
if (from > DEFAULT_FROM) {
from = DEFAULT_FROM;
}
if (to < DEFAULT_TO) {
to = DEFAULT_TO;
}
return new long[]{from, to};
}
}