/*
Copyright 2007 primosync
Licensed under the Apache 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.apache.org/licenses/LICENSE-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.
*
*
* * Changes:
* --/nov/2007 Agustin
* -equals: implemented
* -parse: now timezone information is parsed and used to transform start and end time to GMT time
* -processTZ_START: added; used in parse to parse timezone information
* -compareExceptDatesArray: added; used in equals
* dec/2007 Agstin
* -getRepeatRuleInfo, repeatRuleEquals: added to check if a RepeatRule is set correctly in an Event
*/
package com.primosync.cal;
import com.primosync.log.GCalException;
import com.primosync.util.*;
import com.primosync.store.Store;
import java.util.Date;
import java.util.Enumeration;
import java.util.TimeZone;
import java.util.Vector;
import javax.microedition.pim.RepeatRule;
import harmony.java.util.StringTokenizer;
import java.util.Calendar;
import java.util.Hashtable;
/**
* A crude iCalendar data parser, implemented in the absence of
* J2ME regular expression engine
*
* @author batcage
* @version 1.0.0
* @see iCalendar standard RFC 2455
* <link>http://www.ietf.org/rfc/rfc2445.txt)</link>
* @since JDK1.6.0, WTK2.5-Beta2
*/
public class Recurrence {
int frequency; //type of repeat rule
int interval; //how often rule repeats
int count; //number of occurrences
int monthInYear; //month(s) in the year that event occurs
int dayInWeek; //day(s) in the week that event occurs
int weekInMonth; //week(s) in month that event occurs
int dayInMonth; //day(s) in month that event occurs
int dayInYear; //day(s) in the year that event occurs
int tzOffset; //time zone hour offset from GMT
long expirationDate; //date that recurrence ends in milliseconds
long startTime; //event start date/time in milliseconds
long endTime; //event end date/time in milliseconds
long duration; //event duration
String tzCodeStd; //standard time zone name (abbreviation) e.g. "EST"
String tzCodeDay; //daylight time zone name (abbreviation) e.g. "EDT"
String tzName; //time zone ID (full length) e.g. "America/New_York"
RepeatRule repeatRule; //PIM Repeat Rule
Date[] exceptDates; //exception dates
boolean phoneSupportsThisRecurrence = true; //tells if this recurrence is compleatly compatible with
//the phone (a recurrence is not compleatly compatible if some
//fields are not accepted or if there has been any error setting some fields)
/**
* Constructor
*/
public Recurrence() {
this.exceptDates = null;
this.tzCodeStd = null;
this.tzCodeDay = null;
this.tzName = null;
this.interval = 1;
}
/**
* Constructor that takes an iCal data string as a param
*
* @param rule repeat rule string in iCal format (RFC 2455)
*/
public Recurrence(String rule) {
this();
this.parse(rule);
}
/**
* Constructor that takes a PIM repeat rule structure as a param
*
* @param rule PIM repeat rule
*/
public Recurrence(RepeatRule rule) {
this(rule, 0, 0);
}
/**
* Constructor that takes a PIM repeat rule structure, starting
* and end times of the recurrent event
*
* @param rule PIM repeat rule
* @param startTime starting time of recurrent event
* @param endTime end time of recurring event
*/
public Recurrence(RepeatRule rule, long startTime, long endTime) {
this();
this.startTime = startTime;
this.endTime = endTime;
this.repeatRule = rule;
this.parse(rule);
}
/**
* Gets the PIM repeat rule for this recurrent event
*
* @returns PIM repeat rule
*/
public RepeatRule getRepeat() {
return this.repeatRule;
}
/**
* Tells if the phone supports this recurrence
* @return If this phone compleatly supports this recurrence
*/
public boolean doesPhoneSupportsThisRecurrence() {
return phoneSupportsThisRecurrence;
}
/**
* Adds an exception date to the PIM repeat rule
*
* @param date exception date in milliseconds since 1970 January
* 1
*/
public void addExceptDate(long date) {
if (this.repeatRule != null) this.repeatRule.addExceptDate(date);
}
/**
* Gets the recurring event's start date/time
*
* @returns start date/time in milliseconds since 1970 January 1
*/
public long getStartDateTime() {
return this.startTime;
}
/**
* Gets the recurring event's end date/time
*
* @returns end date/time in milliseconds since 1970 January 1
*/
public long getEndDateTime() {
return this.endTime;
}
/**
* Gets the recurring event's expiration date/time
*
* @returns expiration date/time in milliseconds since 1970
* January 1
*/
public long getExpiration() {
return this.expirationDate;
}
/**
* Gets the name of the time zone at which the recurring event
* occurs
*
* @returns full length name of time zone
* @example "America/New_York"
*/
public String getTimeZoneName() {
return this.tzName;
}
/**
* Sets the name of the time zone at which the recurring event
* occurs
*
* @param full length name of time zone
* @example "America/New_York"
*/
public void setTimeZoneName(String name) {
this.tzName = name;
}
/**
* Parses all assigned fields, including frequency, interval, and
* exception dates, from specified PIM repeat rule
*
* @param PIM repeat rule to parse
* @throws IllegalArgumentException if <code>rule</code> is
* <code>null</code>
*/
public void parse(RepeatRule rule) {
int[] fields;
if (rule != null) {
//get all assigned fields
fields = rule.getFields();
for (int i=0; i<fields.length; i++) {
try {
switch (fields[i]) {
case RepeatRule.FREQUENCY:
this.frequency = rule.getInt(RepeatRule.FREQUENCY);
break;
case RepeatRule.INTERVAL:
this.interval = rule.getInt(RepeatRule.INTERVAL);
break;
case RepeatRule.COUNT:
this.count = rule.getInt(RepeatRule.COUNT);
break;
case RepeatRule.END:
this.expirationDate = rule.getDate(RepeatRule.END);
break;
case RepeatRule.MONTH_IN_YEAR:
this.monthInYear = rule.getInt(RepeatRule.MONTH_IN_YEAR);
break;
case RepeatRule.DAY_IN_WEEK:
this.dayInWeek = rule.getInt(RepeatRule.DAY_IN_WEEK);
break;
case RepeatRule.WEEK_IN_MONTH:
this.weekInMonth = rule.getInt(RepeatRule.WEEK_IN_MONTH);
break;
case RepeatRule.DAY_IN_MONTH:
this.dayInMonth = rule.getInt(RepeatRule.DAY_IN_MONTH);
break;
case RepeatRule.DAY_IN_YEAR:
this.dayInYear = rule.getInt(RepeatRule.DAY_IN_YEAR);
break;
default: break;
}
} catch (Exception e) { }
}
try {
//get exception dates
Enumeration exDates = rule.getExceptDates();
if (exDates.hasMoreElements()) {
Vector exDatesVector = new Vector();
while (exDates.hasMoreElements()) exDatesVector.addElement(exDates.nextElement());
exceptDates = new Date[exDatesVector.size()];
exDatesVector.copyInto(exceptDates);
}
} catch (Exception e){};
} else
throw new IllegalArgumentException("null RepeatRule");
}
/**
* Parses specified iCal repeat rule string
*
* @param rule repeat rule in iCal format (RFC 2455)
* @throws IllegalArgumentException if <code>rule</code> is
* <code>null</code>
*/
public void parse(String rule) {
if (rule == null) throw new IllegalArgumentException("null RepeatRule");
//break string up by newlines or spaces, each line is a field of the rule
StringTokenizer fields = new StringTokenizer(rule, "\n ");
StringTokenizer subFields;
String field;
boolean stdRule = false;
boolean dayRule = false;
int i = 0;
Calendar stdTimezoneStart = null;
Calendar dayTimezoneStart = null;
int stdTimezoneOffset = 0;
int dayTimezoneOffset = 0;
while (fields.hasMoreTokens()) {
//break each field into subfields if possible
field = fields.nextToken();
subFields = new StringTokenizer(field, ";:");
//process the main fields
if (field.startsWith("DTSTART")) {
if (stdRule == false && dayRule == false)
processDTSTART(field, subFields);
//get time zones start time
if (stdRule) stdTimezoneStart = processTZ_START(field, subFields);
if (dayRule) dayTimezoneStart = processTZ_START(field, subFields);
} else if (field.startsWith("DTEND")) {
if (stdRule == false && dayRule == false)
processDTEND(field, subFields);
} else if (field.startsWith("DURATION:")) {
processDURATION(field, subFields);
} else if (field.startsWith("RRULE:")) {
//do not process the repeat rules from the
// time zone rules
if (stdRule == false && dayRule == false)
processRRULE(field, subFields);
} else if (field.startsWith("TZNAME:")) {
if (stdRule || dayRule)
processTZNAME(field, subFields, stdRule);
} else if (field.startsWith("TZOFFSETTO:")) {
//get time zone offsets
if (stdRule) stdTimezoneOffset = processTZOFFSET(field, subFields);
if (dayRule) dayTimezoneOffset = processTZOFFSET(field, subFields);
}else if (field.startsWith("BEGIN:STANDARD")) {
stdRule = true;
dayRule = false;
} else if (field.startsWith("BEGIN:DAYLIGHT")) {
stdRule = false;
dayRule = true;
} else if (field.startsWith("END:STANDARD")) {
stdRule = false;
} else if (field.startsWith("END:DAYLIGHT")) {
dayRule = false;
}
//else ignore other fields since we don't need them
}
//get the right time offset depending on if we are in standard time or daylight saving time
this.tzOffset = stdTimezoneOffset;
//try to identify if we are in daylight saving time
if(stdTimezoneStart != null && dayTimezoneStart != null) {
Calendar startCalendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
startCalendar.setTime(new Date(this.startTime));
startCalendar.set(Calendar.YEAR, 1970);
if(stdTimezoneStart.before(dayTimezoneStart) ) {
if(startCalendar.before(stdTimezoneStart) || startCalendar.after(dayTimezoneStart)) {
this.tzOffset = dayTimezoneOffset;
}
} else {
if(startCalendar.after(dayTimezoneStart) && startCalendar.before(stdTimezoneStart)) {
this.tzOffset = dayTimezoneOffset;
}
}
}
//fix the start time and end time to be in GMT time
this.startTime -= this.tzOffset;
this.endTime -= this.tzOffset;
this.repeatRule.setDate(RepeatRule.END, this.expirationDate);
}
/**
* Gets the iCal representation of the repeat rule if populated
*
* @returns iCal repeat rule string (RFC 2455). String is empty
* if repeat rule is not populated.
*/
public String toString() {
StringBuffer sb = new StringBuffer();
String rval = "";
try {
//rule must always start with "DTSTART;"
getDTSTART(sb, false);//getDTSTART(sb, true);
//DTEND or DURATION required
getDTEND(sb, false);//getDTEND(sb, true);
//RRULE required
getRRULE(sb);
//EXDATE required only if exception dates exist
getEXDATE(sb);
//VTIMEZONE required
getVTIMEZONE(sb);
rval = sb.toString().replace(' ', '\n');
} catch (Exception e) {}
return rval;
}
/**
* Gets a readable string for the repeat rule, including
* frequency and exception dates
*
* @returns repeat rule in readable format. String is empty if
* repeat rule is not populated.
*/
public String toReadableString() throws Exception {
try {
StringBuffer sb = new StringBuffer();
getReadableRule(sb);
getReadableExceptions(sb);
return sb.toString();
}catch(Exception e) {
throw new GCalException(this.getClass(), "toReadableString", e);
}
}
/**
* Appends specified DateTime value to the given string buffer
*
* @param sb string buffer to which the DateTime value is
* appended
* @param includeTZID if true, includes time zone ID
* @param startEnd if "DTSTART", gets the start time of the
* repeat rule; if "DTEND", gets the end time
*/
void getDT(StringBuffer sb, boolean includeTZID, String startEnd) {
long time;
time = startEnd.equals("DTSTART") ? this.startTime : this.endTime;
sb.append(startEnd);
if (time > 0) {
//include option time zone ID
if (this.tzName != null && includeTZID) sb.append(";TZID=" + this.tzName);
else sb.append(";VALUE=DATE-TIME");
//append date time string
sb.append(":" + DateUtil.longToDateTimeGMT(time) + "\n");
} else {
sb.append(";VALUE=DATE:" + DateUtil.longToDateGMT(time) + "\n");
}
}
/**
* Appends start time of repeat rule to the given string buffer
*
* @param sb string buffer to which start time is appended
* @param includeTZID if true, includes time zone ID
*/
void getDTSTART(StringBuffer sb, boolean includeTZID) {
getDT(sb, includeTZID, "DTSTART");
}
/**
* Appends end time of repeat rule to the given string buffer
*
* @param sb string buffer to which end time is appended
* @param includeTZID if true, includes time zone ID
*/
void getDTEND(StringBuffer sb, boolean includeTZID) {
getDT(sb, includeTZID, "DTEND");
}
/**
* Appends event duration of repeat rule to the given string
* buffer
*
* @param sb string buffer to which duration is appended
*/
void getDURATION(StringBuffer sb) {
//get event duration in seconds
long time = this.endTime - this.startTime;
time /= 1000;
sb.append("DURATION:PT" + time + "S\n");
}
/**
* Appends repeat rule info, including frequency and interval, to
* the given string buffer
*
* @param sb string buffer to which rule is appended
*/
void getRRULE(StringBuffer sb) {
if (this.frequency != 0) {
sb.append("RRULE:FREQ=");
//FREQ
switch (this.frequency) {
case RepeatRule.YEARLY: sb.append("YEARLY"); break;
case RepeatRule.MONTHLY: sb.append("MONTHLY"); break;
case RepeatRule.WEEKLY: sb.append("WEEKLY"); break;
case RepeatRule.DAILY: sb.append("DAILY"); break;
default: break;
}
//INTERVAL
if (this.interval != 0) sb.append(";INTERVAL=" + this.interval);
//COUNT
if (this.count != 0) sb.append(";COUNT=" + this.count);
//BYDAY
if (this.dayInWeek != 0) {
Vector dayVector = new Vector();
sb.append(";BYDAY=");
//get list of days into vector
if ((this.dayInWeek & RepeatRule.SUNDAY)!=0) dayVector.addElement("SU");
if ((this.dayInWeek & RepeatRule.MONDAY)!=0) dayVector.addElement("MO");
if ((this.dayInWeek & RepeatRule.TUESDAY)!=0) dayVector.addElement("TU");
if ((this.dayInWeek & RepeatRule.WEDNESDAY)!=0) dayVector.addElement("WE");
if ((this.dayInWeek & RepeatRule.THURSDAY)!=0) dayVector.addElement("TH");
if ((this.dayInWeek & RepeatRule.FRIDAY)!=0) dayVector.addElement("FR");
if ((this.dayInWeek & RepeatRule.SATURDAY)!=0) dayVector.addElement("SA");
//assemble vector elements into comma separated list
for (int i=0; i<dayVector.size(); i++) {
if (i != 0) sb.append(",");
sb.append((String)dayVector.elementAt(i));
}
}
//BYWEEK
if (this.weekInMonth != 0) {
sb.append(";BYWEEK=");
switch (this.weekInMonth) {
case RepeatRule.FIRST: sb.append("1"); break;
case RepeatRule.SECOND: sb.append("2"); break;
case RepeatRule.THIRD: sb.append("3"); break;
case RepeatRule.FOURTH: sb.append("4"); break;
case RepeatRule.FIFTH: sb.append("5"); break;
case RepeatRule.SECONDLAST: sb.append("-1"); break;
case RepeatRule.THIRDLAST: sb.append("-2"); break;
case RepeatRule.FOURTHLAST: sb.append("-3"); break;
case RepeatRule.FIFTHLAST: sb.append("-4"); break;
default: break;
}
}
//BYMONTH
if (this.monthInYear != 0) {
Vector moVector = new Vector();
sb.append(";BYMONTH=");
//get list of months into vector
if ((this.monthInYear & RepeatRule.JANUARY)!=0) moVector.addElement("1");
if ((this.monthInYear & RepeatRule.FEBRUARY)!=0) moVector.addElement("2");
if ((this.monthInYear & RepeatRule.MARCH)!=0) moVector.addElement("3");
if ((this.monthInYear & RepeatRule.APRIL)!=0) moVector.addElement("4");
if ((this.monthInYear & RepeatRule.MAY)!=0) moVector.addElement("5");
if ((this.monthInYear & RepeatRule.JUNE)!=0) moVector.addElement("6");
if ((this.monthInYear & RepeatRule.JULY)!=0) moVector.addElement("7");
if ((this.monthInYear & RepeatRule.AUGUST)!=0) moVector.addElement("8");
if ((this.monthInYear & RepeatRule.SEPTEMBER)!=0) moVector.addElement("9");
if ((this.monthInYear & RepeatRule.OCTOBER)!=0) moVector.addElement("10");
if ((this.monthInYear & RepeatRule.NOVEMBER)!=0) moVector.addElement("11");
if ((this.monthInYear & RepeatRule.DECEMBER)!=0) moVector.addElement("12");
//assemble vector elements into comma separated list
for (int i=0; i<moVector.size(); i++) {
if (i != 0) sb.append(",");
sb.append((String)moVector.elementAt(i));
}
}
//UNTIL
//Note: Google Calendar has a bug where the UNTIL date-time must end with UTC indicator
// or must not contain time or else the recurring event is not created properly; the
// event gets created on the start date only even though the event details show the repeat rule.
if (this.expirationDate != 0) sb.append(";UNTIL=" + DateUtil.longToDateTimeGMT(expirationDate));
sb.append("\n");
}
}
/**
* Gets a readable string for the repeat rule
*
* @param sb string buffer to which repeat rule is appended
*/
void getReadableRule(StringBuffer sb) throws Exception {
try {
if (this.frequency != 0) {
//INTERVAL
sb.append("Every ");
if (this.interval > 1) sb.append(this.interval + " ");
switch (this.frequency) {
case RepeatRule.YEARLY: sb.append("year"); break;
case RepeatRule.MONTHLY: sb.append("month"); break;
case RepeatRule.WEEKLY: sb.append("week"); break;
case RepeatRule.DAILY: sb.append("day"); break;
default: sb.append("time"); break;
}
if (this.interval > 1) sb.append("s");
//COUNT
if (this.count != 0) {
sb.append("; " + this.count + " time");
if (this.count > 1) sb.append("s");
}
//BYDAY
if (this.dayInWeek != 0) {
Vector dayVector = new Vector();
//get list of days into vector
if ((this.dayInWeek & RepeatRule.SUNDAY)!=0) dayVector.addElement("SU");
if ((this.dayInWeek & RepeatRule.MONDAY)!=0) dayVector.addElement("MO");
if ((this.dayInWeek & RepeatRule.TUESDAY)!=0) dayVector.addElement("TU");
if ((this.dayInWeek & RepeatRule.WEDNESDAY)!=0) dayVector.addElement("WE");
if ((this.dayInWeek & RepeatRule.THURSDAY)!=0) dayVector.addElement("TH");
if ((this.dayInWeek & RepeatRule.FRIDAY)!=0) dayVector.addElement("FR");
if ((this.dayInWeek & RepeatRule.SATURDAY)!=0) dayVector.addElement("SA");
//assemble vector elements into comma separated list
if (dayVector.size() != 0) sb.append("; on ");
for (int i=0; i<dayVector.size(); i++) {
if (i!=0) sb.append(",");
sb.append((String)dayVector.elementAt(i));
}
}
//BYWEEK
if (this.weekInMonth != 0) {
sb.append("; on the ");
switch (this.weekInMonth) {
case RepeatRule.FIRST: sb.append("1st"); break;
case RepeatRule.SECOND: sb.append("2nd"); break;
case RepeatRule.THIRD: sb.append("3rd"); break;
case RepeatRule.FOURTH: sb.append("4th"); break;
case RepeatRule.FIFTH: sb.append("5th"); break;
case RepeatRule.SECONDLAST: sb.append("2nd to last"); break;
case RepeatRule.THIRDLAST: sb.append("3rd to last"); break;
case RepeatRule.FOURTHLAST: sb.append("4th to last"); break;
case RepeatRule.FIFTHLAST: sb.append("5th to last"); break;
default: sb.append("?"); break;
}
sb.append(" week");
}
//BYMONTH
if (this.monthInYear != 0) {
Vector moVector = new Vector();
//get list of months into vector
if ((this.monthInYear & RepeatRule.JANUARY)!=0) moVector.addElement("Jan");
if ((this.monthInYear & RepeatRule.FEBRUARY)!=0) moVector.addElement("Feb");
if ((this.monthInYear & RepeatRule.MARCH)!=0) moVector.addElement("Mar");
if ((this.monthInYear & RepeatRule.APRIL)!=0) moVector.addElement("Apr");
if ((this.monthInYear & RepeatRule.MAY)!=0) moVector.addElement("May");
if ((this.monthInYear & RepeatRule.JUNE)!=0) moVector.addElement("Jun");
if ((this.monthInYear & RepeatRule.JULY)!=0) moVector.addElement("Jul");
if ((this.monthInYear & RepeatRule.AUGUST)!=0) moVector.addElement("Aug");
if ((this.monthInYear & RepeatRule.SEPTEMBER)!=0) moVector.addElement("Sep");
if ((this.monthInYear & RepeatRule.OCTOBER)!=0) moVector.addElement("Oct");
if ((this.monthInYear & RepeatRule.NOVEMBER)!=0) moVector.addElement("Nov");
if ((this.monthInYear & RepeatRule.DECEMBER)!=0) moVector.addElement("Dec");
//assemble vector elements into comma separated list
sb.append("; on ");
for (int i=0; i<moVector.size(); i++) {
if (i != 0) sb.append(", ");
sb.append((String)moVector.elementAt(i));
}
}
//UNTIL
if (this.expirationDate != 0) {
sb.append("; until " +
DateUtil.formatTimeGMT(this.expirationDate+Store.getOptions().uploadTimeZoneOffset,
true,
DateUtil.MONTH_MASK | DateUtil.DAY_MASK | DateUtil.YEAR_MASK));
}
}
}catch(Exception e) {
throw new GCalException(this.getClass(), "getReadableRule", e);
}
}
/**
* Appends exception dates of repeat rule to the given string
* buffer
*
* @param sb string buffer to which exception dates are appended
*/
void getEXDATE(StringBuffer sb) {
copyExceptions();
if (this.exceptDates != null && this.exceptDates.length != 0) {
sb.append("EXDATE:");
for (int i=0; i<this.exceptDates.length; i++) {
if (i != 0) sb.append(",");
sb.append(DateUtil.longToDateTime(this.exceptDates[i].getTime()));
}
sb.append("\n");
}
}
/**
* Copies the exception dates from the repeat rule into local
* variable. This is necessary for retreiving the ex dates when
* they are added manually instead of being parsed from the
* repeat rule.
*/
void copyExceptions() {
if (this.repeatRule != null) {
Vector dates = new Vector();
Enumeration exDates = this.repeatRule.getExceptDates();
while (exDates.hasMoreElements())
dates.addElement(exDates.nextElement());
this.exceptDates = new Date[dates.size()];
dates.copyInto(this.exceptDates);
}
}
/**
* Gets a readable string for the repeat rule's exception dates
*
* @param sb string buffer to which exception dates are appended
*/
void getReadableExceptions(StringBuffer sb) throws Exception {
try {
copyExceptions();
if (this.exceptDates != null && this.exceptDates.length != 0) {
sb.append("; except on ");
for (int i=0; i<this.exceptDates.length; i++) {
if (i != 0) sb.append(", ");
sb.append(DateUtil.formatTime(this.exceptDates[i].getTime()+Store.getOptions().uploadTimeZoneOffset,
true,
DateUtil.MONTH_MASK | DateUtil.DAY_MASK | DateUtil.YEAR_MASK));
}
}
}catch(Exception e) {
throw new GCalException(this.getClass(), "getReadableExceptions", e);
}
}
/**
* Appends time zone rules of repeat rule to the given string
* buffer
*
* @param sb string buffer to which time zone rules are appended
*/
void getVTIMEZONE(StringBuffer sb) {
String tznm;
sb.append("BEGIN:VTIMEZONE\n");
//TODO: get phone's time zone as default if <this.tzName> is null
if (this.tzName != null) tznm = this.tzName;
else tznm = "Universal_Time_Coordinated";
sb.append("TZID:" + tznm + "\nX-LIC-LOCATION:" + tznm + "\n");
//get standard time zone rule
getVTIME(sb, true);
//get daylight time zone rule
getVTIME(sb, false);
sb.append("END:VTIMEZONE\n");
}
/**
* Appends specified time zone rule of repeat rule to the given
* string buffer
*
* @param sb string buffer to which time zone is appended
* @param standard if true, gets the standard time zone rule; if
* false, gets the daylight saving time zone rule
*/
void getVTIME(StringBuffer sb, boolean standard) {
if (standard)sb.append("BEGIN:STANDARD\n");
else sb.append("BEGIN:DAYLIGHT\n");
String tzCode = (standard ? this.tzCodeStd : this.tzCodeDay);
sb.append("TZOFFSETFROM:");
getFormattedTime(sb, (standard ? this.tzOffset+100 : this.tzOffset));
sb.append("\nTZOFFSETTO:");
//Note: Not sure if this is a Google Calendar bug...
//Assume time zone is US Eastern and in Daylight Saving Time...
// If uploading a repeating event with TZOFFSETFROM/TZOFFSETTO different
// for each zone rule (FROM: -0400 TO: -0500 for standard and FROM: -0500
// TO: -0400 for daylight), the event's start time is 1 hour early.
// The repeat rule downloaded from Google Calendar follows this formula
// for daylight saving time:
// "TZOFFSETFROM: [STDTIMEOFFSET] TZOFFSETTO: [STDTIMEOFFSET+1hr]".
// Match this bug's behavior so that our events are uploaded
// at the right start time.
//getFormattedTime(sb, (standard ? this.tzOffset : this.tzOffset+100));
getFormattedTime(sb, (standard ? this.tzOffset+100 : this.tzOffset));
sb.append("\n");
if (tzCode != null) sb.append("TZNAME:" + tzCode + "\n");
//don't include time zone info for this DTSTART
getDTSTART(sb, false);
getRRULE(sb);
if (standard) sb.append("END:STANDARD\n");
else sb.append("END:DAYLIGHT\n");
}
/**
* Appends a 4-digit representation of the specified time to the
* given string buffer
*
* @param sb string buffer to which formatted time is appended
* @param time number of hours * 100 + number of minutes, ex: 1
* hour 40 minutes is 140
* @example -400 -> -0400<br>230 -> +0230
*/
void getFormattedTime(StringBuffer sb, int time) {
//pad if shorter than 4 digits
if (Math.abs(time) < 1000) {
if (time < 0) sb.append("-");
else sb.append("+");
if (time == 0)
sb.append("0000");
else
sb.append("0" + Math.abs(time));
} else
sb.append(Integer.toString(time));
}
/**
* Processes DateTime values (i.e. DTSTART and DTEND) from a iCal
* repeat rule string
*
* @param startEnd if "DTSTART", assigns parsed time to the
* starting time of recurring event; if "DTEND",
* assigns parsed time to the end time
* @param field string that contains the DateTime data in iCal
* format
* @param subFields all tokens of <code>field</code>
*/
void processDT(String startEnd, String field, StringTokenizer subFields) {
String subField;
int idx;
while (subFields.hasMoreTokens()) {
subField = subFields.nextToken();
//Ignore Start/End field identifier and Value field
if (subField.equals(startEnd) || subField.startsWith("VALUE")) {
}
//Time Zone ID
else if (subField.startsWith("TZID")) {
this.tzName = getFieldValue(subField, "TZID");
}
//Start/End time
else {
try {
long date = DateUtil.dateToLong(subField);
if (startEnd.equals("DTSTART")) {
this.startTime = date;
} else {
//this end time is not the same as the repeat rule's expiration time
this.endTime = date;
}
} catch (Exception e){}
}
}
}
/**
* Processes starting time from given iCal repeat rule string
*
* @param field string that contains start time in iCal format
* @param subFields all tokens of <code>field</code>
* @example <code>field</code>: <strong>
* "DTSTART;TZID=America/New_York:20070101T173511"</strong><br>
* <code>subFields[0]</code>: "DTSTART"<br>
* <code>subFields[1]</code>: "TZID=America/New_York"<br>
* <code>subFields[2]</code>: "20070101T173511"
*/
void processDTSTART(String field, StringTokenizer subFields) {
processDT("DTSTART", field, subFields);
}
/**
* Processes end time from given iCal repeat rule string
*
* @param field string that contains end time in iCal format
* @param subFields all tokens of <code>field</code>
* @example <code>field</code>:
* "DTEND;TZID=America/New_York:20070101T173511"<br>
* <code>subFields[0]</code>: "DTEND"<br>
* <code>subFields[1]</code>: "TZID=America/New_York"<br>
* <code>subFields[2]</code>: "20070101T173511"
*/
void processDTEND(String field, StringTokenizer subFields) {
processDT("DTEND", field, subFields);
if (this.startTime > 0 && this.endTime > 0 && this.duration == 0) {
this.duration = this.endTime - this.startTime;
}
}
/**
* Processes time zone name from given iCal repeat rule string
*
* @param field string that contains time zone name in iCal
* format
* @param subFields all tokens of <code>field</code>
* @param standard if true, assigns the parsed time zone name to
* the standard time zone code; else assigns the
* name to the daylight saving time zone code
* @example <code>field</code>:
* "TZNAME:EST"<br> <code>subFields[0]</code>: "TZNAME"<br>
* <code>subFields[1]</code>: "EST"<br>
*/
void processTZNAME(String field, StringTokenizer subFields, boolean standard) {
String tz;
//time zone name is second subfield
if (subFields.countTokens() >= 2) {
subFields.nextToken();
tz = subFields.nextToken();
if (TimeZone.getTimeZone(tz) != null) {
if (standard) this.tzCodeStd = tz;
else this.tzCodeDay = tz;
}
}
}
/**
* Processes time zone offset from given iCal repeat rule string
*
* @param field string that contains time zone offset in iCal
* format
* @param subFields all tokens of <code>field</code>
* @example <code>field</code>:
* "TZOFFSETFROM:-0400"<br> <code>subFields[0]</code>:
* "TZOFFSETFROM"<br> <code>subFields[1]</code>: "-0400"<br>
* "TZOFFSETTO:-0500"<br> <code>subFields[0]</code>:
* "TZOFFSETTO"<br> <code>subFields[1]</code>: "-0500"<br>
*/
int processTZOFFSET(String field, StringTokenizer subFields) {
String offset;
//time zone offset is second subfield
if (subFields.countTokens() >= 2) {
subFields.nextToken();
offset = subFields.nextToken().trim();
int mult = 1;
if(offset.startsWith("-")) {
offset = offset.substring(1);
mult = -1;
}
while(offset.startsWith("0")) {
offset = offset.substring(1);
}
try {
int val = DateUtil.parseSignedInt(offset);
int hours = val / 100;
int minutes = val % 100;
return (hours * 60 + minutes) * 60 * 1000 * mult;
} catch (Exception e) { }
}
return 0;
}
private Calendar processTZ_START(String field, StringTokenizer subFields) {
try {
if (subFields.countTokens() >= 2) {
subFields.nextToken();
String startString = subFields.nextToken().trim();
long startTime = DateUtil.dateToLong(startString);
Calendar result = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
result.setTime(new Date(startTime));
return result;
}
}catch(Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Processes event duration from given iCal repeat rule string
*
* @param field string that contains event duration in iCal
* format
* @param subFields all tokens of <code>field</code>
* @example <code>field</code>:
* "DURATION:PT3600S"<br> <code>subFields[0]</code>:
* "DURATION"<br> <code>subFields[1]</code>: "PT3600S"<br>
*/
void processDURATION(String field, StringTokenizer subFields) {
String dur;
int idx;
long timeMs = 0;
//second subfield is duration
if (subFields.countTokens() >= 2) {
try {
subFields.nextToken();
dur = subFields.nextToken();
idx = dur.indexOf('P');
//skip the optional neg sign and 'P'
if (idx >= 0) dur = dur.substring(idx + 1);
//else, unexpected format
else return;
} catch (Exception e) {
return;
}
//get weeks
timeMs += getUnitValue(dur, "W") * 1000 * 60 * 60 * 24 * 7;
//get days
timeMs += getUnitValue(dur, "D") * 1000 * 60 * 60 * 24;
//get hours
timeMs += getUnitValue(dur, "H") * 1000 * 60 * 60;
//get minutes
timeMs += getUnitValue(dur, "M") * 1000 * 60;
//get seconds
timeMs += getUnitValue(dur, "S") * 1000;
this.duration = timeMs;
if (this.endTime == 0) this.endTime = this.startTime + this.duration;
}
}
/**
* Processes repeat rule from given iCal string
*
* @param field string that contains repeat rule in iCal
* format
* @param subFields all tokens of <code>field</code>
* @example <code>field</code>:
* "RRULE:FREQ=MONTHLY;INTERVAL=2"<br> <code>subFields[0]</code>:
* "RRULE"<br> <code>subFields[1]</code>: "FREQ=MONTHLY"<br>
* <code>subFields[1]</code>: "INTERVAL=2"<br>
*/
private void processRRULE( String field, StringTokenizer subFields) {
while (subFields.hasMoreTokens()) {
//create new RepeatRule
if (this.repeatRule == null) {
this.repeatRule = new RepeatRule();
// default values
this.repeatRule.setInt(RepeatRule.FREQUENCY, RepeatRule.DAILY);
this.repeatRule.setInt(RepeatRule.INTERVAL, 1);
}
String subField = subFields.nextToken();
if (subField.startsWith("FREQ")) {
try {
if (subField.indexOf("YEARLY") >= 0) {
this.frequency = RepeatRule.YEARLY;
this.repeatRule.setInt(RepeatRule.FREQUENCY, RepeatRule.YEARLY);
} else if (subField.indexOf("MONTHLY") >= 0) {
this.frequency = RepeatRule.MONTHLY;
this.repeatRule.setInt(RepeatRule.FREQUENCY, RepeatRule.MONTHLY);
} else if (subField.indexOf("WEEKLY") >= 0) {
this.frequency = RepeatRule.WEEKLY;
this.repeatRule.setInt(RepeatRule.FREQUENCY, RepeatRule.WEEKLY);
} else if (subField.indexOf("DAILY") >= 0) {
this.frequency = RepeatRule.DAILY;
this.repeatRule.setInt(RepeatRule.FREQUENCY, RepeatRule.DAILY);
}
} catch (Exception e){ phoneSupportsThisRecurrence = false; }
} else if (subField.startsWith("INTERVAL")) {
try {
this.interval = Integer.parseInt(subField.substring(subField.indexOf("=") + 1));
this.repeatRule.setInt(RepeatRule.INTERVAL, this.interval);
} catch (Exception e){ phoneSupportsThisRecurrence = false; }
}
//parse expiration date of event recurrence
else if (subField.startsWith("UNTIL")) {
String date;
date = getFieldValue(subField, "UNTIL");
if (date != null) {
try {
this.expirationDate = DateUtil.dateToLong(date);
} catch (Exception e) {}
}
} else if (subField.startsWith("BYMONTH")) {
String mo;
mo = getFieldValue(subField, "BYMONTH");
if (mo != null) {
try { this.repeatRule.setInt(RepeatRule.MONTH_IN_YEAR, Integer.parseInt(mo)); } catch (Exception e) { phoneSupportsThisRecurrence = false; }
}
} else if (subField.startsWith("BYDAY")) {
//get days of week that event occurs
if (subField.indexOf("SU") >= 0) this.dayInWeek |= RepeatRule.SUNDAY;
if (subField.indexOf("MO") >= 0) this.dayInWeek |= RepeatRule.MONDAY;
if (subField.indexOf("TU") >= 0) this.dayInWeek |= RepeatRule.TUESDAY;
if (subField.indexOf("WE") >= 0) this.dayInWeek |= RepeatRule.WEDNESDAY;
if (subField.indexOf("TH") >= 0) this.dayInWeek |= RepeatRule.THURSDAY;
if (subField.indexOf("FR") >= 0) this.dayInWeek |= RepeatRule.FRIDAY;
if (subField.indexOf("SA") >= 0) this.dayInWeek |= RepeatRule.SATURDAY;
//get week in month that event occurs
//the number that precedes the day of the week indicates
// the week in month that event occurs
this.weekInMonth |= getWeekInMonth(subField, "SU");
this.weekInMonth |= getWeekInMonth(subField, "MO");
this.weekInMonth |= getWeekInMonth(subField, "TU");
this.weekInMonth |= getWeekInMonth(subField, "WE");
this.weekInMonth |= getWeekInMonth(subField, "TH");
this.weekInMonth |= getWeekInMonth(subField, "FR");
this.weekInMonth |= getWeekInMonth(subField, "SA");
try {
//set day(s) in week
this.repeatRule.setInt(RepeatRule.DAY_IN_WEEK, this.dayInWeek);
//set week in month
if (this.weekInMonth != 0) this.repeatRule.setInt(RepeatRule.WEEK_IN_MONTH, this.weekInMonth);
} catch (Exception e) { phoneSupportsThisRecurrence = false; }
}
}
}
/**
* Gets the week in month from given iCal string
*
* @param field string that contains repeat rule in iCal
* format
* @param subFields all tokens of <code>field</code>
* @returns <code>RepeatRule.FIRST</code>,
* <code>RepeatRule.SECOND</code>,
* <code>RepeatRule.THIRD</code>,
* <code>RepeatRule.FOURTH</code>, or
* <code>RepeatRule.FIFTH</code>
* @example <code>field</code>:
* "BYDAY=2FR"<br> <code>day</code>: "FR"<br> returns
* <code>RepeatRule.SECOND</code>
*/
int getWeekInMonth(String field, String day) {
int rval = 0;
int wk = getUnitValue(field, day);
//GCal uses -1 to indicate fifth week
if (wk < 0) wk = 5;
switch (wk) {
case 1: rval = RepeatRule.FIRST; break;
case 2: rval = RepeatRule.SECOND; break;
case 3: rval = RepeatRule.THIRD; break;
case 4: rval = RepeatRule.FOURTH; break;
case 5: rval = RepeatRule.FIFTH; break;
default: break;
}
return rval;
}
/**
* Gets the integer that precedes a unit name in given
* field
*
* @param unitField string that contains an integer and unit
* @param unitName unit name whose integer is to be parsed
* @returns integer value of <code>unitField</code>;
* <code>0</code> if <code>unitName</code> cannot be found
* @example <code>unitField</code>:
* "DURATION:123M"<br> <code>unitName</code>: "M"<br> returns
* <code>123</code>
*/
int getUnitValue(String unitField, String unitName) {
int idx;
int indexStart;
int rval = 0;
try {
//find unit name
idx = unitField.indexOf(unitName);
if (idx >= 0) {
//skip the unit name
indexStart = idx - 1;
//find any preceding digits
if (java.lang.Character.isDigit(unitField.charAt(indexStart))) {
//look behind the unit name until non-digit found
for (int i = indexStart; i >= 0; i--) {
//quit when non-digit found
if (java.lang.Character.isDigit(unitField.charAt(i)) == false) {
//include negative sign
if (unitField.charAt(i) == '-') --indexStart;
break;
}
indexStart = i;
}
rval = Integer.parseInt(unitField.substring(indexStart, idx));
}
}
} catch (Exception e){}
return rval;
}
/**
* Gets the value of a specified field
*
* @param field string that contains a token name and value
* separated by the equal (=) or colon signs (:)
* @param fieldName name of field whose value is to be parsed
* @returns string value of <code>field</code>; <code>null</code>
* if <code>fieldName</code> cannot be found
* @example <code>field</code>:
* "COUNT=2"<br> <code>fieldName</code>: "COUNT"<br> returns
* <code>2</code>
*/
String getFieldValue(String field, String fieldName) {
StringTokenizer tokens = new StringTokenizer(field, "=:");
while (tokens.hasMoreTokens()) {
if (tokens.nextToken().equals(fieldName)) {
//found field, next token must be its value
if (tokens.hasMoreTokens()) return tokens.nextToken();
break;
}
}
//token not found
return null;
}
public boolean equals(Object obj) {
if(!(obj instanceof Recurrence)) {
return false;
}
Recurrence o = (Recurrence) obj;
//verify that the recurrence attributes are all the same for both object
boolean eq =
(this.startTime % (24 * 60 * 60 * 1000) == o.startTime % (24 * 60 * 60 * 1000)) &&
(this.endTime % (24 * 60 * 60 * 1000) == o.endTime % (24 * 60 * 60 * 1000)) &&
(this.expirationDate == o.expirationDate) &&
(this.frequency == o.frequency) &&
(
(this.interval == o.interval) ||
( (this.interval == 0 || this.interval == 1) && (o.interval == 0 || o.interval == 1) )
) /*&&
(this.weekInMonth == o.weekInMonth) &&
(this.dayInMonth == o.dayInMonth) &&
(this.dayInWeek == o.dayInWeek) &&
(this.dayInYear == o.dayInYear) &&
(this.duration == o.duration)*/;
if(!eq) {
return false;
}
//check that the start time is the same for both
if(this.startTime != o.startTime) {
//Google will not return the actual start time of a recurrent event if it started in the past, but
//it will return the current date, so I am going to ignore if the start time is different if the recurrent
//event starts in the past of about today
long nearFuture = System.currentTimeMillis() + (24 * 60 * 60 * 1000);
if(this.startTime > nearFuture && o.startTime > nearFuture) { //ignore the start only if it happens before the near future
return false;
}
}
//verify that the except dates are alll the same
//if the except dates are null or empty then they equal
boolean exceptDatesEquals =
(this.exceptDates == null && o.exceptDates == null) ||
(this.exceptDates == null && o.exceptDates != null && o.exceptDates.length == 0) ||
(this.exceptDates != null && o.exceptDates == null && this.exceptDates.length == 0);
if(exceptDatesEquals) {
return true;
}
//if the except dates are not null or empty then verify that they are the same
return compareExceptDatesArray(this.exceptDates, o.exceptDates) && compareExceptDatesArray(o.exceptDates, this.exceptDates);
}
private boolean compareExceptDatesArray(Date[] array1, Date[] array2) {
long nearFuture = System.currentTimeMillis() + (24 * 60 * 60 * 1000);
for(int i = 0; i < array1.length; i++) {
//ignore the except dates in the past
if(array1[i].getTime() < nearFuture) {
continue;
}
//look for the equal date in the other array
boolean found = false;
for(int j = 0; j < array2.length && !found; j++) {
if(array1[i].equals(array2[j])) {
found = true;
}
}
if(!found) {
return false;
}
}
return true;
}
public static Hashtable getRepeatRuleInfo(RepeatRule rr) {
if(rr == null) {
return null;
}
Hashtable result = new Hashtable();
int[] fields = rr.getFields();
for(int i = 0; i < fields.length; i++) {
Integer key = new Integer(fields[i]);
if(fields[i] == RepeatRule.END) {
result.put(key, new Long(rr.getDate(fields[i])));
} else {
result.put(key, new Integer(rr.getInt(fields[i])));
}
}
return result;
}
public static boolean repeatRuleEquals(RepeatRule rr, Hashtable rrInfo) {
if(rr == null && rrInfo == null) {
return true;
} else if(rr == null || rrInfo == null) {
return false;
} else {
int[] fields = rr.getFields();
if(fields.length != rrInfo.size()) {
return false;
}
for(int i = 0; i < fields.length; i++) {
//get the value from the other repeat rule
Object rrInfoValue = rrInfo.get(new Integer(fields[i]));
//check if the other repeat rule has the field
if(rrInfoValue == null) {
return false;
}
//check if the other repeat rule field value and this repeat rule
//field value equials
if(fields[i] == RepeatRule.END) {
if(rr.getDate(fields[i]) != ((Long)rrInfoValue).longValue() ) {
return false;
}
} else {
if(rr.getInt(fields[i]) != ((Integer)rrInfoValue).intValue() ) {
return false;
}
}
}
//no differences found
return true;
}
}
}
|