1/*
2 This file is part of the kcalcore library.
3
4 Copyright (c) 2001,2004 Cornelius Schumacher <schumacher@kde.org>
5 Copyright (C) 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
6 Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). All rights reserved.
7 Contact: Alvaro Manera <alvaro.manera@nokia.com>
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 IncidenceBase class.
28
29 @brief
30 An abstract base class that provides a common base for all calendar incidence
31 classes.
32
33 @author Cornelius Schumacher \<schumacher@kde.org\>
34 @author Reinhold Kainhofer \<reinhold@kainhofer.com\>
35*/
36
37#include "incidencebase.h"
38#include "calformat.h"
39#include "visitor.h"
40
41#include <QTime>
42#include <KDebug>
43#include <KUrl>
44
45#include <QtCore/QStringList>
46
47#define KCALCORE_MAGIC_NUMBER 0xCA1C012E
48#define KCALCORE_SERIALIZATION_VERSION 1
49
50using namespace KCalCore;
51
52/**
53 Private class that helps to provide binary compatibility between releases.
54 @internal
55*/
56//@cond PRIVATE
57class KCalCore::IncidenceBase::Private
58{
59public:
60 Private()
61 : mOrganizer(0),
62 mUpdateGroupLevel(0),
63 mUpdatedPending(false),
64 mAllDay(true),
65 mHasDuration(false)
66 {}
67
68 Private(const Private &other)
69 : mUpdateGroupLevel(0),
70 mUpdatedPending(false),
71 mAllDay(true),
72 mHasDuration(false)
73 {
74 init(other);
75 }
76
77 ~Private()
78 {
79 }
80
81 void init(const Private &other);
82
83 KDateTime mLastModified; // incidence last modified date
84 KDateTime mDtStart; // incidence start time
85 Person::Ptr mOrganizer; // incidence person (owner)
86 QString mUid; // incidence unique id
87 Duration mDuration; // incidence duration
88 int mUpdateGroupLevel; // if non-zero, suppresses update() calls
89 bool mUpdatedPending; // true if an update has occurred since startUpdates()
90 bool mAllDay; // true if the incidence is all-day
91 bool mHasDuration; // true if the incidence has a duration
92 Attendee::List mAttendees; // list of incidence attendees
93 QStringList mComments; // list of incidence comments
94 QStringList mContacts; // list of incidence contacts
95 QList<IncidenceObserver*> mObservers; // list of incidence observers
96 QSet<Field> mDirtyFields; // Fields that changed since last time the incidence was created
97 // or since resetDirtyFlags() was called
98 QUrl mUrl; // incidence url property
99};
100
101void IncidenceBase::Private::init(const Private &other)
102{
103 mLastModified = other.mLastModified;
104 mDtStart = other.mDtStart;
105 mOrganizer = other.mOrganizer;
106 mUid = other.mUid;
107 mDuration = other.mDuration;
108 mAllDay = other.mAllDay;
109 mHasDuration = other.mHasDuration;
110
111 mComments = other.mComments;
112 mContacts = other.mContacts;
113
114 mAttendees.clear();
115 Attendee::List::ConstIterator it;
116 for (it = other.mAttendees.constBegin(); it != other.mAttendees.constEnd(); ++it) {
117 mAttendees.append(Attendee::Ptr(new Attendee(*(*it))));
118 }
119 mUrl = other.mUrl;
120}
121//@endcond
122
123IncidenceBase::IncidenceBase()
124 : d(new KCalCore::IncidenceBase::Private)
125{
126 mReadOnly = false;
127 setUid(CalFormat::createUniqueId());
128}
129
130IncidenceBase::IncidenceBase(const IncidenceBase &i)
131 : CustomProperties(i),
132 d(new KCalCore::IncidenceBase::Private(*i.d))
133{
134 mReadOnly = i.mReadOnly;
135}
136
137IncidenceBase::~IncidenceBase()
138{
139 delete d;
140}
141
142IncidenceBase &IncidenceBase::operator=(const IncidenceBase &other)
143{
144 Q_ASSERT(type() == other.type());
145
146 startUpdates();
147
148 // assign is virtual, will call the derived class's
149 IncidenceBase &ret = assign(other);
150 endUpdates();
151 return ret;
152}
153
154IncidenceBase &IncidenceBase::assign(const IncidenceBase &other)
155{
156 CustomProperties::operator=(other);
157 d->init(*other.d);
158 mReadOnly = other.mReadOnly;
159 d->mDirtyFields.clear();
160 d->mDirtyFields.insert(FieldUnknown);
161 return *this;
162}
163
164bool IncidenceBase::operator==(const IncidenceBase &i2) const
165{
166 if (i2.type() != type()) {
167 return false;
168 } else {
169 // equals is virtual, so here we're calling the derived class method
170 return equals(i2);
171 }
172}
173
174bool IncidenceBase::operator!=(const IncidenceBase &i2) const
175{
176 return !operator==(i2);
177}
178
179bool IncidenceBase::equals(const IncidenceBase &i2) const
180{
181 if (attendees().count() != i2.attendees().count()) {
182 // kDebug() << "Attendee count is different";
183 return false;
184 }
185
186 Attendee::List al1 = attendees();
187 Attendee::List al2 = i2.attendees();
188 Attendee::List::ConstIterator a1 = al1.constBegin();
189 Attendee::List::ConstIterator a2 = al2.constBegin();
190 //TODO Does the order of attendees in the list really matter?
191 //Please delete this comment if you know it's ok, kthx
192 for (; a1 != al1.constEnd() && a2 != al2.constEnd(); ++a1, ++a2) {
193 if (!(**a1 == **a2)) {
194 // kDebug() << "Attendees are different";
195 return false;
196 }
197 }
198
199 if (!CustomProperties::operator==(i2)) {
200 // kDebug() << "Properties are different";
201 return false;
202 }
203
204 // Don't compare lastModified, otherwise the operator is not
205 // of much use. We are not comparing for identity, after all.
206 // no need to compare mObserver
207
208 bool a = ((dtStart() == i2.dtStart()) || (!dtStart().isValid() && !i2.dtStart().isValid()));
209 bool b = *(organizer().data()) == *(i2.organizer().data());
210 bool c = uid() == i2.uid();
211 bool d = allDay() == i2.allDay();
212 bool e = duration() == i2.duration();
213 bool f = hasDuration() == i2.hasDuration();
214 bool g = url() == i2.url();
215
216 //kDebug() << a << b << c << d << e << f << g;
217 return a && b && c && d && e && f && g;
218}
219
220bool IncidenceBase::accept(Visitor &v, IncidenceBase::Ptr incidence)
221{
222 Q_UNUSED(v);
223 Q_UNUSED(incidence);
224 return false;
225}
226
227void IncidenceBase::setUid(const QString &uid)
228{
229 update();
230 d->mUid = uid;
231 d->mDirtyFields.insert(FieldUid);
232 updated();
233}
234
235QString IncidenceBase::uid() const
236{
237 return d->mUid;
238}
239
240void IncidenceBase::setLastModified(const KDateTime &lm)
241{
242 // DON'T! updated() because we call this from
243 // Calendar::updateEvent().
244
245 d->mDirtyFields.insert(FieldLastModified);
246
247 // Convert to UTC and remove milliseconds part.
248 KDateTime current = lm.toUtc();
249 QTime t = current.time();
250 t.setHMS(t.hour(), t.minute(), t.second(), 0);
251 current.setTime(t);
252
253 d->mLastModified = current;
254}
255
256KDateTime IncidenceBase::lastModified() const
257{
258 return d->mLastModified;
259}
260
261void IncidenceBase::setOrganizer(const Person::Ptr &organizer)
262{
263 if (organizer) {
264 update();
265 // we don't check for readonly here, because it is
266 // possible that by setting the organizer we are changing
267 // the event's readonly status...
268 d->mOrganizer = organizer;
269
270 d->mDirtyFields.insert(FieldOrganizer);
271
272 updated();
273 }
274}
275
276void IncidenceBase::setOrganizer(const QString &o)
277{
278 QString mail(o);
279 if (mail.startsWith(QLatin1String("MAILTO:"), Qt::CaseInsensitive)) {
280 mail = mail.remove(0, 7);
281 }
282
283 // split the string into full name plus email.
284 const Person::Ptr organizer = Person::fromFullName(mail);
285 setOrganizer(organizer);
286}
287
288Person::Ptr IncidenceBase::organizer() const
289{
290 if (!d->mOrganizer)
291 d->mOrganizer = Person::Ptr(new Person()); // init at first use only to save memory
292
293 return d->mOrganizer;
294}
295
296void IncidenceBase::setReadOnly(bool readOnly)
297{
298 mReadOnly = readOnly;
299}
300
301bool IncidenceBase::isReadOnly() const
302{
303 return mReadOnly;
304}
305
306void IncidenceBase::setDtStart(const KDateTime &dtStart)
307{
308// if ( mReadOnly ) return;
309
310 if (!dtStart.isValid() && type() != IncidenceBase::TypeTodo) {
311 kWarning() << "Invalid dtStart";
312 }
313
314 update();
315 d->mDtStart = dtStart;
316 d->mAllDay = dtStart.isDateOnly();
317 d->mDirtyFields.insert(FieldDtStart);
318 updated();
319}
320
321KDateTime IncidenceBase::dtStart() const
322{
323 return d->mDtStart;
324}
325
326bool IncidenceBase::allDay() const
327{
328 return d->mAllDay;
329}
330
331void IncidenceBase::setAllDay(bool f)
332{
333 if (mReadOnly || f == d->mAllDay) {
334 return;
335 }
336 update();
337 d->mAllDay = f;
338 if (d->mDtStart.isValid()) {
339 d->mDirtyFields.insert(FieldDtStart);
340 }
341 updated();
342}
343
344void IncidenceBase::shiftTimes(const KDateTime::Spec &oldSpec,
345 const KDateTime::Spec &newSpec)
346{
347 update();
348 d->mDtStart = d->mDtStart.toTimeSpec(oldSpec);
349 d->mDtStart.setTimeSpec(newSpec);
350 d->mDirtyFields.insert(FieldDtStart);
351 d->mDirtyFields.insert(FieldDtEnd);
352 updated();
353}
354
355void IncidenceBase::addComment(const QString &comment)
356{
357 d->mComments += comment;
358}
359
360bool IncidenceBase::removeComment(const QString &comment)
361{
362 bool found = false;
363 QStringList::Iterator i;
364
365 for (i = d->mComments.begin(); !found && i != d->mComments.end(); ++i) {
366 if ((*i) == comment) {
367 found = true;
368 d->mComments.erase(i);
369 }
370 }
371
372 if (found) {
373 d->mDirtyFields.insert(FieldComment);
374 }
375
376 return found;
377}
378
379void IncidenceBase::clearComments()
380{
381 d->mDirtyFields.insert(FieldComment);
382 d->mComments.clear();
383}
384
385QStringList IncidenceBase::comments() const
386{
387 return d->mComments;
388}
389
390void IncidenceBase::addContact(const QString &contact)
391{
392 if (!contact.isEmpty()) {
393 d->mContacts += contact;
394 d->mDirtyFields.insert(FieldContact);
395 }
396}
397
398bool IncidenceBase::removeContact(const QString &contact)
399{
400 bool found = false;
401 QStringList::Iterator i;
402
403 for (i = d->mContacts.begin(); !found && i != d->mContacts.end(); ++i) {
404 if ((*i) == contact) {
405 found = true;
406 d->mContacts.erase(i);
407 }
408 }
409
410 if (found) {
411 d->mDirtyFields.insert(FieldContact);
412 }
413
414 return found;
415}
416
417void IncidenceBase::clearContacts()
418{
419 d->mDirtyFields.insert(FieldContact);
420 d->mContacts.clear();
421}
422
423QStringList IncidenceBase::contacts() const
424{
425 return d->mContacts;
426}
427
428void IncidenceBase::addAttendee(const Attendee::Ptr &a, bool doupdate)
429{
430 if (!a || mReadOnly) {
431 return;
432 }
433
434 Q_ASSERT(!d->mAttendees.contains(a));
435
436 if (doupdate) {
437 update();
438 }
439 if (a->name().left(7).toUpper() == QLatin1String("MAILTO:")) {
440 a->setName(a->name().remove(0, 7));
441 }
442
443 /* If Uid is empty, just use the pointer to Attendee (encoded to
444 * string) as Uid. Only thing that matters is that the Uid is unique
445 * insofar IncidenceBase is concerned, and this does that (albeit
446 * not very nicely). If these are ever saved to disk, should use
447 * (considerably more expensive) CalFormat::createUniqueId(). As Uid
448 * is not part of Attendee in iCal std, it's fairly safe bet that
449 * these will never hit disc though so faster generation speed is
450 * more important than actually being forever unique.*/
451 if (a->uid().isEmpty()) {
452 a->setUid(QString::number((qlonglong)a.data()));
453 }
454
455 d->mAttendees.append(a);
456 if (doupdate) {
457 d->mDirtyFields.insert(FieldAttendees);
458 updated();
459 }
460}
461
462void IncidenceBase::deleteAttendee(const Attendee::Ptr &a, bool doupdate)
463{
464 if (!a || mReadOnly) {
465 return;
466 }
467
468 int index = d->mAttendees.indexOf(a);
469 if (index >= 0) {
470 if (doupdate) {
471 update();
472 }
473
474 d->mAttendees.remove(index);
475
476 if (doupdate) {
477 d->mDirtyFields.insert(FieldAttendees);
478 updated();
479 }
480 }
481}
482
483Attendee::List IncidenceBase::attendees() const
484{
485 return d->mAttendees;
486}
487
488int IncidenceBase::attendeeCount() const
489{
490 return d->mAttendees.count();
491}
492
493void IncidenceBase::clearAttendees()
494{
495 if (mReadOnly) {
496 return;
497 }
498 d->mDirtyFields.insert(FieldAttendees);
499 d->mAttendees.clear();
500}
501
502Attendee::Ptr IncidenceBase::attendeeByMail(const QString &email) const
503{
504 Attendee::List::ConstIterator it;
505 for (it = d->mAttendees.constBegin(); it != d->mAttendees.constEnd(); ++it) {
506 if ((*it)->email() == email) {
507 return *it;
508 }
509 }
510
511 return Attendee::Ptr();
512}
513
514Attendee::Ptr IncidenceBase::attendeeByMails(const QStringList &emails,
515 const QString &email) const
516{
517 QStringList mails = emails;
518 if (!email.isEmpty()) {
519 mails.append(email);
520 }
521
522 Attendee::List::ConstIterator itA;
523 for (itA = d->mAttendees.constBegin(); itA != d->mAttendees.constEnd(); ++itA) {
524 for (QStringList::const_iterator it = mails.constBegin(); it != mails.constEnd(); ++it) {
525 if ((*itA)->email() == (*it)) {
526 return *itA;
527 }
528 }
529 }
530
531 return Attendee::Ptr();
532}
533
534Attendee::Ptr IncidenceBase::attendeeByUid(const QString &uid) const
535{
536 Attendee::List::ConstIterator it;
537 for (it = d->mAttendees.constBegin(); it != d->mAttendees.constEnd(); ++it) {
538 if ((*it)->uid() == uid) {
539 return *it;
540 }
541 }
542
543 return Attendee::Ptr();
544}
545
546void IncidenceBase::setDuration(const Duration &duration)
547{
548 update();
549 d->mDuration = duration;
550 setHasDuration(true);
551 d->mDirtyFields.insert(FieldDuration);
552 updated();
553}
554
555Duration IncidenceBase::duration() const
556{
557 return d->mDuration;
558}
559
560void IncidenceBase::setHasDuration(bool hasDuration)
561{
562 d->mHasDuration = hasDuration;
563}
564
565bool IncidenceBase::hasDuration() const
566{
567 return d->mHasDuration;
568}
569
570void IncidenceBase::setUrl(const QUrl& url)
571{
572 d->mDirtyFields.insert(FieldUrl);
573 d->mUrl = url;
574}
575
576QUrl IncidenceBase::url() const
577{
578 return d->mUrl;
579}
580
581void IncidenceBase::registerObserver(IncidenceBase::IncidenceObserver *observer)
582{
583 if (observer && !d->mObservers.contains(observer)) {
584 d->mObservers.append(observer);
585 }
586}
587
588void IncidenceBase::unRegisterObserver(IncidenceBase::IncidenceObserver *observer)
589{
590 d->mObservers.removeAll(observer);
591}
592
593void IncidenceBase::update()
594{
595 if (!d->mUpdateGroupLevel) {
596 d->mUpdatedPending = true;
597 KDateTime rid = recurrenceId();
598 foreach(IncidenceObserver *o, d->mObservers) {
599 o->incidenceUpdate(uid(), rid);
600 }
601 }
602}
603
604void IncidenceBase::updated()
605{
606 if (d->mUpdateGroupLevel) {
607 d->mUpdatedPending = true;
608 } else {
609 const KDateTime rid = recurrenceId();
610 foreach(IncidenceObserver *o, d->mObservers) {
611 o->incidenceUpdated(uid(), rid);
612 }
613 }
614}
615
616void IncidenceBase::startUpdates()
617{
618 update();
619 ++d->mUpdateGroupLevel;
620}
621
622void IncidenceBase::endUpdates()
623{
624 if (d->mUpdateGroupLevel > 0) {
625 if (--d->mUpdateGroupLevel == 0 && d->mUpdatedPending) {
626 d->mUpdatedPending = false;
627 updated();
628 }
629 }
630}
631
632void IncidenceBase::customPropertyUpdate()
633{
634 update();
635}
636
637void IncidenceBase::customPropertyUpdated()
638{
639 updated();
640}
641
642KDateTime IncidenceBase::recurrenceId() const
643{
644 return KDateTime();
645}
646
647void IncidenceBase::resetDirtyFields()
648{
649 d->mDirtyFields.clear();
650}
651
652QSet<IncidenceBase::Field> IncidenceBase::dirtyFields() const
653{
654 return d->mDirtyFields;
655}
656
657void IncidenceBase::setFieldDirty(IncidenceBase::Field field)
658{
659 d->mDirtyFields.insert(field);
660}
661
662KUrl IncidenceBase::uri() const
663{
664 return KUrl(QLatin1String("urn:x-ical:") + uid());
665}
666
667void IncidenceBase::setDirtyFields(const QSet<IncidenceBase::Field> &dirtyFields)
668{
669 d->mDirtyFields = dirtyFields;
670}
671
672/** static */
673quint32 IncidenceBase::magicSerializationIdentifier()
674{
675 return KCALCORE_MAGIC_NUMBER;
676}
677
678QDataStream& KCalCore::operator<<(QDataStream &out, const KCalCore::IncidenceBase::Ptr &i)
679{
680 if (!i)
681 return out;
682
683 out << static_cast<quint32>(KCALCORE_MAGIC_NUMBER); // Magic number to identify KCalCore data
684 out << static_cast<quint32>(KCALCORE_SERIALIZATION_VERSION);
685 out << static_cast<qint32>(i->type());
686
687 out << *(static_cast<CustomProperties*>(i.data()));
688 out << i->d->mLastModified << i->d->mDtStart << i->organizer() << i->d->mUid << i->d->mDuration
689 << i->d->mAllDay << i->d->mHasDuration << i->d->mComments << i->d->mContacts
690 << i->d->mAttendees.count() << i->d->mUrl;
691
692 foreach(const Attendee::Ptr &attendee, i->d->mAttendees) {
693 out << attendee;
694 }
695
696 // Serialize the sub-class data. In KDE5 we can add new virtuals.
697 i->virtual_hook(KCalCore::IncidenceBase::SerializerHook, &out);
698
699 return out;
700}
701
702QDataStream& KCalCore::operator>>(QDataStream &in, const KCalCore::IncidenceBase::Ptr &i)
703{
704 if (!i)
705 return in;
706
707 qint32 attendeeCount, type;
708 quint32 magic, version;
709
710 in >> magic;
711
712 if (magic != KCALCORE_MAGIC_NUMBER) {
713 kWarning() << "Invalid magic on serialized data";
714 return in;
715 }
716
717 in >> version;
718
719 if (version > KCALCORE_MAGIC_NUMBER) {
720 kWarning() << "Invalid version on serialized data";
721 return in;
722 }
723
724 in >> type;
725
726 in >> *(static_cast<CustomProperties*>(i.data()));
727 in >> i->d->mLastModified >> i->d->mDtStart >> i->d->mOrganizer >> i->d->mUid >> i->d->mDuration
728 >> i->d->mAllDay >> i->d->mHasDuration >> i->d->mComments >> i->d->mContacts >> attendeeCount
729 >> i->d->mUrl;
730
731 i->d->mAttendees.clear();
732 for (int it=0; it<attendeeCount; it++) {
733 Attendee::Ptr attendee = Attendee::Ptr(new Attendee(QString(), QString()));
734 in >> attendee;
735 i->d->mAttendees.append(attendee);
736 }
737
738 // Deserialize the sub-class data. In KDE5 we can add new virtuals.
739 i->virtual_hook(KCalCore::IncidenceBase::DeserializerHook, &in);
740
741 return in;
742}
743
744IncidenceBase::IncidenceObserver::~IncidenceObserver()
745{
746}
747