1/*
2 This file is part of the kcalcore library.
3
4 Copyright (c) 1998 Preston Brown <pbrown@kde.org>
5 Copyright (c) 2000-2004 Cornelius Schumacher <schumacher@kde.org>
6 Copyright (C) 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
7 Copyright (c) 2006 David Jarvie <software@astrojar.org.uk>
8
9 This library is free software; you can redistribute it and/or
10 modify it under the terms of the GNU Library General Public
11 License as published by the Free Software Foundation; either
12 version 2 of the License, or (at your option) any later version.
13
14 This library is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 Library General Public License for more details.
18
19 You should have received a copy of the GNU Library General Public License
20 along with this library; see the file COPYING.LIB. If not, write to
21 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22 Boston, MA 02110-1301, USA.
23*/
24/**
25 @file
26 This file is part of the API for handling calendar data and
27 defines the Calendar class.
28
29 @brief
30 Represents the main calendar class.
31
32 @author Preston Brown \<pbrown@kde.org\>
33 @author Cornelius Schumacher \<schumacher@kde.org\>
34 @author Reinhold Kainhofer \<reinhold@kainhofer.com\>
35 @author David Jarvie \<software@astrojar.org.uk\>
36*/
37#include "calendar.h"
38#include "calfilter.h"
39#include "icaltimezones.h"
40#include "sorting.h"
41#include "visitor.h"
42
43#include <KDebug>
44
45extern "C" {
46#include <icaltimezone.h>
47}
48
49#include <algorithm> // for std::remove()
50
51using namespace KCalCore;
52
53/**
54 Private class that helps to provide binary compatibility between releases.
55 @internal
56*/
57//@cond PRIVATE
58class KCalCore::Calendar::Private
59{
60public:
61 Private()
62 : mTimeZones(new ICalTimeZones),
63 mModified(false),
64 mNewObserver(false),
65 mObserversEnabled(true),
66 mDefaultFilter(new CalFilter),
67 batchAddingInProgress(false),
68 mDeletionTracking(true)
69 {
70 // Setup default filter, which does nothing
71 mFilter = mDefaultFilter;
72 mFilter->setEnabled(false);
73
74 mOwner = Person::Ptr(new Person());
75 mOwner->setName(QLatin1String("Unknown Name"));
76 mOwner->setEmail(QLatin1String("unknown@nowhere"));
77 }
78
79 ~Private()
80 {
81 delete mTimeZones;
82 mTimeZones = 0;
83 if (mFilter != mDefaultFilter) {
84 delete mFilter;
85 }
86 delete mDefaultFilter;
87 }
88 KDateTime::Spec timeZoneIdSpec(const QString &timeZoneId, bool view);
89
90 QString mProductId;
91 Person::Ptr mOwner;
92 ICalTimeZones *mTimeZones; // collection of time zones used in this calendar
93 ICalTimeZone mBuiltInTimeZone; // cached time zone lookup
94 ICalTimeZone mBuiltInViewTimeZone; // cached viewing time zone lookup
95 KDateTime::Spec mTimeSpec;
96 mutable KDateTime::Spec mViewTimeSpec;
97 bool mModified;
98 bool mNewObserver;
99 bool mObserversEnabled;
100 QList<CalendarObserver*> mObservers;
101
102 CalFilter *mDefaultFilter;
103 CalFilter *mFilter;
104
105 // These lists are used to put together related To-dos
106 QMultiHash<QString, Incidence::Ptr> mOrphans;
107 QMultiHash<QString, Incidence::Ptr> mOrphanUids;
108
109 // Lists for associating incidences to notebooks
110 QMultiHash<QString, Incidence::Ptr >mNotebookIncidences;
111 QHash<QString, QString>mUidToNotebook;
112 QHash<QString, bool>mNotebooks; // name to visibility
113 QHash<Incidence::Ptr, bool>mIncidenceVisibility; // incidence -> visibility
114 QString mDefaultNotebook; // uid of default notebook
115 QMap<QString, Incidence::List > mIncidenceRelations;
116 bool batchAddingInProgress;
117 bool mDeletionTracking;
118};
119
120/**
121 Make a QHash::value that returns a QVector.
122*/
123template <typename K, typename V>
124QVector<V> values(const QMultiHash<K,V> &c)
125{
126 QVector<V> v;
127 v.reserve(c.size());
128 for (typename QMultiHash<K,V>::const_iterator it = c.begin(), end = c.end(); it != end; ++it) {
129 v.push_back(it.value());
130 }
131 return v;
132}
133
134template <typename K, typename V>
135QVector<V> values(const QMultiHash<K,V> &c, const K &x)
136{
137 QVector<V> v;
138 typename QMultiHash<K,V>::const_iterator it = c.find(x);
139 while (it != c.end() && it.key() == x) {
140 v.push_back(it.value());
141 ++it;
142 }
143 return v;
144}
145
146/**
147 Template for a class that implements a visitor for adding an Incidence
148 to a resource supporting addEvent(), addTodo() and addJournal() calls.
149*/
150template<class T>
151class AddVisitor : public Visitor
152{
153public:
154 AddVisitor(T *r) : mResource(r) {}
155
156 bool visit(Event::Ptr e)
157 {
158 return mResource->addEvent(e);
159 }
160 bool visit(Todo::Ptr t)
161 {
162 return mResource->addTodo(t);
163 }
164 bool visit(Journal::Ptr j)
165 {
166 return mResource->addJournal(j);
167 }
168 bool visit(FreeBusy::Ptr)
169 {
170 return false;
171 }
172
173private:
174 T *mResource;
175};
176
177/**
178 Template for a class that implements a visitor for deleting an Incidence
179 from a resource supporting deleteEvent(), deleteTodo() and deleteJournal()
180 calls.
181*/
182template<class T>
183class DeleteVisitor : public Visitor
184{
185public:
186 DeleteVisitor(T *r) : mResource(r) {}
187
188 bool visit(Event::Ptr e)
189 {
190 mResource->deleteEvent(e);
191 return true;
192 }
193 bool visit(Todo::Ptr t)
194 {
195 mResource->deleteTodo(t);
196 return true;
197 }
198 bool visit(Journal::Ptr j)
199 {
200 mResource->deleteJournal(j);
201 return true;
202 }
203 bool visit(FreeBusy::Ptr)
204 {
205 return false;
206 }
207
208private:
209 T *mResource;
210};
211//@endcond
212
213Calendar::Calendar(const KDateTime::Spec &timeSpec)
214 : d(new KCalCore::Calendar::Private)
215{
216 d->mTimeSpec = timeSpec;
217 d->mViewTimeSpec = timeSpec;
218}
219
220Calendar::Calendar(const QString &timeZoneId)
221 : d(new KCalCore::Calendar::Private)
222{
223 setTimeZoneId(timeZoneId);
224}
225
226Calendar::~Calendar()
227{
228 delete d;
229}
230
231Person::Ptr Calendar::owner() const
232{
233 return d->mOwner;
234}
235
236void Calendar::setOwner(const Person::Ptr &owner)
237{
238 Q_ASSERT(owner);
239 d->mOwner = owner;
240 setModified(true);
241}
242
243void Calendar::setTimeSpec(const KDateTime::Spec &timeSpec)
244{
245 d->mTimeSpec = timeSpec;
246 d->mBuiltInTimeZone = ICalTimeZone();
247 setViewTimeSpec(timeSpec);
248
249 doSetTimeSpec(d->mTimeSpec);
250}
251
252KDateTime::Spec Calendar::timeSpec() const
253{
254 return d->mTimeSpec;
255}
256
257void Calendar::setTimeZoneId(const QString &timeZoneId)
258{
259 d->mTimeSpec = d->timeZoneIdSpec(timeZoneId, false);
260 d->mViewTimeSpec = d->mTimeSpec;
261 d->mBuiltInViewTimeZone = d->mBuiltInTimeZone;
262
263 doSetTimeSpec(d->mTimeSpec);
264}
265
266//@cond PRIVATE
267KDateTime::Spec Calendar::Private::timeZoneIdSpec(const QString &timeZoneId,
268 bool view)
269{
270 if (view) {
271 mBuiltInViewTimeZone = ICalTimeZone();
272 } else {
273 mBuiltInTimeZone = ICalTimeZone();
274 }
275 if (timeZoneId == QLatin1String("UTC")) {
276 return KDateTime::UTC;
277 }
278 ICalTimeZone tz = mTimeZones->zone(timeZoneId);
279 if (!tz.isValid()) {
280 ICalTimeZoneSource tzsrc;
281 tz = tzsrc.parse(icaltimezone_get_builtin_timezone(timeZoneId.toLatin1()));
282 if (view) {
283 mBuiltInViewTimeZone = tz;
284 } else {
285 mBuiltInTimeZone = tz;
286 }
287 }
288 if (tz.isValid()) {
289 return tz;
290 } else {
291 return KDateTime::ClockTime;
292 }
293}
294//@endcond
295
296QString Calendar::timeZoneId() const
297{
298 KTimeZone tz = d->mTimeSpec.timeZone();
299 return tz.isValid() ? tz.name() : QString();
300}
301
302void Calendar::setViewTimeSpec(const KDateTime::Spec &timeSpec) const
303{
304 d->mViewTimeSpec = timeSpec;
305 d->mBuiltInViewTimeZone = ICalTimeZone();
306}
307
308void Calendar::setViewTimeZoneId(const QString &timeZoneId) const
309{
310 d->mViewTimeSpec = d->timeZoneIdSpec(timeZoneId, true);
311}
312
313KDateTime::Spec Calendar::viewTimeSpec() const
314{
315 return d->mViewTimeSpec;
316}
317
318QString Calendar::viewTimeZoneId() const
319{
320 KTimeZone tz = d->mViewTimeSpec.timeZone();
321 return tz.isValid() ? tz.name() : QString();
322}
323
324ICalTimeZones *Calendar::timeZones() const
325{
326 return d->mTimeZones;
327}
328
329void Calendar::setTimeZones(ICalTimeZones *zones)
330{
331 if (!zones) {
332 return;
333 }
334
335 if (d->mTimeZones && (d->mTimeZones != zones)) {
336 delete d->mTimeZones;
337 d->mTimeZones = 0;
338 }
339 d->mTimeZones = zones;
340}
341
342void Calendar::shiftTimes(const KDateTime::Spec &oldSpec, const KDateTime::Spec &newSpec)
343{
344 setTimeSpec(newSpec);
345
346 int i, end;
347 Event::List ev = events();
348 for (i = 0, end = ev.count(); i < end; ++i) {
349 ev[i]->shiftTimes(oldSpec, newSpec);
350 }
351
352 Todo::List to = todos();
353 for (i = 0, end = to.count(); i < end; ++i) {
354 to[i]->shiftTimes(oldSpec, newSpec);
355 }
356
357 Journal::List jo = journals();
358 for (i = 0, end = jo.count(); i < end; ++i) {
359 jo[i]->shiftTimes(oldSpec, newSpec);
360 }
361}
362
363void Calendar::setFilter(CalFilter *filter)
364{
365 if (filter) {
366 d->mFilter = filter;
367 } else {
368 d->mFilter = d->mDefaultFilter;
369 }
370 emit filterChanged();
371}
372
373CalFilter *Calendar::filter() const
374{
375 return d->mFilter;
376}
377
378QStringList Calendar::categories() const
379{
380 Incidence::List rawInc(rawIncidences());
381 QStringList cats, thisCats;
382 // @TODO: For now just iterate over all incidences. In the future,
383 // the list of categories should be built when reading the file.
384 for (Incidence::List::ConstIterator i = rawInc.constBegin();
385 i != rawInc.constEnd(); ++i) {
386 thisCats = (*i)->categories();
387 for (QStringList::ConstIterator si = thisCats.constBegin();
388 si != thisCats.constEnd(); ++si) {
389 if (!cats.contains(*si)) {
390 cats.append(*si);
391 }
392 }
393 }
394 return cats;
395}
396
397Incidence::List Calendar::incidences(const QDate &date) const
398{
399 return mergeIncidenceList(events(date), todos(date), journals(date));
400}
401
402Incidence::List Calendar::incidences() const
403{
404 return mergeIncidenceList(events(), todos(), journals());
405}
406
407Incidence::List Calendar::rawIncidences() const
408{
409 return mergeIncidenceList(rawEvents(), rawTodos(), rawJournals());
410}
411
412Incidence::List Calendar::instances(const Incidence::Ptr &incidence) const
413{
414 if (incidence) {
415 Event::List elist;
416 Todo::List tlist;
417 Journal::List jlist;
418
419 if (incidence->type() == Incidence::TypeEvent) {
420 elist = eventInstances(incidence);
421 } else if (incidence->type() == Incidence::TypeTodo) {
422 tlist = todoInstances(incidence);
423 } else if (incidence->type() == Incidence::TypeJournal) {
424 jlist = journalInstances(incidence);
425 }
426 return mergeIncidenceList(elist, tlist, jlist);
427 } else {
428 return Incidence::List();
429 }
430}
431
432Incidence::List Calendar::duplicates(const Incidence::Ptr &incidence)
433{
434 if (incidence) {
435 Incidence::List list;
436 Incidence::List vals = values(d->mNotebookIncidences);
437 Incidence::List::const_iterator it;
438 for (it = vals.constBegin(); it != vals.constEnd(); ++it) {
439 if (((incidence->dtStart() == (*it)->dtStart()) ||
440 (!incidence->dtStart().isValid() && !(*it)->dtStart().isValid())) &&
441 (incidence->summary() == (*it)->summary())) {
442 list.append(*it);
443 }
444 }
445 return list;
446 } else {
447 return Incidence::List();
448 }
449}
450
451bool Calendar::addNotebook(const QString &notebook, bool isVisible)
452{
453 if (d->mNotebooks.contains(notebook)) {
454 return false;
455 } else {
456 d->mNotebooks.insert(notebook, isVisible);
457 return true;
458 }
459}
460
461bool Calendar::updateNotebook(const QString &notebook, bool isVisible)
462{
463 if (!d->mNotebooks.contains(notebook)) {
464 return false;
465 } else {
466 d->mNotebooks.insert(notebook, isVisible);
467 return true;
468 }
469}
470
471bool Calendar::deleteNotebook(const QString &notebook)
472{
473 if (!d->mNotebooks.contains(notebook)) {
474 return false;
475 } else {
476 return d->mNotebooks.remove(notebook);
477 }
478}
479
480bool Calendar::setDefaultNotebook(const QString &notebook)
481{
482 if (!d->mNotebooks.contains(notebook)) {
483 return false;
484 } else {
485 d->mDefaultNotebook = notebook;
486 return true;
487 }
488}
489
490QString Calendar::defaultNotebook() const
491{
492 return d->mDefaultNotebook;
493}
494
495bool Calendar::hasValidNotebook(const QString &notebook) const
496{
497 return d->mNotebooks.contains(notebook);
498}
499
500bool Calendar::isVisible(const Incidence::Ptr &incidence) const
501{
502 if (d->mIncidenceVisibility.contains(incidence)) {
503 return d->mIncidenceVisibility[incidence];
504 }
505 const QString nuid = notebook(incidence);
506 bool rv;
507 if (d->mNotebooks.contains(nuid)) {
508 rv = d->mNotebooks.value(nuid);
509 } else {
510 // NOTE returns true also for nonexisting notebooks for compatibility
511 rv = true;
512 }
513 d->mIncidenceVisibility[incidence] = rv;
514 return rv;
515}
516
517void Calendar::clearNotebookAssociations()
518{
519 d->mNotebookIncidences.clear();
520 d->mUidToNotebook.clear();
521 d->mIncidenceVisibility.clear();
522}
523
524bool Calendar::setNotebook(const Incidence::Ptr &inc, const QString &notebook)
525{
526 if (!inc) {
527 return false;
528 }
529
530 if (!notebook.isEmpty() &&
531 !incidence(inc->uid(), inc->recurrenceId())) {
532 kWarning() << "cannot set notebook until incidence has been added";
533 return false;
534 }
535
536 if (d->mUidToNotebook.contains(inc->uid())) {
537 QString old = d->mUidToNotebook.value(inc->uid());
538 if (!old.isEmpty() && notebook != old) {
539 if (inc->hasRecurrenceId()) {
540 kWarning() << "cannot set notebook for child incidences";
541 return false;
542 }
543 // Move all possible children also.
544 Incidence::List list = instances(inc);
545 Incidence::List::Iterator it;
546 for (it = list.begin(); it != list.end(); ++it) {
547 d->mNotebookIncidences.remove(old, *it);
548 d->mNotebookIncidences.insert(notebook, *it);
549 }
550 notifyIncidenceChanged(inc); // for removing from old notebook
551 // don not remove from mUidToNotebook to keep deleted incidences
552 d->mNotebookIncidences.remove(old, inc);
553 }
554 }
555 if (!notebook.isEmpty()) {
556 d->mUidToNotebook.insert(inc->uid(), notebook);
557 d->mNotebookIncidences.insert(notebook, inc);
558 kDebug() << "setting notebook" << notebook << "for" << inc->uid();
559 notifyIncidenceChanged(inc); // for inserting into new notebook
560 }
561
562 return true;
563}
564
565QString Calendar::notebook(const Incidence::Ptr &incidence) const
566{
567 if (incidence) {
568 return d->mUidToNotebook.value(incidence->uid());
569 } else {
570 return QString();
571 }
572}
573
574QString Calendar::notebook(const QString &uid) const
575{
576 return d->mUidToNotebook.value(uid);
577}
578
579QStringList Calendar::notebooks() const
580{
581 return d->mNotebookIncidences.uniqueKeys();
582}
583
584Incidence::List Calendar::incidences(const QString &notebook) const
585{
586 if (notebook.isEmpty()) {
587 return values(d->mNotebookIncidences);
588 } else {
589 return values(d->mNotebookIncidences, notebook);
590 }
591}
592
593/** static */
594Event::List Calendar::sortEvents(const Event::List &eventList,
595 EventSortField sortField,
596 SortDirection sortDirection)
597{
598
599 if (eventList.isEmpty()) {
600 return Event::List();
601 }
602
603 Event::List eventListSorted;
604
605 // Notice we alphabetically presort Summaries first.
606 // We do this so comparison "ties" stay in a nice order.
607 eventListSorted = eventList;
608 switch (sortField) {
609 case EventSortUnsorted:
610 break;
611
612 case EventSortStartDate:
613 if (sortDirection == SortDirectionAscending) {
614 qSort(eventListSorted.begin(), eventListSorted.end(), Events::startDateLessThan);
615 } else {
616 qSort(eventListSorted.begin(), eventListSorted.end(), Events::startDateMoreThan);
617 }
618 break;
619
620 case EventSortEndDate:
621 if (sortDirection == SortDirectionAscending) {
622 qSort(eventListSorted.begin(), eventListSorted.end(), Events::endDateLessThan);
623 } else {
624 qSort(eventListSorted.begin(), eventListSorted.end(), Events::endDateMoreThan);
625 }
626 break;
627
628 case EventSortSummary:
629 if (sortDirection == SortDirectionAscending) {
630 qSort(eventListSorted.begin(), eventListSorted.end(), Events::summaryLessThan);
631 } else {
632 qSort(eventListSorted.begin(), eventListSorted.end(), Events::summaryMoreThan);
633 }
634 break;
635 }
636
637 return eventListSorted;
638
639}
640
641Event::List Calendar::events(const QDate &date,
642 const KDateTime::Spec &timeSpec,
643 EventSortField sortField,
644 SortDirection sortDirection) const
645{
646 Event::List el = rawEventsForDate(date, timeSpec, sortField, sortDirection);
647 d->mFilter->apply(&el);
648 return el;
649}
650
651Event::List Calendar::events(const KDateTime &dt) const
652{
653 Event::List el = rawEventsForDate(dt);
654 d->mFilter->apply(&el);
655 return el;
656}
657
658Event::List Calendar::events(const QDate &start, const QDate &end,
659 const KDateTime::Spec &timeSpec,
660 bool inclusive) const
661{
662 Event::List el = rawEvents(start, end, timeSpec, inclusive);
663 d->mFilter->apply(&el);
664 return el;
665}
666
667Event::List Calendar::events(EventSortField sortField,
668 SortDirection sortDirection) const
669{
670 Event::List el = rawEvents(sortField, sortDirection);
671 d->mFilter->apply(&el);
672 return el;
673}
674
675bool Calendar::addIncidence(const Incidence::Ptr &incidence)
676{
677 if (!incidence) {
678 return false;
679 }
680
681 AddVisitor<Calendar> v(this);
682 return incidence->accept(v, incidence);
683}
684
685bool Calendar::deleteIncidence(const Incidence::Ptr &incidence)
686{
687 if (!incidence) {
688 return false;
689 }
690
691 if (beginChange(incidence)) {
692 DeleteVisitor<Calendar> v(this);
693 const bool result = incidence->accept(v, incidence);
694 endChange(incidence);
695 return result;
696 } else {
697 return false;
698 }
699}
700
701Incidence::Ptr Calendar::createException(const Incidence::Ptr &incidence,
702 const KDateTime &recurrenceId,
703 bool thisAndFuture)
704{
705 Q_ASSERT(recurrenceId.isValid());
706 if (!incidence || !incidence->recurs() || !recurrenceId.isValid()) {
707 return Incidence::Ptr();
708 }
709
710 Incidence::Ptr newInc(incidence->clone());
711 newInc->setCreated(KDateTime::currentUtcDateTime());
712 newInc->setRevision(0);
713 //Recurring exceptions are not support for now
714 newInc->clearRecurrence();
715
716 newInc->setRecurrenceId(recurrenceId);
717 newInc->setThisAndFuture(thisAndFuture);
718 newInc->setDtStart(recurrenceId);
719
720 // Calculate and set the new end of the incidence
721 KDateTime end = incidence->dateTime(IncidenceBase::RoleEnd);
722
723 if (end.isValid()) {
724 if (incidence->dtStart().isDateOnly()) {
725 int offset = incidence->dtStart().daysTo(recurrenceId);
726 end = end.addDays(offset);
727 } else {
728 qint64 offset = incidence->dtStart().secsTo_long(recurrenceId);
729 end = end.addSecs(offset);
730 }
731 newInc->setDateTime(end, IncidenceBase::RoleEnd);
732 }
733 return newInc;
734}
735
736// Dissociate a single occurrence or all future occurrences from a recurring
737// sequence. The new incidence is returned, but not automatically inserted
738// into the calendar, which is left to the calling application.
739Incidence::Ptr Calendar::dissociateOccurrence(const Incidence::Ptr &incidence,
740 const QDate &date,
741 const KDateTime::Spec &spec,
742 bool single)
743{
744 if (!incidence || !incidence->recurs()) {
745 return Incidence::Ptr();
746 }
747
748 Incidence::Ptr newInc(incidence->clone());
749 newInc->recreate();
750 // Do not call setRelatedTo() when dissociating recurring to-dos, otherwise the new to-do
751 // will appear as a child. Originally, we planned to set a relation with reltype SIBLING
752 // when dissociating to-dos, but currently kcalcore only supports reltype PARENT.
753 // We can uncomment the following line when we support the PARENT reltype.
754 //newInc->setRelatedTo( incidence );
755 Recurrence *recur = newInc->recurrence();
756 if (single) {
757 recur->clear();
758 } else {
759 // Adjust the recurrence for the future incidences. In particular adjust
760 // the "end after n occurrences" rules! "No end date" and "end by ..."
761 // don't need to be modified.
762 int duration = recur->duration();
763 if (duration > 0) {
764 int doneduration = recur->durationTo(date.addDays(-1));
765 if (doneduration >= duration) {
766 kDebug() << "The dissociated event already occurred more often"
767 << "than it was supposed to ever occur. ERROR!";
768 recur->clear();
769 } else {
770 recur->setDuration(duration - doneduration);
771 }
772 }
773 }
774 // Adjust the date of the incidence
775 if (incidence->type() == Incidence::TypeEvent) {
776 Event::Ptr ev = newInc.staticCast<Event>();
777 KDateTime start(ev->dtStart());
778 int daysTo = start.toTimeSpec(spec).date().daysTo(date);
779 ev->setDtStart(start.addDays(daysTo));
780 ev->setDtEnd(ev->dtEnd().addDays(daysTo));
781 } else if (incidence->type() == Incidence::TypeTodo) {
782 Todo::Ptr td = newInc.staticCast<Todo>();
783 bool haveOffset = false;
784 int daysTo = 0;
785 if (td->hasDueDate()) {
786 KDateTime due(td->dtDue());
787 daysTo = due.toTimeSpec(spec).date().daysTo(date);
788 td->setDtDue(due.addDays(daysTo), true);
789 haveOffset = true;
790 }
791 if (td->hasStartDate()) {
792 KDateTime start(td->dtStart());
793 if (!haveOffset) {
794 daysTo = start.toTimeSpec(spec).date().daysTo(date);
795 }
796 td->setDtStart(start.addDays(daysTo));
797 haveOffset = true;
798 }
799 }
800 recur = incidence->recurrence();
801 if (recur) {
802 if (single) {
803 recur->addExDate(date);
804 } else {
805 // Make sure the recurrence of the past events ends
806 // at the corresponding day
807 recur->setEndDate(date.addDays(-1));
808 }
809 }
810 return newInc;
811}
812
813Incidence::Ptr Calendar::incidence(const QString &uid,
814 const KDateTime &recurrenceId) const
815{
816 Incidence::Ptr i = event(uid, recurrenceId);
817 if (i) {
818 return i;
819 }
820
821 i = todo(uid, recurrenceId);
822 if (i) {
823 return i;
824 }
825
826 i = journal(uid, recurrenceId);
827 return i;
828}
829
830Incidence::Ptr Calendar::deleted(const QString &uid, const KDateTime &recurrenceId) const
831{
832 Incidence::Ptr i = deletedEvent(uid, recurrenceId);
833 if (i) {
834 return i;
835 }
836
837 i = deletedTodo(uid, recurrenceId);
838 if (i) {
839 return i;
840 }
841
842 i = deletedJournal(uid, recurrenceId);
843 return i;
844}
845
846Incidence::List Calendar::incidencesFromSchedulingID(const QString &sid) const
847{
848 Incidence::List result;
849 const Incidence::List incidences = rawIncidences();
850 Incidence::List::const_iterator it = incidences.begin();
851 for (; it != incidences.end(); ++it) {
852 if ((*it)->schedulingID() == sid) {
853 result.append(*it);
854 }
855 }
856 return result;
857}
858
859Incidence::Ptr Calendar::incidenceFromSchedulingID(const QString &uid) const
860{
861 const Incidence::List incidences = rawIncidences();
862 Incidence::List::const_iterator it = incidences.begin();
863 for (; it != incidences.end(); ++it) {
864 if ((*it)->schedulingID() == uid) {
865 // Touchdown, and the crowd goes wild
866 return *it;
867 }
868 }
869 // Not found
870 return Incidence::Ptr();
871}
872
873/** static */
874Todo::List Calendar::sortTodos(const Todo::List &todoList,
875 TodoSortField sortField,
876 SortDirection sortDirection)
877{
878 if (todoList.isEmpty()) {
879 return Todo::List();
880 }
881
882 Todo::List todoListSorted;
883
884 // Notice we alphabetically presort Summaries first.
885 // We do this so comparison "ties" stay in a nice order.
886
887 // Note that To-dos may not have Start DateTimes nor due DateTimes.
888
889 todoListSorted = todoList;
890 switch (sortField) {
891 case TodoSortUnsorted:
892 break;
893
894 case TodoSortStartDate:
895 if (sortDirection == SortDirectionAscending) {
896 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::startDateLessThan);
897 } else {
898 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::startDateMoreThan);
899 }
900 break;
901
902 case TodoSortDueDate:
903 if (sortDirection == SortDirectionAscending) {
904 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::dueDateLessThan);
905 } else {
906 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::dueDateMoreThan);
907 }
908 break;
909
910 case TodoSortPriority:
911 if (sortDirection == SortDirectionAscending) {
912 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::priorityLessThan);
913 } else {
914 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::priorityMoreThan);
915 }
916 break;
917
918 case TodoSortPercentComplete:
919 if (sortDirection == SortDirectionAscending) {
920 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::percentLessThan);
921 } else {
922 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::percentMoreThan);
923 }
924 break;
925
926 case TodoSortSummary:
927 if (sortDirection == SortDirectionAscending) {
928 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::summaryLessThan);
929 } else {
930 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::summaryMoreThan);
931 }
932 break;
933
934 case TodoSortCreated:
935 if (sortDirection == SortDirectionAscending) {
936 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::createdLessThan);
937 } else {
938 qSort(todoListSorted.begin(), todoListSorted.end(), Todos::createdMoreThan);
939 }
940 break;
941 }
942
943 return todoListSorted;
944}
945
946Todo::List Calendar::todos(TodoSortField sortField,
947 SortDirection sortDirection) const
948{
949 Todo::List tl = rawTodos(sortField, sortDirection);
950 d->mFilter->apply(&tl);
951 return tl;
952}
953
954Todo::List Calendar::todos(const QDate &date) const
955{
956 Todo::List el = rawTodosForDate(date);
957 d->mFilter->apply(&el);
958 return el;
959}
960
961Todo::List Calendar::todos(const QDate &start, const QDate &end,
962 const KDateTime::Spec &timespec, bool inclusive) const
963{
964 Todo::List tl = rawTodos(start, end, timespec, inclusive);
965 d->mFilter->apply(&tl);
966 return tl;
967}
968
969/** static */
970Journal::List Calendar::sortJournals(const Journal::List &journalList,
971 JournalSortField sortField,
972 SortDirection sortDirection)
973{
974 if (journalList.isEmpty()) {
975 return Journal::List();
976 }
977
978 Journal::List journalListSorted = journalList;
979
980 switch (sortField) {
981 case JournalSortUnsorted:
982 break;
983
984 case JournalSortDate:
985 if (sortDirection == SortDirectionAscending) {
986 qSort(journalListSorted.begin(), journalListSorted.end(), Journals::dateLessThan);
987 } else {
988 qSort(journalListSorted.begin(), journalListSorted.end(), Journals::dateMoreThan);
989 }
990 break;
991
992 case JournalSortSummary:
993 if (sortDirection == SortDirectionAscending) {
994 qSort(journalListSorted.begin(), journalListSorted.end(), Journals::summaryLessThan);
995 } else {
996 qSort(journalListSorted.begin(), journalListSorted.end(), Journals::summaryMoreThan);
997 }
998 break;
999 }
1000
1001 return journalListSorted;
1002}
1003
1004Journal::List Calendar::journals(JournalSortField sortField,
1005 SortDirection sortDirection) const
1006{
1007 Journal::List jl = rawJournals(sortField, sortDirection);
1008 d->mFilter->apply(&jl);
1009 return jl;
1010}
1011
1012Journal::List Calendar::journals(const QDate &date) const
1013{
1014 Journal::List el = rawJournalsForDate(date);
1015 d->mFilter->apply(&el);
1016 return el;
1017}
1018
1019// When this is called, the to-dos have already been added to the calendar.
1020// This method is only about linking related to-dos.
1021void Calendar::setupRelations(const Incidence::Ptr &forincidence)
1022{
1023 if (!forincidence) {
1024 return;
1025 }
1026
1027 const QString uid = forincidence->uid();
1028
1029 // First, go over the list of orphans and see if this is their parent
1030 Incidence::List l = values(d->mOrphans, uid);
1031 d->mOrphans.remove(uid);
1032 for (int i = 0, end = l.count(); i < end; ++i) {
1033 d->mIncidenceRelations[uid].append(l[i]);
1034 d->mOrphanUids.remove(l[i]->uid());
1035 }
1036
1037 // Now see about this incidences parent
1038 if (forincidence->relatedTo().isEmpty() && !forincidence->relatedTo().isEmpty()) {
1039 // Incidence has a uid it is related to but is not registered to it yet.
1040 // Try to find it
1041 Incidence::Ptr parent = incidence(forincidence->relatedTo());
1042 if (parent) {
1043 // Found it
1044
1045 // look for hierarchy loops
1046 if (isAncestorOf(forincidence, parent)) {
1047 forincidence->setRelatedTo(QString());
1048 kWarning() << "hierarchy loop beetween " << forincidence->uid() << " and " << parent->uid();
1049 } else {
1050 d->mIncidenceRelations[parent->uid()].append(forincidence);
1051 }
1052 } else {
1053 // Not found, put this in the mOrphans list
1054 // Note that the mOrphans dict might contain multiple entries with the
1055 // same key! which are multiple children that wait for the parent
1056 // incidence to be inserted.
1057 d->mOrphans.insert(forincidence->relatedTo(), forincidence);
1058 d->mOrphanUids.insert(forincidence->uid(), forincidence);
1059 }
1060 }
1061}
1062
1063// If a to-do with sub-to-dos is deleted, move it's sub-to-dos to the orphan list
1064void Calendar::removeRelations(const Incidence::Ptr &incidence)
1065{
1066 if (!incidence) {
1067 kDebug() << "Warning: incidence is 0";
1068 return;
1069 }
1070
1071 const QString uid = incidence->uid();
1072
1073 foreach(Incidence::Ptr i, d->mIncidenceRelations[uid]) {
1074 if (!d->mOrphanUids.contains(i->uid())) {
1075 d->mOrphans.insert(uid, i);
1076 d->mOrphanUids.insert(i->uid(), i);
1077 i->setRelatedTo(uid);
1078 }
1079 }
1080
1081 const QString parentUid = incidence->relatedTo();
1082
1083 // If this incidence is related to something else, tell that about it
1084 if (!parentUid.isEmpty()) {
1085 d->mIncidenceRelations[parentUid].erase(
1086 std::remove(d->mIncidenceRelations[parentUid].begin(),
1087 d->mIncidenceRelations[parentUid].end(), incidence),
1088 d->mIncidenceRelations[parentUid].end());
1089 }
1090
1091 // Remove this one from the orphans list
1092 if (d->mOrphanUids.remove(uid)) {
1093 // This incidence is located in the orphans list - it should be removed
1094 // Since the mOrphans dict might contain the same key (with different
1095 // child incidence pointers!) multiple times, take care that we remove
1096 // the correct one. So we need to remove all items with the given
1097 // parent UID, and readd those that are not for this item. Also, there
1098 // might be other entries with differnet UID that point to this
1099 // incidence (this might happen when the relatedTo of the item is
1100 // changed before its parent is inserted. This might happen with
1101 // groupware servers....). Remove them, too
1102 QStringList relatedToUids;
1103
1104 // First, create a list of all keys in the mOrphans list which point
1105 // to the removed item
1106 relatedToUids << incidence->relatedTo();
1107 for (QMultiHash<QString, Incidence::Ptr>::Iterator it = d->mOrphans.begin();
1108 it != d->mOrphans.end(); ++it) {
1109 if (it.value()->uid() == uid) {
1110 relatedToUids << it.key();
1111 }
1112 }
1113
1114 // now go through all uids that have one entry that point to the incidence
1115 for (QStringList::const_iterator uidit = relatedToUids.constBegin();
1116 uidit != relatedToUids.constEnd(); ++uidit) {
1117 Incidence::List tempList;
1118 // Remove all to get access to the remaining entries
1119 QList<Incidence::Ptr> l = d->mOrphans.values(*uidit);
1120 d->mOrphans.remove(*uidit);
1121 foreach(Incidence::Ptr i, l) {
1122 if (i != incidence) {
1123 tempList.append(i);
1124 }
1125 }
1126 // Readd those that point to a different orphan incidence
1127 for (Incidence::List::Iterator incit = tempList.begin();
1128 incit != tempList.end(); ++incit) {
1129 d->mOrphans.insert(*uidit, *incit);
1130 }
1131 }
1132 }
1133
1134 // Make sure the deleted incidence doesn't relate to a non-deleted incidence,
1135 // since that would cause trouble in MemoryCalendar::close(), as the deleted
1136 // incidences are destroyed after the non-deleted incidences. The destructor
1137 // of the deleted incidences would then try to access the already destroyed
1138 // non-deleted incidence, which would segfault.
1139 //
1140 // So in short: Make sure dead incidences don't point to alive incidences
1141 // via the relation.
1142 //
1143 // This crash is tested in MemoryCalendarTest::testRelationsCrash().
1144// incidence->setRelatedTo( Incidence::Ptr() );
1145}
1146
1147bool Calendar::isAncestorOf(const Incidence::Ptr &ancestor,
1148 const Incidence::Ptr &incidence) const
1149{
1150 if (!incidence || incidence->relatedTo().isEmpty()) {
1151 return false;
1152 } else if (incidence->relatedTo() == ancestor->uid()) {
1153 return true;
1154 } else {
1155 return isAncestorOf(ancestor, this->incidence(incidence->relatedTo()));
1156 }
1157}
1158
1159Incidence::List Calendar::relations(const QString &uid) const
1160{
1161 return d->mIncidenceRelations[uid];
1162}
1163
1164Calendar::CalendarObserver::~CalendarObserver()
1165{
1166}
1167
1168void Calendar::CalendarObserver::calendarModified(bool modified, Calendar *calendar)
1169{
1170 Q_UNUSED(modified);
1171 Q_UNUSED(calendar);
1172}
1173
1174void Calendar::CalendarObserver::calendarIncidenceAdded(const Incidence::Ptr &incidence)
1175{
1176 Q_UNUSED(incidence);
1177}
1178
1179void Calendar::CalendarObserver::calendarIncidenceChanged(const Incidence::Ptr &incidence)
1180{
1181 Q_UNUSED(incidence);
1182}
1183
1184void Calendar::CalendarObserver::calendarIncidenceDeleted(const Incidence::Ptr &incidence)
1185{
1186 Q_UNUSED(incidence);
1187}
1188
1189void
1190Calendar::CalendarObserver::calendarIncidenceAdditionCanceled(const Incidence::Ptr &incidence)
1191{
1192 Q_UNUSED(incidence);
1193}
1194
1195void Calendar::registerObserver(CalendarObserver *observer)
1196{
1197 if (!observer) {
1198 return;
1199 }
1200
1201 if (!d->mObservers.contains(observer)) {
1202 d->mObservers.append(observer);
1203 } else {
1204 d->mNewObserver = true;
1205 }
1206}
1207
1208void Calendar::unregisterObserver(CalendarObserver *observer)
1209{
1210 if (!observer) {
1211 return;
1212 } else {
1213 d->mObservers.removeAll(observer);
1214 }
1215}
1216
1217bool Calendar::isSaving() const
1218{
1219 return false;
1220}
1221
1222void Calendar::setModified(bool modified)
1223{
1224 if (modified != d->mModified || d->mNewObserver) {
1225 d->mNewObserver = false;
1226 foreach(CalendarObserver *observer, d->mObservers) {
1227 observer->calendarModified(modified, this);
1228 }
1229 d->mModified = modified;
1230 }
1231}
1232
1233bool Calendar::isModified() const
1234{
1235 return d->mModified;
1236}
1237
1238bool Calendar::save()
1239{
1240 return true;
1241}
1242
1243bool Calendar::reload()
1244{
1245 return true;
1246}
1247
1248void Calendar::incidenceUpdated(const QString &uid, const KDateTime &recurrenceId)
1249{
1250
1251 Incidence::Ptr inc = incidence(uid, recurrenceId);
1252
1253 if (!inc) {
1254 return;
1255 }
1256
1257 inc->setLastModified(KDateTime::currentUtcDateTime());
1258 // we should probably update the revision number here,
1259 // or internally in the Event itself when certain things change.
1260 // need to verify with ical documentation.
1261
1262 notifyIncidenceChanged(inc);
1263
1264 setModified(true);
1265}
1266
1267void Calendar::doSetTimeSpec(const KDateTime::Spec &timeSpec)
1268{
1269 Q_UNUSED(timeSpec);
1270}
1271
1272void Calendar::notifyIncidenceAdded(const Incidence::Ptr &incidence)
1273{
1274 if (!incidence) {
1275 return;
1276 }
1277
1278 if (!d->mObserversEnabled) {
1279 return;
1280 }
1281
1282 foreach(CalendarObserver *observer, d->mObservers) {
1283 observer->calendarIncidenceAdded(incidence);
1284 }
1285}
1286
1287void Calendar::notifyIncidenceChanged(const Incidence::Ptr &incidence)
1288{
1289 if (!incidence) {
1290 return;
1291 }
1292
1293 if (!d->mObserversEnabled) {
1294 return;
1295 }
1296
1297 foreach(CalendarObserver *observer, d->mObservers) {
1298 observer->calendarIncidenceChanged(incidence);
1299 }
1300}
1301
1302void Calendar::notifyIncidenceDeleted(const Incidence::Ptr &incidence)
1303{
1304 if (!incidence) {
1305 return;
1306 }
1307
1308 if (!d->mObserversEnabled) {
1309 return;
1310 }
1311
1312 foreach(CalendarObserver *observer, d->mObservers) {
1313 observer->calendarIncidenceDeleted(incidence);
1314 }
1315}
1316
1317void Calendar::notifyIncidenceAdditionCanceled(const Incidence::Ptr &incidence)
1318{
1319 if (!incidence) {
1320 return;
1321 }
1322
1323 if (!d->mObserversEnabled) {
1324 return;
1325 }
1326
1327 foreach(CalendarObserver *observer, d->mObservers) {
1328 observer->calendarIncidenceAdditionCanceled(incidence);
1329 }
1330}
1331
1332void Calendar::customPropertyUpdated()
1333{
1334 setModified(true);
1335}
1336
1337void Calendar::setProductId(const QString &id)
1338{
1339 d->mProductId = id;
1340}
1341
1342QString Calendar::productId() const
1343{
1344 return d->mProductId;
1345}
1346
1347/** static */
1348Incidence::List Calendar::mergeIncidenceList(const Event::List &events,
1349 const Todo::List &todos,
1350 const Journal::List &journals)
1351{
1352 Incidence::List incidences;
1353
1354 int i, end;
1355 for (i = 0, end = events.count(); i < end; ++i) {
1356 incidences.append(events[i]);
1357 }
1358
1359 for (i = 0, end = todos.count(); i < end; ++i) {
1360 incidences.append(todos[i]);
1361 }
1362
1363 for (i = 0, end = journals.count(); i < end; ++i) {
1364 incidences.append(journals[i]);
1365 }
1366
1367 return incidences;
1368}
1369
1370bool Calendar::beginChange(const Incidence::Ptr &incidence)
1371{
1372 Q_UNUSED(incidence);
1373 return true;
1374}
1375
1376bool Calendar::endChange(const Incidence::Ptr &incidence)
1377{
1378 Q_UNUSED(incidence);
1379 return true;
1380}
1381
1382void Calendar::setObserversEnabled(bool enabled)
1383{
1384 d->mObserversEnabled = enabled;
1385}
1386
1387void Calendar::appendAlarms(Alarm::List &alarms, const Incidence::Ptr &incidence,
1388 const KDateTime &from, const KDateTime &to) const
1389{
1390 KDateTime preTime = from.addSecs(-1);
1391
1392 Alarm::List alarmlist = incidence->alarms();
1393 for (int i = 0, iend = alarmlist.count(); i < iend; ++i) {
1394 if (alarmlist[i]->enabled()) {
1395 KDateTime dt = alarmlist[i]->nextRepetition(preTime);
1396 if (dt.isValid() && dt <= to) {
1397 kDebug() << incidence->summary() << "':" << dt.toString();
1398 alarms.append(alarmlist[i]);
1399 }
1400 }
1401 }
1402}
1403
1404void Calendar::appendRecurringAlarms(Alarm::List &alarms,
1405 const Incidence::Ptr &incidence,
1406 const KDateTime &from,
1407 const KDateTime &to) const
1408{
1409 KDateTime dt;
1410 bool endOffsetValid = false;
1411 Duration endOffset(0);
1412 Duration period(from, to);
1413
1414 Alarm::List alarmlist = incidence->alarms();
1415 for (int i = 0, iend = alarmlist.count(); i < iend; ++i) {
1416 Alarm::Ptr a = alarmlist[i];
1417 if (a->enabled()) {
1418 if (a->hasTime()) {
1419 // The alarm time is defined as an absolute date/time
1420 dt = a->nextRepetition(from.addSecs(-1));
1421 if (!dt.isValid() || dt > to) {
1422 continue;
1423 }
1424 } else {
1425 // Alarm time is defined by an offset from the event start or end time.
1426 // Find the offset from the event start time, which is also used as the
1427 // offset from the recurrence time.
1428 Duration offset(0);
1429 if (a->hasStartOffset()) {
1430 offset = a->startOffset();
1431 } else if (a->hasEndOffset()) {
1432 offset = a->endOffset();
1433 if (!endOffsetValid) {
1434 endOffset = Duration(incidence->dtStart(),
1435 incidence->dateTime(Incidence::RoleAlarmEndOffset));
1436 endOffsetValid = true;
1437 }
1438 }
1439
1440 // Find the incidence's earliest alarm
1441 KDateTime alarmStart =
1442 offset.end(a->hasEndOffset() ? incidence->dateTime(Incidence::RoleAlarmEndOffset) :
1443 incidence->dtStart());
1444// KDateTime alarmStart = incidence->dtStart().addSecs( offset );
1445 if (alarmStart > to) {
1446 continue;
1447 }
1448 KDateTime baseStart = incidence->dtStart();
1449 if (from > alarmStart) {
1450 alarmStart = from; // don't look earlier than the earliest alarm
1451 baseStart = (-offset).end((-endOffset).end(alarmStart));
1452 }
1453
1454 // Adjust the 'alarmStart' date/time and find the next recurrence at or after it.
1455 // Treate the two offsets separately in case one is daily and the other not.
1456 dt = incidence->recurrence()->getNextDateTime(baseStart.addSecs(-1));
1457 if (!dt.isValid() ||
1458 (dt = endOffset.end(offset.end(dt))) > to) // adjust 'dt' to get the alarm time
1459 {
1460 // The next recurrence is too late.
1461 if (!a->repeatCount()) {
1462 continue;
1463 }
1464
1465 // The alarm has repetitions, so check whether repetitions of previous
1466 // recurrences fall within the time period.
1467 bool found = false;
1468 Duration alarmDuration = a->duration();
1469 for (KDateTime base = baseStart;
1470 (dt = incidence->recurrence()->getPreviousDateTime(base)).isValid();
1471 base = dt) {
1472 if (a->duration().end(dt) < base) {
1473 break; // this recurrence's last repetition is too early, so give up
1474 }
1475
1476 // The last repetition of this recurrence is at or after 'alarmStart' time.
1477 // Check if a repetition occurs between 'alarmStart' and 'to'.
1478 int snooze = a->snoozeTime().value(); // in seconds or days
1479 if (a->snoozeTime().isDaily()) {
1480 Duration toFromDuration(dt, base);
1481 int toFrom = toFromDuration.asDays();
1482 if (a->snoozeTime().end(from) <= to ||
1483 (toFromDuration.isDaily() && toFrom % snooze == 0) ||
1484 (toFrom / snooze + 1) * snooze <= toFrom + period.asDays()) {
1485 found = true;
1486#ifndef NDEBUG
1487 // for debug output
1488 dt = offset.end(dt).addDays(((toFrom - 1) / snooze + 1) * snooze);
1489#endif
1490 break;
1491 }
1492 } else {
1493 int toFrom = dt.secsTo(base);
1494 if (period.asSeconds() >= snooze ||
1495 toFrom % snooze == 0 ||
1496 (toFrom / snooze + 1) * snooze <= toFrom + period.asSeconds())
1497 {
1498 found = true;
1499#ifndef NDEBUG
1500 // for debug output
1501 dt = offset.end(dt).addSecs(((toFrom - 1) / snooze + 1) * snooze);
1502#endif
1503 break;
1504 }
1505 }
1506 }
1507 if (!found) {
1508 continue;
1509 }
1510 }
1511 }
1512 kDebug() << incidence->summary() << "':" << dt.toString();
1513 alarms.append(a);
1514 }
1515 }
1516}
1517
1518void Calendar::startBatchAdding()
1519{
1520 d->batchAddingInProgress = true;
1521}
1522
1523void Calendar::endBatchAdding()
1524{
1525 d->batchAddingInProgress = false;
1526}
1527
1528bool Calendar::batchAdding() const
1529{
1530 return d->batchAddingInProgress;
1531}
1532
1533void Calendar::setDeletionTracking(bool enable)
1534{
1535 d->mDeletionTracking = enable;
1536}
1537
1538bool Calendar::deletionTracking() const
1539{
1540 return d->mDeletionTracking;
1541}
1542
1543void Calendar::virtual_hook(int id, void *data)
1544{
1545 Q_UNUSED(id);
1546 Q_UNUSED(data);
1547 Q_ASSERT(false);
1548}
1549
1550