1/*
2 This file is part of the kcal library.
3
4 Copyright (c) 1998 Preston Brown <pbrown@kde.org>
5 Copyright (c) 2001,2003,2004 Cornelius Schumacher <schumacher@kde.org>
6 Copyright (C) 2003-2004 Reinhold Kainhofer <reinhold@kainhofer.com>
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Library General Public
10 License as published by the Free Software Foundation; either
11 version 2 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Library General Public License for more details.
17
18 You should have received a copy of the GNU Library General Public License
19 along with this library; see the file COPYING.LIB. If not, write to
20 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 Boston, MA 02110-1301, USA.
22*/
23/**
24 @file
25 This file is part of the API for handling calendar data and
26 defines the CalendarLocal class.
27
28 @brief
29 This class provides a calendar stored as a local file.
30
31 @author Preston Brown \<pbrown@kde.org\>
32 @author Cornelius Schumacher \<schumacher@kde.org\>
33 */
34
35#include "calendarlocal.h"
36
37#include "incidence.h"
38#include "event.h"
39#include "todo.h"
40#include "journal.h"
41#include "filestorage.h"
42#include <QtCore/QDate>
43#include <QtCore/QHash>
44#include <QtCore/QMultiHash>
45#include <QtCore/QString>
46
47#include <kdebug.h>
48#include <kdatetime.h>
49#include <klocalizedstring.h>
50#include <kmessagebox.h>
51
52using namespace KCal;
53
54/**
55 Private class that helps to provide binary compatibility between releases.
56 @internal
57*/
58//@cond PRIVATE
59class KCal::CalendarLocal::Private
60{
61 public:
62 Private() : mFormat(0) {}
63 QString mFileName; // filename where calendar is stored
64 CalFormat *mFormat; // calendar format
65
66 QHash<QString, Event *>mEvents; // hash on uids of all Events
67 QMultiHash<QString, Event *>mEventsForDate;// on start dates of non-recurring, single-day Events
68 QHash<QString, Todo *>mTodos; // hash on uids of all Todos
69 QMultiHash<QString, Todo*>mTodosForDate;// on due dates for all Todos
70 QHash<QString, Journal *>mJournals; // hash on uids of all Journals
71 QMultiHash<QString, Journal *>mJournalsForDate; // on dates of all Journals
72
73 void insertEvent( Event *event );
74 void insertTodo( Todo *todo );
75 void insertJournal( Journal *journal );
76};
77
78// helper
79namespace {
80template <typename T>
81void removeIncidenceFromMultiHashByUID( QMultiHash< QString, T >& container,
82 const QString &key,
83 const QString &uid )
84{
85 const QList<T> values = container.values( key );
86 QListIterator<T> it(values);
87 while ( it.hasNext() ) {
88 T const inc = it.next();
89 if ( inc->uid() == uid ) {
90 container.remove( key, inc );
91 }
92 }
93}
94}
95//@endcond
96
97CalendarLocal::CalendarLocal( const KDateTime::Spec &timeSpec )
98 : Calendar( timeSpec ),
99 d( new KCal::CalendarLocal::Private )
100{
101}
102
103CalendarLocal::CalendarLocal( const QString &timeZoneId )
104 : Calendar( timeZoneId ),
105 d( new KCal::CalendarLocal::Private )
106{
107}
108
109CalendarLocal::~CalendarLocal()
110{
111 close();
112 delete d;
113}
114
115bool CalendarLocal::load( const QString &fileName, CalFormat *format )
116{
117 d->mFileName = fileName;
118 FileStorage storage( this, fileName, format );
119 return storage.load();
120}
121
122bool CalendarLocal::reload()
123{
124 const QString filename = d->mFileName;
125 save();
126 close();
127 d->mFileName = filename;
128 FileStorage storage( this, d->mFileName );
129 return storage.load();
130}
131
132bool CalendarLocal::save()
133{
134 if ( d->mFileName.isEmpty() ) {
135 return false;
136 }
137
138 if ( isModified() ) {
139 FileStorage storage( this, d->mFileName, d->mFormat );
140 return storage.save();
141 } else {
142 return true;
143 }
144}
145
146bool CalendarLocal::save( const QString &fileName, CalFormat *format )
147{
148 // Save only if the calendar is either modified, or saved to a
149 // different file than it was loaded from
150 if ( d->mFileName != fileName || isModified() ) {
151 FileStorage storage( this, fileName, format );
152 return storage.save();
153 } else {
154 return true;
155 }
156}
157
158void CalendarLocal::close()
159{
160 setObserversEnabled( false );
161 d->mFileName.clear();
162
163 deleteAllEvents();
164 deleteAllTodos();
165 deleteAllJournals();
166
167 setModified( false );
168
169 setObserversEnabled( true );
170}
171
172bool CalendarLocal::addEvent( Event *event )
173{
174 d->insertEvent( event );
175
176 event->registerObserver( this );
177
178 setModified( true );
179
180 notifyIncidenceAdded( event );
181
182 return true;
183}
184
185bool CalendarLocal::deleteEvent( Event *event )
186{
187 const QString uid = event->uid();
188 if ( d->mEvents.remove( uid ) ) {
189 setModified( true );
190 notifyIncidenceDeleted( event );
191 if ( !event->recurs() ) {
192 removeIncidenceFromMultiHashByUID<Event *>(
193 d->mEventsForDate, event->dtStart().date().toString(), event->uid() );
194 }
195 return true;
196 } else {
197 kWarning() << "Event not found.";
198 return false;
199 }
200}
201
202void CalendarLocal::deleteAllEvents()
203{
204 QHashIterator<QString, Event *>i( d->mEvents );
205 while ( i.hasNext() ) {
206 i.next();
207 notifyIncidenceDeleted( i.value() );
208 // suppress update notifications for the relation removal triggered
209 // by the following deletions
210 i.value()->startUpdates();
211 }
212 qDeleteAll( d->mEvents );
213 d->mEvents.clear();
214 d->mEventsForDate.clear();
215}
216
217Event *CalendarLocal::event( const QString &uid )
218{
219 return d->mEvents.value( uid );
220}
221
222bool CalendarLocal::addTodo( Todo *todo )
223{
224 d->insertTodo( todo );
225
226 todo->registerObserver( this );
227
228 // Set up sub-to-do relations
229 setupRelations( todo );
230
231 setModified( true );
232
233 notifyIncidenceAdded( todo );
234
235 return true;
236}
237
238//@cond PRIVATE
239void CalendarLocal::Private::insertTodo( Todo *todo )
240{
241 QString uid = todo->uid();
242 if ( !mTodos.contains( uid ) ) {
243 mTodos.insert( uid, todo );
244 if ( todo->hasDueDate() ) {
245 mTodosForDate.insert( todo->dtDue().date().toString(), todo );
246 }
247
248 } else {
249#ifndef NDEBUG
250 // if we already have an to-do with this UID, it must be the same to-do,
251 // otherwise something's really broken
252 Q_ASSERT( mTodos.value( uid ) == todo );
253#endif
254 }
255}
256//@endcond
257
258bool CalendarLocal::deleteTodo( Todo *todo )
259{
260 // Handle orphaned children
261 removeRelations( todo );
262
263 if ( d->mTodos.remove( todo->uid() ) ) {
264 setModified( true );
265 notifyIncidenceDeleted( todo );
266 if ( todo->hasDueDate() ) {
267 removeIncidenceFromMultiHashByUID<Todo *>(
268 d->mTodosForDate, todo->dtDue().date().toString(), todo->uid() );
269 }
270 return true;
271 } else {
272 kWarning() << "Todo not found.";
273 return false;
274 }
275}
276
277void CalendarLocal::deleteAllTodos()
278{
279 QHashIterator<QString, Todo *>i( d->mTodos );
280 while ( i.hasNext() ) {
281 i.next();
282 notifyIncidenceDeleted( i.value() );
283 // suppress update notifications for the relation removal triggered
284 // by the following deletions
285 i.value()->startUpdates();
286 }
287 qDeleteAll( d->mTodos );
288 d->mTodos.clear();
289 d->mTodosForDate.clear();
290}
291
292Todo *CalendarLocal::todo( const QString &uid )
293{
294 return d->mTodos.value( uid );
295}
296
297Todo::List CalendarLocal::rawTodos( TodoSortField sortField,
298 SortDirection sortDirection )
299{
300 Todo::List todoList;
301 QHashIterator<QString, Todo *>i( d->mTodos );
302 while ( i.hasNext() ) {
303 i.next();
304 todoList.append( i.value() );
305 }
306 return sortTodos( &todoList, sortField, sortDirection );
307}
308
309Todo::List CalendarLocal::rawTodosForDate( const QDate &date )
310{
311 Todo::List todoList;
312 Todo *t;
313
314 QString dateStr = date.toString();
315 QMultiHash<QString, Todo *>::const_iterator it = d->mTodosForDate.constFind( dateStr );
316 while ( it != d->mTodosForDate.constEnd() && it.key() == dateStr ) {
317 t = it.value();
318 todoList.append( t );
319 ++it;
320 }
321 return todoList;
322}
323
324Alarm::List CalendarLocal::alarmsTo( const KDateTime &to )
325{
326 return alarms( KDateTime( QDate( 1900, 1, 1 ) ), to );
327}
328
329Alarm::List CalendarLocal::alarms( const KDateTime &from, const KDateTime &to )
330{
331 Alarm::List alarmList;
332 QHashIterator<QString, Event *>ie( d->mEvents );
333 Event *e;
334 while ( ie.hasNext() ) {
335 ie.next();
336 e = ie.value();
337 if ( e->recurs() ) {
338 appendRecurringAlarms( alarmList, e, from, to );
339 } else {
340 appendAlarms( alarmList, e, from, to );
341 }
342 }
343
344 QHashIterator<QString, Todo *>it( d->mTodos );
345 Todo *t;
346 while ( it.hasNext() ) {
347 it.next();
348 t = it.value();
349 if ( !t->isCompleted() ) {
350 if ( t->recurs() ) {
351 appendRecurringAlarms( alarmList, t, from, to );
352 } else {
353 appendAlarms( alarmList, t, from, to );
354 }
355 }
356 }
357
358 return alarmList;
359}
360
361//@cond PRIVATE
362void CalendarLocal::Private::insertEvent( Event *event )
363{
364 QString uid = event->uid();
365 if ( !mEvents.contains( uid ) ) {
366 mEvents.insert( uid, event );
367 if ( !event->recurs() && !event->isMultiDay() ) {
368 mEventsForDate.insert( event->dtStart().date().toString(), event );
369 }
370 } else {
371#ifdef NDEBUG
372 // if we already have an event with this UID, it must be the same event,
373 // otherwise something's really broken
374 Q_ASSERT( mEvents.value( uid ) == event );
375#endif
376 }
377}
378//@endcond
379
380void CalendarLocal::incidenceUpdated( IncidenceBase *incidence )
381{
382 KDateTime nowUTC = KDateTime::currentUtcDateTime();
383 incidence->setLastModified( nowUTC );
384 // we should probably update the revision number here,
385 // or internally in the Event itself when certain things change.
386 // need to verify with ical documentation.
387
388 if ( incidence->type() == "Event" ) {
389 Event *event = static_cast<Event*>( incidence );
390 removeIncidenceFromMultiHashByUID<Event *>(
391 d->mEventsForDate, event->dtStart().date().toString(), event->uid() );
392 if ( !event->recurs() && !event->isMultiDay() ) {
393 d->mEventsForDate.insert( event->dtStart().date().toString(), event );
394 }
395 } else if ( incidence->type() == "Todo" ) {
396 Todo *todo = static_cast<Todo*>( incidence );
397 removeIncidenceFromMultiHashByUID<Todo *>(
398 d->mTodosForDate, todo->dtDue().date().toString(), todo->uid() );
399 if ( todo->hasDueDate() ) {
400 d->mTodosForDate.insert( todo->dtDue().date().toString(), todo );
401 }
402 } else if ( incidence->type() == "Journal" ) {
403 Journal *journal = static_cast<Journal*>( incidence );
404 removeIncidenceFromMultiHashByUID<Journal *>(
405 d->mJournalsForDate, journal->dtStart().date().toString(), journal->uid() );
406 d->mJournalsForDate.insert( journal->dtStart().date().toString(), journal );
407 } else {
408 Q_ASSERT( false );
409 }
410
411 // The static_cast is ok as the CalendarLocal only observes Incidence objects
412 notifyIncidenceChanged( static_cast<Incidence *>( incidence ) );
413
414 setModified( true );
415}
416
417Event::List CalendarLocal::rawEventsForDate( const QDate &date,
418 const KDateTime::Spec &timespec,
419 EventSortField sortField,
420 SortDirection sortDirection )
421{
422 Event::List eventList;
423 Event *ev;
424
425 // Find the hash for the specified date
426 QString dateStr = date.toString();
427 QMultiHash<QString, Event *>::const_iterator it = d->mEventsForDate.constFind( dateStr );
428 // Iterate over all non-recurring, single-day events that start on this date
429 KDateTime::Spec ts = timespec.isValid() ? timespec : timeSpec();
430 KDateTime kdt( date, ts );
431 while ( it != d->mEventsForDate.constEnd() && it.key() == dateStr ) {
432 ev = it.value();
433 KDateTime end( ev->dtEnd().toTimeSpec( ev->dtStart() ) );
434 if ( ev->allDay() ) {
435 end.setDateOnly( true );
436 } else {
437 end = end.addSecs( -1 );
438 }
439 if ( end >= kdt ) {
440 eventList.append( ev );
441 }
442 ++it;
443 }
444
445 // Iterate over all events. Look for recurring events that occur on this date
446 QHashIterator<QString, Event *>i( d->mEvents );
447 while ( i.hasNext() ) {
448 i.next();
449 ev = i.value();
450 if ( ev->recurs() ) {
451 if ( ev->isMultiDay() ) {
452 int extraDays = ev->dtStart().date().daysTo( ev->dtEnd().date() );
453 for ( int i = 0; i <= extraDays; ++i ) {
454 if ( ev->recursOn( date.addDays( -i ), ts ) ) {
455 eventList.append( ev );
456 break;
457 }
458 }
459 } else {
460 if ( ev->recursOn( date, ts ) ) {
461 eventList.append( ev );
462 }
463 }
464 } else {
465 if ( ev->isMultiDay() ) {
466 if ( ev->dtStart().date() <= date && ev->dtEnd().date() >= date ) {
467 eventList.append( ev );
468 }
469 }
470 }
471 }
472
473 return sortEventsForDate( &eventList, date, timespec, sortField, sortDirection );
474}
475
476Event::List CalendarLocal::rawEvents( const QDate &start, const QDate &end,
477 const KDateTime::Spec &timespec, bool inclusive )
478{
479 Event::List eventList;
480 KDateTime::Spec ts = timespec.isValid() ? timespec : timeSpec();
481 KDateTime st( start, ts );
482 KDateTime nd( end, ts );
483 KDateTime yesterStart = st.addDays( -1 );
484
485 // Get non-recurring events
486 QHashIterator<QString, Event *>i( d->mEvents );
487 Event *event;
488 while ( i.hasNext() ) {
489 i.next();
490 event = i.value();
491 KDateTime rStart = event->dtStart();
492 if ( nd < rStart ) {
493 continue;
494 }
495 if ( inclusive && rStart < st ) {
496 continue;
497 }
498
499 if ( !event->recurs() ) { // non-recurring events
500 KDateTime rEnd = event->dtEnd();
501 if ( rEnd < st ) {
502 continue;
503 }
504 if ( inclusive && nd < rEnd ) {
505 continue;
506 }
507 } else { // recurring events
508 switch( event->recurrence()->duration() ) {
509 case -1: // infinite
510 if ( inclusive ) {
511 continue;
512 }
513 break;
514 case 0: // end date given
515 default: // count given
516 KDateTime rEnd( event->recurrence()->endDate(), ts );
517 if ( !rEnd.isValid() ) {
518 continue;
519 }
520 if ( rEnd < st ) {
521 continue;
522 }
523 if ( inclusive && nd < rEnd ) {
524 continue;
525 }
526 break;
527 } // switch(duration)
528 } //if(recurs)
529
530 eventList.append( event );
531 }
532
533 return eventList;
534}
535
536Event::List CalendarLocal::rawEventsForDate( const KDateTime &kdt )
537{
538 return rawEventsForDate( kdt.date(), kdt.timeSpec() );
539}
540
541Event::List CalendarLocal::rawEvents( EventSortField sortField,
542 SortDirection sortDirection )
543{
544 Event::List eventList;
545 QHashIterator<QString, Event *>i( d->mEvents );
546 while ( i.hasNext() ) {
547 i.next();
548 eventList.append( i.value() );
549 }
550 return sortEvents( &eventList, sortField, sortDirection );
551}
552
553bool CalendarLocal::addJournal( Journal *journal )
554{
555 d->insertJournal( journal );
556
557 journal->registerObserver( this );
558
559 setModified( true );
560
561 notifyIncidenceAdded( journal );
562
563 return true;
564}
565
566//@cond PRIVATE
567void CalendarLocal::Private::insertJournal( Journal *journal )
568{
569 QString uid = journal->uid();
570 if ( !mJournals.contains( uid ) ) {
571 mJournals.insert( uid, journal );
572 mJournalsForDate.insert( journal->dtStart().date().toString(), journal );
573 } else {
574#ifndef NDEBUG
575 // if we already have an journal with this UID, it must be the same journal,
576 // otherwise something's really broken
577 Q_ASSERT( mJournals.value( uid ) == journal );
578#endif
579 }
580}
581//@endcond
582
583bool CalendarLocal::deleteJournal( Journal *journal )
584{
585 if ( d->mJournals.remove( journal->uid() ) ) {
586 setModified( true );
587 notifyIncidenceDeleted( journal );
588 removeIncidenceFromMultiHashByUID<Journal *>(
589 d->mJournalsForDate, journal->dtStart().date().toString(), journal->uid() );
590 return true;
591 } else {
592 kWarning() << "Journal not found.";
593 return false;
594 }
595}
596
597void CalendarLocal::deleteAllJournals()
598{
599 QHashIterator<QString, Journal *>i( d->mJournals );
600 while ( i.hasNext() ) {
601 i.next();
602 notifyIncidenceDeleted( i.value() );
603 // suppress update notifications for the relation removal triggered
604 // by the following deletions
605 i.value()->startUpdates();
606 }
607 qDeleteAll( d->mJournals );
608 d->mJournals.clear();
609 d->mJournalsForDate.clear();
610}
611
612Journal *CalendarLocal::journal( const QString &uid )
613{
614 return d->mJournals.value( uid );
615}
616
617Journal::List CalendarLocal::rawJournals( JournalSortField sortField,
618 SortDirection sortDirection )
619{
620 Journal::List journalList;
621 QHashIterator<QString, Journal *>i( d->mJournals );
622 while ( i.hasNext() ) {
623 i.next();
624 journalList.append( i.value() );
625 }
626 return sortJournals( &journalList, sortField, sortDirection );
627}
628
629Journal::List CalendarLocal::rawJournalsForDate( const QDate &date )
630{
631 Journal::List journalList;
632 Journal *j;
633
634 QString dateStr = date.toString();
635 QMultiHash<QString, Journal *>::const_iterator it = d->mJournalsForDate.constFind( dateStr );
636
637 while ( it != d->mJournalsForDate.constEnd() && it.key() == dateStr ) {
638 j = it.value();
639 journalList.append( j );
640 ++it;
641 }
642 return journalList;
643}
644