3 /// Conversion routines for vevents (VCALENDAR, etc)
7 Copyright (C) 2006-2012, Net Direct Inc. (http://www.netdirect.ca/)
8 Copyright (C) 2010, Nicolas VIVIEN
9 Copyright (C) 2009, Dr J A Gow <J.A.Gow@wellfrazzled.com>
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2 of the License, or
14 (at your option) any later version.
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
20 See the GNU General Public License in the COPYING file at the
21 root directory of this project for more details.
36 namespace Barry
{ namespace Sync
{
38 //////////////////////////////////////////////////////////////////////////////
41 vCalendar::vCalendar(vTimeConverter
&vtc
)
47 vCalendar::~vCalendar()
54 const char *vCalendar::WeekDays
[] = { "SU", "MO", "TU", "WE", "TH", "FR", "SA" };
56 uint16_t vCalendar::GetWeekDayIndex(const char *dayname
)
58 for( int i
= 0; i
< 7; i
++ ) {
59 if( strcasecmp(dayname
, WeekDays
[i
]) == 0 )
65 uint16_t vCalendar::GetMonthWeekNumFromBYDAY(const std::string
& ByDay
)
67 return atoi(ByDay
.substr(0,ByDay
.length()-2).c_str());
70 uint16_t vCalendar::GetWeekDayIndexFromBYDAY(const std::string
& ByDay
)
72 return GetWeekDayIndex(ByDay
.substr(ByDay
.length()-2).c_str());
76 bool vCalendar::HasMultipleVEvents() const
79 b_VFormat
*format
= const_cast<b_VFormat
*>(Format());
80 GList
*attrs
= format
? b_vformat_get_attributes(format
) : 0;
81 for( ; attrs
; attrs
= attrs
->next
) {
82 b_VFormatAttribute
*attr
= (b_VFormatAttribute
*) attrs
->data
;
83 if( strcasecmp(b_vformat_attribute_get_name(attr
), "BEGIN") == 0 &&
84 strcasecmp(b_vformat_attribute_get_nth_value(attr
, 0), "VEVENT") == 0 )
92 void vCalendar::RecurToVCal()
94 using namespace Barry
;
96 Barry::Calendar
&cal
= m_BarryCal
;
101 vAttrPtr attr
= NewAttr("RRULE");
103 switch( cal
.RecurringType
)
105 case Calendar::Day
: // eg. every day
106 AddValue(attr
,"FREQ=DAILY");
109 case Calendar::MonthByDate
: // eg. every month on the 12th
111 AddValue(attr
,"FREQ=MONTHLY");
114 oss
<< "BYMONTHDAY=" << cal
.DayOfMonth
;
115 AddValue(attr
, oss
.str().c_str());
119 case Calendar::MonthByDay
: // eg. every month on 3rd Wed
120 // see: DayOfWeek and WeekOfMonth
121 AddValue(attr
, "FREQ=MONTHLY");
122 if( cal
.DayOfWeek
<= 6 ) { // DayOfWeek is unsigned
124 oss
<< "BYDAY=" << cal
.WeekOfMonth
<< WeekDays
[cal
.DayOfWeek
];
125 AddValue(attr
, oss
.str().c_str());
129 case Calendar::YearByDate
: // eg. every year on March 5
130 // see: DayOfMonth and MonthOfYear
131 AddValue(attr
, "FREQ=YEARLY");
134 oss
<< "BYMONTH=" << cal
.MonthOfYear
;
135 AddValue(attr
, oss
.str().c_str());
139 oss
<< "BYMONTHDAY=" << cal
.DayOfMonth
;
140 AddValue(attr
, oss
.str().c_str());
144 case Calendar::YearByDay
: // eg. every year on 3rd Wed of Jan
145 // see: DayOfWeek, WeekOfMonth, and
147 AddValue(attr
, "FREQ=YEARLY");
148 if( cal
.DayOfWeek
<= 6 ) { // DayOfWeek is unsigned
150 oss
<< "BYDAY=" << cal
.WeekOfMonth
<< WeekDays
[cal
.DayOfWeek
];
151 AddValue(attr
, oss
.str().c_str());
154 oss
<< "BYMONTH=" << cal
.MonthOfYear
;
155 AddValue(attr
, oss
.str().c_str());
159 case Calendar::Week
: // eg. every week on Mon and Fri
161 AddValue(attr
, "FREQ=WEEKLY");
165 for( int i
= 0, bm
= 1, cnt
= 0; i
< 7; i
++, bm
<<= 1 ) {
166 if( cal
.WeekDays
& bm
) {
173 AddValue(attr
, oss
.str().c_str());
178 throw ConvertError("Unknown RecurringType in Barry Calendar object");
181 // add some common parameters
182 if( cal
.Interval
> 1 ) {
184 oss
<< "INTERVAL=" << cal
.Interval
;
185 AddValue(attr
, oss
.str().c_str());
187 if( !cal
.Perpetual
) {
189 oss
<< "UNTIL=" << m_vtc
.unix2vtime(&cal
.RecurringEndTime
.Time
);
190 AddValue(attr
, oss
.str().c_str());
201 /// Note: interval can be used on all of these recurring types to
202 /// make it happen "every other time" or more, etc.
206 RecurringCodeType RecurringType;
207 uint16_t Interval; // must be >= 1
208 time_t RecurringEndTime; // only pertains if Recurring is true
209 // sets the date and time when
210 // recurrence of this appointment
211 // should no longer occur
212 // If a perpetual appointment, this
213 // is 0xFFFFFFFF in the low level data
214 // Instead, set the following flag.
215 bool Perpetual; // if true, this will always recur
216 uint16_t TimeZoneCode; // the time zone originally used
217 // for the recurrence data...
218 // seems to have little use, but
219 // set to your current time zone
222 uint16_t // recurring details, depending on type
227 unsigned char WeekDays; // bitmask, bit 0 = sunday
229 #define CAL_WD_SUN 0x01
230 #define CAL_WD_MON 0x02
231 #define CAL_WD_TUE 0x04
232 #define CAL_WD_WED 0x08
233 #define CAL_WD_THU 0x10
234 #define CAL_WD_FRI 0x20
235 #define CAL_WD_SAT 0x40
241 void vCalendar::RecurToBarryCal(vAttr
& rrule
, time_t starttime
)
243 using namespace Barry
;
245 Barry::Calendar
&cal
= m_BarryCal
;
246 // Trace trace("vCalendar::RecurToBarryCal");
247 std::map
<std::string
,unsigned char> pmap
;
248 pmap
["SU"] = CAL_WD_SUN
;
249 pmap
["MO"] = CAL_WD_MON
;
250 pmap
["TU"] = CAL_WD_TUE
;
251 pmap
["WE"] = CAL_WD_WED
;
252 pmap
["TH"] = CAL_WD_THU
;
253 pmap
["FR"] = CAL_WD_FRI
;
254 pmap
["SA"] = CAL_WD_SAT
;
258 unsigned int count
=0;
260 std::map
<std::string
,std::string
> args
;
262 val
=rrule
.GetValue(i
++);
263 if(val
.length()==0) {
266 string n
=val
.substr(0,val
.find("="));
267 string v
=val
.substr(val
.find("=")+1);
269 // trace.logf("RecurToBarryCal: |%s|%s|",n.c_str(),v.c_str());
272 // now process the interval.
275 if(args
.find(string("INTERVAL"))!=args
.end()) {
276 int interval
= atoi(args
["INTERVAL"].c_str());
278 // force to at least 1, for math below
281 cal
.Interval
= interval
;
284 // default to 1, for the math below.
285 // RecurBase::Clear() does this for us as well, but
289 if(args
.find(string("UNTIL"))!=args
.end()) {
290 cal
.Perpetual
= FALSE
;
291 cal
.RecurringEndTime
.Time
= m_vtc
.vtime2unix(args
["UNTIL"].c_str());
292 if( cal
.RecurringEndTime
.Time
== (time_t)-1 ) {
293 // trace.logf("osync_time_vtime2unix() failed: UNTIL = %s, zoneoffset = %d", args["UNTIL"].c_str(), zoneoffset);
296 // if we do not also have COUNT, then we must be forerver
297 if(args
.find(string("COUNT"))==args
.end()) {
300 // we do have COUNT. This means we won't have UNTIL.
301 // So we need to process the RecurringEndTime from
302 // the current start date. Set the count level to
303 // something other than zero to indicate we need
304 // to process it as the exact end date will
305 // depend upon the frequency.
306 count
=atoi(args
["COUNT"].c_str());
308 throw std::runtime_error("Invalid COUNT in recurring rule: " + args
["COUNT"]);
313 // we need these if COUNT is true, or if we are a yearly job.
315 // TO-DO: we must process COUNT in terms of an end date if we have it.
317 // Now deal with the freq
319 if(args
.find(string("FREQ"))==args
.end()) {
320 // trace.logf("RecurToBarryCal: No frequency specified!");
324 if(args
["FREQ"]==string("DAILY")) {
325 cal
.RecurringType
=Calendar::Day
;
328 // add count-1*interval days to find the end time:
329 // i.e. if starting on 2012/01/01 and going
330 // for 3 days, then the last day will be
333 // For intervals, the count is every interval days,
334 // so interval of 2 means 2012/01/01, 2012/01/03, etc.
335 // and the calculation still works.
336 cal
.RecurringEndTime
.Time
=
337 starttime
+ (count
-1) * cal
.Interval
* 24*60*60;
339 } else if(args
["FREQ"]==string("WEEKLY")) {
340 cal
.RecurringType
=Calendar::Week
;
341 // we must have a dayofweek entry
342 if(args
.find(string("BYDAY"))!=args
.end()) {
343 std::vector
<std::string
> v
=Tokenize(args
["BYDAY"]);
344 // iterate along our vector and convert
345 for(unsigned int idx
=0;idx
<v
.size();idx
++) {
346 cal
.WeekDays
|=pmap
[v
[idx
]];
349 // we must have at least one day selected, and if no
350 // BYDAY is selected, use the start time's day
351 struct tm datestruct
;
352 localtime_r(&starttime
,&datestruct
);
353 cal
.WeekDays
= pmap
[WeekDays
[datestruct
.tm_wday
]];
355 barryverbose("Warning: WEEKLY VEVENT without a day selected. Assuming day of start time.\nRecord data so far:\n" << cal
);
359 // need to process end date. This is easy
360 // for weeks, as a number of weeks can be
361 // reduced to seconds simply.
362 cal
.RecurringEndTime
.Time
=
363 starttime
+ (count
-1) * cal
.Interval
* 60*60*24*7;
365 } else if(args
["FREQ"]=="MONTHLY") {
366 if(args
.find(string("BYMONTHDAY"))!=args
.end()) {
367 cal
.RecurringType
=Calendar::MonthByDate
;
368 cal
.DayOfMonth
=atoi(args
["BYMONTHDAY"].c_str());
370 if(args
.find(string("BYDAY"))!=args
.end()) {
371 cal
.RecurringType
=Calendar::MonthByDay
;
372 cal
.WeekOfMonth
=GetMonthWeekNumFromBYDAY(args
["BYDAY"]);
373 cal
.DayOfWeek
=GetWeekDayIndexFromBYDAY(args
["BYDAY"]);
375 // must have a recurring type, so assume
376 // that monthly means every day on the day
377 // of month specified by starttime
378 struct tm datestruct
;
379 localtime_r(&starttime
,&datestruct
);
381 cal
.RecurringType
= Calendar::MonthByDate
;
382 cal
.DayOfMonth
= datestruct
.tm_mday
;
383 barryverbose("Warning: MONTHLY VEVENT without a day type specified (no BYMONTHDAY nor BYDAY). Assuming BYMONTHDAY, using day of start time.\nRecord data so far:\n" << cal
);
387 // Nasty. We need to convert to struct tm,
388 // do some modulo-12 addition then back
390 struct tm datestruct
;
391 localtime_r(&starttime
,&datestruct
);
392 // now do some modulo-12 on the month and year
393 // We could end up with an illegal date if
394 // the day of month is >28 and the resulting
395 // month falls on a February. We don't need
396 // to worry about day of week as mktime()
398 int add
= (count
-1) * cal
.Interval
;
399 datestruct
.tm_year
+= (datestruct
.tm_mon
+add
)/12;
400 datestruct
.tm_mon
= (datestruct
.tm_mon
+add
)%12;
401 if(datestruct
.tm_mday
>28 && datestruct
.tm_mon
==1) {
402 // force it to 1st Mar
403 // TODO Potential bug on leap years
405 datestruct
.tm_mday
=1;
407 if(datestruct
.tm_mday
==31 && (datestruct
.tm_mon
==8 ||
408 datestruct
.tm_mon
==3 ||
409 datestruct
.tm_mon
==5 ||
410 datestruct
.tm_mon
==10)) {
411 datestruct
.tm_mon
+=1;
412 datestruct
.tm_mday
=1;
414 // Just in case we're crossing DST boundaries,
415 // add an hour, to make sure we reach the ending
416 // month, in the case of intervals
417 datestruct
.tm_hour
++;
418 cal
.RecurringEndTime
.Time
= mktime(&datestruct
);
420 } else if(args
["FREQ"]=="YEARLY") {
421 bool need_assumption
= true;
422 if(args
.find(string("BYMONTH"))!=args
.end()) {
423 cal
.MonthOfYear
=atoi(args
["BYMONTH"].c_str());
424 if(args
.find(string("BYMONTHDAY"))!=args
.end()) {
425 cal
.RecurringType
=Calendar::YearByDate
;
426 cal
.DayOfMonth
=atoi(args
["BYMONTHDAY"].c_str());
427 need_assumption
= false;
429 if(args
.find(string("BYDAY"))!=args
.end()) {
430 cal
.RecurringType
=Calendar::YearByDay
;
431 cal
.WeekOfMonth
=GetMonthWeekNumFromBYDAY(args
["BYDAY"]);
432 cal
.DayOfWeek
=GetWeekDayIndexFromBYDAY(args
["BYDAY"]);
433 need_assumption
= false;
435 // needs assumption below...
440 if( need_assumption
) {
441 // otherwise use the start date and translate
443 // cal.StartTime has already been processed
444 // when we get here we need month of year,
446 struct tm datestruct
;
447 localtime_r(&starttime
,&datestruct
);
448 cal
.RecurringType
=Calendar::YearByDate
;
449 cal
.MonthOfYear
=datestruct
.tm_mon
;
450 cal
.DayOfMonth
=datestruct
.tm_mday
;
451 barryverbose("Warning: YEARLY VEVENT without a day type specified (no BYMONTHDAY nor BYDAY). Assuming BYMONTHDAY, using day and month of start time.\nRecord data so far:\n" << cal
);
454 // convert to struct tm, then simply add to the year.
456 // Note: intervals do work in the device firmware,
457 // but not all of the devices allow you to edit it
458 // with their GUI... hmmm... oh well, allow it
459 // anyway, and do the multiplication below.
460 struct tm datestruct
;
461 localtime_r(&starttime
,&datestruct
);
462 datestruct
.tm_year
+= (count
-1) * cal
.Interval
;
463 cal
.RecurringEndTime
.Time
= mktime(&datestruct
);
467 // unsigned char WeekDays; // bitmask, bit 0 = sunday
469 // #define CAL_WD_SUN 0x01
470 // #define CAL_WD_MON 0x02
471 // #define CAL_WD_TUE 0x04
472 // #define CAL_WD_WED 0x08
473 // #define CAL_WD_THU 0x10
474 // #define CAL_WD_FRI 0x20
475 // #define CAL_WD_SAT 0x40
478 // Main conversion routine for converting from Barry::Calendar to
479 // a vCalendar string of data.
480 const std::string
& vCalendar::ToVCal(const Barry::Calendar
&cal
)
482 // Trace trace("vCalendar::ToVCal");
483 std::ostringstream oss
;
485 // trace.logf("ToVCal, initial Barry record: %s", oss.str().c_str());
489 SetFormat( b_vformat_new() );
491 throw ConvertError("resource error allocating vformat");
493 // store the Barry object we're working with
496 // RFC section 4.8.7.2 requires DTSTAMP in all VEVENT, VTODO,
497 // VJOURNAL, and VFREEBUSY calendar components, and it must be
498 // in UTC. DTSTAMP holds the timestamp of when the iCal object itself
499 // was created, not when the object was created in the device or app.
500 // So, find out what time it is "now".
501 time_t now
= time(NULL
);
503 // begin building vCalendar data
504 AddAttr(NewAttr("PRODID", "-//OpenSync//NONSGML Barry Calendar Record//EN"));
505 AddAttr(NewAttr("BEGIN", "VEVENT"));
506 AddAttr(NewAttr("DTSTAMP", m_vtc
.unix2vtime(&now
).c_str())); // see note above
507 AddAttr(NewAttr("SEQUENCE", "0"));
508 AddAttr(NewAttr("SUMMARY", cal
.Subject
.c_str()));
509 AddAttr(NewAttr("DESCRIPTION", cal
.Notes
.c_str()));
510 AddAttr(NewAttr("LOCATION", cal
.Location
.c_str()));
512 string
start(m_vtc
.unix2vtime(&cal
.StartTime
.Time
));
513 string
end(m_vtc
.unix2vtime(&cal
.EndTime
.Time
));
514 string
notify(m_vtc
.unix2vtime(&cal
.NotificationTime
.Time
));
516 // if an all day event, only print the date parts of the string
517 if( cal
.AllDayEvent
&& start
.find('T') != string::npos
) {
518 // truncate start date
519 start
= start
.substr(0, start
.find('T'));
521 // create end date 1 day in future
522 time_t end_t
= cal
.StartTime
.Time
+ 24 * 60 * 60;
523 end
= m_vtc
.unix2vtime(&end_t
);
524 end
= end
.substr(0, end
.find('T'));
527 AddAttr(NewAttr("DTSTART", start
.c_str()));
528 AddAttr(NewAttr("DTEND", end
.c_str()));
529 // FIXME - add a truly globally unique "UID" string?
532 AddAttr(NewAttr("BEGIN", "VALARM"));
533 AddAttr(NewAttr("ACTION", "AUDIO"));
535 // notify must be UTC, when specified in DATE-TIME
536 vAttrPtr trigger
= NewAttr("TRIGGER", notify
.c_str());
537 AddParam(trigger
, "VALUE", "DATE-TIME");
540 AddAttr(NewAttr("END", "VALARM"));
543 if( cal
.Recurring
) {
547 AddAttr(NewAttr("END", "VEVENT"));
549 // generate the raw VCALENDAR data
550 m_gCalData
= b_vformat_to_string(Format(), VFORMAT_EVENT_20
);
551 m_vCalData
= m_gCalData
;
553 // trace.logf("ToVCal, resulting vcal data: %s", m_vCalData.c_str());
557 // Main conversion routine for converting from vCalendar data string
558 // to a Barry::Calendar object.
559 const Barry::Calendar
& vCalendar::ToBarry(const char *vcal
, uint32_t RecordId
)
563 // Trace trace("vCalendar::ToBarry");
564 // trace.logf("ToBarry, working on vcal data: %s", vcal);
566 // we only handle vCalendar data with one vevent block
567 if( HasMultipleVEvents() )
568 throw ConvertError("vCalendar data contains more than one VEVENT block, unsupported");
573 // store the vCalendar raw data
576 // create format parser structures
577 SetFormat( b_vformat_new_from_string(vcal
) );
579 throw ConvertError("resource error allocating vformat");
581 string start
= GetAttr("DTSTART", "/vevent");
582 // trace.logf("DTSTART attr retrieved: %s", start.c_str());
583 string end
= GetAttr("DTEND", "/vevent");
584 // trace.logf("DTEND attr retrieved: %s", end.c_str());
585 string subject
= GetAttr("SUMMARY", "/vevent");
586 // trace.logf("SUMMARY attr retrieved: %s", subject.c_str());
587 if( subject
.size() == 0 ) {
588 subject
= "<blank subject>";
589 // trace.logf("ERROR: bad data, blank SUMMARY: %s", vcal);
591 vAttr trigger_obj
= GetAttrObj("TRIGGER", 0, "/valarm");
593 string location
= GetAttr("LOCATION", "/vevent");
594 // trace.logf("LOCATION attr retrieved: %s", location.c_str());
596 string notes
= GetAttr("DESCRIPTION", "/vevent");
597 // trace.logf("DESCRIPTION attr retrieved: %s", notes.c_str());
599 vAttr rrule
= GetAttrObj("RRULE",0,"/vevent");
603 // Now, run checks and convert into Barry object
607 // FIXME - we are assuming that any non-UTC timestamps
608 // in the vcalendar record will be in the current timezone...
609 // This is wrong! fix this later.
611 // Also, we currently ignore any time zone
612 // parameters that might be in the vcalendar format... this
615 Barry::Calendar
&rec
= m_BarryCal
;
616 rec
.SetIds(Barry::Calendar::GetDefaultRecType(), RecordId
);
619 throw ConvertError("Blank DTSTART");
620 rec
.StartTime
.Time
= m_vtc
.vtime2unix(start
.c_str());
623 // DTEND is actually optional! According to the
624 // RFC, a DTSTART with no DTEND should be treated
625 // like a "special day" like an anniversary, which occupies
628 // Since the Blackberry doesn't really map well to this
629 // case, we'll set the end time to 1 day past start.
631 rec
.EndTime
.Time
= rec
.StartTime
.Time
+ 24 * 60 * 60;
634 rec
.EndTime
.Time
= m_vtc
.vtime2unix(end
.c_str());
637 // check for "all day event" which is specified by a DTSTART
638 // and a DTEND with no times, and one day apart
639 if( start
.find('T') == string::npos
&& end
.size() &&
640 end
.find('T') == string::npos
&&
641 (rec
.EndTime
.Time
- rec
.StartTime
.Time
) == 24 * 60 * 60 )
643 rec
.AllDayEvent
= true;
646 rec
.Subject
= subject
;
647 rec
.Location
= location
;
651 RecurToBarryCal(rrule
, rec
.StartTime
.Time
);
654 // convert trigger time into notification time
655 // assume no notification, by default
656 rec
.NotificationTime
.Time
= 0;
657 if( trigger_obj
.Get() ) {
658 string trigger_type
= trigger_obj
.GetParam("VALUE");
659 string trigger
= trigger_obj
.GetValue();
661 if( trigger
.size() == 0 ) {
662 // trace.logf("ERROR: no TRIGGER found in calendar entry, assuming notification time as 15 minutes before start.");
664 else if( trigger_type
== "DATE-TIME" ) {
665 rec
.NotificationTime
.Time
= m_vtc
.vtime2unix(trigger
.c_str());
667 else if( trigger_type
== "DURATION" || trigger_type
.size() == 0 ) {
668 // default is DURATION (RFC 4.8.6.3)
669 string related
= trigger_obj
.GetParam("RELATED");
671 // default to relative to start time
672 time_t *relative
= &rec
.StartTime
.Time
;
673 if( related
== "END" )
674 relative
= &rec
.EndTime
.Time
;
676 rec
.NotificationTime
.Time
= *relative
+ m_vtc
.alarmduration2sec(trigger
.c_str());
679 throw ConvertError("Unknown TRIGGER VALUE");
683 // trace.logf("ERROR: no TRIGGER found in calendar entry, assuming notification time as 15 minutes before start.");
686 std::ostringstream oss
;
687 m_BarryCal
.Dump(oss
);
688 // trace.logf("ToBarry, resulting Barry record: %s", oss.str().c_str());
692 // Transfers ownership of m_gCalData to the caller.
693 char* vCalendar::ExtractVCal()
695 char *ret
= m_gCalData
;
700 void vCalendar::Clear()
712 }} // namespace Barry::Sync