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 | |
52 | using namespace KCal; |
53 | |
54 | /** |
55 | Private class that helps to provide binary compatibility between releases. |
56 | @internal |
57 | */ |
58 | //@cond PRIVATE |
59 | class 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 |
79 | namespace { |
80 | template <typename T> |
81 | void 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 | |
97 | CalendarLocal::CalendarLocal( const KDateTime::Spec &timeSpec ) |
98 | : Calendar( timeSpec ), |
99 | d( new KCal::CalendarLocal::Private ) |
100 | { |
101 | } |
102 | |
103 | CalendarLocal::CalendarLocal( const QString &timeZoneId ) |
104 | : Calendar( timeZoneId ), |
105 | d( new KCal::CalendarLocal::Private ) |
106 | { |
107 | } |
108 | |
109 | CalendarLocal::~CalendarLocal() |
110 | { |
111 | close(); |
112 | delete d; |
113 | } |
114 | |
115 | bool CalendarLocal::load( const QString &fileName, CalFormat *format ) |
116 | { |
117 | d->mFileName = fileName; |
118 | FileStorage storage( this, fileName, format ); |
119 | return storage.load(); |
120 | } |
121 | |
122 | bool 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 | |
132 | bool 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 | |
146 | bool 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 | |
158 | void 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 | |
172 | bool 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 | |
185 | bool 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 | |
202 | void 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 | |
217 | Event *CalendarLocal::event( const QString &uid ) |
218 | { |
219 | return d->mEvents.value( uid ); |
220 | } |
221 | |
222 | bool 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 |
239 | void 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 | |
258 | bool 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 | |
277 | void 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 | |
292 | Todo *CalendarLocal::todo( const QString &uid ) |
293 | { |
294 | return d->mTodos.value( uid ); |
295 | } |
296 | |
297 | Todo::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 | |
309 | Todo::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 | |
324 | Alarm::List CalendarLocal::alarmsTo( const KDateTime &to ) |
325 | { |
326 | return alarms( KDateTime( QDate( 1900, 1, 1 ) ), to ); |
327 | } |
328 | |
329 | Alarm::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 |
362 | void 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 | |
380 | void 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 | |
417 | Event::List CalendarLocal::rawEventsForDate( const QDate &date, |
418 | const KDateTime::Spec ×pec, |
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 = 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 | |
476 | Event::List CalendarLocal::rawEvents( const QDate &start, const QDate &end, |
477 | const KDateTime::Spec ×pec, 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 | |
536 | Event::List CalendarLocal::rawEventsForDate( const KDateTime &kdt ) |
537 | { |
538 | return rawEventsForDate( kdt.date(), kdt.timeSpec() ); |
539 | } |
540 | |
541 | Event::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 | |
553 | bool 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 |
567 | void 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 | |
583 | bool 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 | |
597 | void 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 | |
612 | Journal *CalendarLocal::journal( const QString &uid ) |
613 | { |
614 | return d->mJournals.value( uid ); |
615 | } |
616 | |
617 | Journal::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 | |
629 | Journal::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 | |