2 * calendarmigrator.cpp - migrates or creates KAlarm Akonadi resources
4 * Copyright © 2011 by David Jarvie <djarvie@kde.org>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 #include "calendarmigrator.h"
22 #include "akonadimodel.h"
23 #include "kalarmsettings.h"
24 #include "kalarmdirsettings.h"
25 #include "collectionattribute.h"
26 #include "compatibilityattribute.h"
27 #include "mainwindow.h"
28 #include "messagebox.h"
31 #include <akonadi/agentinstancecreatejob.h>
32 #include <akonadi/agentmanager.h>
33 #include <akonadi/collectionfetchjob.h>
34 #include <akonadi/collectionfetchscope.h>
35 #include <akonadi/collectionmodifyjob.h>
36 #include <akonadi/entitydisplayattribute.h>
37 #include <akonadi/resourcesynchronizationjob.h>
40 #include <kconfiggroup.h>
41 #include <kstandarddirs.h>
46 using namespace Akonadi
;
47 using KAlarm::CollectionAttribute
;
48 using KAlarm::CompatibilityAttribute
;
51 // Creates, or migrates from KResources, a single alarm calendar
52 class CalendarCreator
: public QObject
56 CalendarCreator(const QString
& resourceType
, const KConfigGroup
&);
57 CalendarCreator(KAlarm::CalEvent::Type
, const QString
& file
, const QString
& name
);
58 bool isValid() const { return mAlarmType
!= KAlarm::CalEvent::EMPTY
; }
59 bool newCalendar() const { return mNew
; }
60 QString
resourceName() const { return mName
; }
61 QString
path() const { return mPath
; }
62 QString
errorMessage() const { return mErrorMessage
; }
63 void createAgent(const QString
& agentType
, QObject
* parent
);
66 void agentCreated(KJob
*);
69 void creating(const QString
& path
);
70 void finished(CalendarCreator
*);
73 void fetchCollection();
74 void collectionFetchResult(KJob
*);
75 void resourceSynchronised(KJob
*);
76 void modifyCollectionJobDone(KJob
*);
79 void finish(bool cleanup
);
80 bool migrateLocalFile();
81 bool migrateLocalDirectory();
82 bool migrateRemoteFile();
83 template <class Interface
> Interface
* migrateBasic();
85 enum ResourceType
{ LocalFile
, LocalDir
, RemoteFile
};
87 Akonadi::AgentInstance mAgent
;
88 KAlarm::CalEvent::Type mAlarmType
;
89 ResourceType mResourceType
;
93 QString mErrorMessage
;
94 int mCollectionFetchRetryCount
;
102 // Updates the backend calendar format of a single alarm calendar
103 class CalendarUpdater
: public QObject
107 CalendarUpdater(const Collection
& collection
, bool dirResource
,
108 bool ignoreKeepFormat
, bool newCollection
, QObject
* parent
);
114 Akonadi::Collection mCollection
;
117 bool mIgnoreKeepFormat
;
122 CalendarMigrator
* CalendarMigrator::mInstance
= 0;
124 CalendarMigrator::CalendarMigrator(QObject
* parent
)
129 CalendarMigrator::~CalendarMigrator()
134 /******************************************************************************
135 * Create and return the unique CalendarMigrator instance.
137 CalendarMigrator
* CalendarMigrator::instance()
140 mInstance
= new CalendarMigrator
;
144 /******************************************************************************
145 * Migrate old KResource calendars, or if none, create default Akonadi resources.
147 void CalendarMigrator::execute()
149 instance()->migrateOrCreate();
152 /******************************************************************************
153 * Migrate old KResource calendars, or if none, create default Akonadi resources.
155 void CalendarMigrator::migrateOrCreate()
158 CalendarCreator
* creator
;
160 // First migrate any KResources alarm calendars from pre-Akonadi versions of KAlarm.
161 bool haveResources
= false;
162 const QString configFile
= KStandardDirs::locateLocal("config", QLatin1String("kresources/alarms/stdrc"));
163 KConfig
config(configFile
, KConfig::SimpleConfig
);
165 // Fetch all the resource identifiers which are actually in use
166 KConfigGroup group
= config
.group("General");
167 QStringList keys
= group
.readEntry("ResourceKeys", QStringList())
168 + group
.readEntry("PassiveResourceKeys", QStringList());
170 // Create an Akonadi resource for each resource id
171 foreach (const QString
& id
, keys
)
173 KConfigGroup configGroup
= config
.group(QLatin1String("Resource_") + id
);
174 QString resourceType
= configGroup
.readEntry("ResourceType", QString());
176 if (resourceType
== QLatin1String("file"))
177 agentType
= QLatin1String("akonadi_kalarm_resource");
178 else if (resourceType
== QLatin1String("dir"))
179 agentType
= QLatin1String("akonadi_kalarm_dir_resource");
180 else if (resourceType
== QLatin1String("remote"))
181 agentType
= QLatin1String("akonadi_kalarm_resource");
183 continue; // unknown resource type - can't convert
185 haveResources
= true;
186 creator
= new CalendarCreator(resourceType
, configGroup
);
187 if (!creator
->isValid())
191 connect(creator
, SIGNAL(finished(CalendarCreator
*)), SLOT(calendarCreated(CalendarCreator
*)));
192 connect(creator
, SIGNAL(creating(QString
)), SLOT(creatingCalendar(QString
)));
193 mCalendarsPending
<< creator
;
194 creator
->createAgent(agentType
, this);
200 // There were no KResources calendars to migrate, so create default ones.
201 // Normally this occurs on first installation of KAlarm.
202 // If the default files already exist, they will be used; otherwise they
204 creator
= new CalendarCreator(KAlarm::CalEvent::ACTIVE
, QLatin1String("calendar.ics"), i18nc("@info/plain", "Active Alarms"));
205 connect(creator
, SIGNAL(finished(CalendarCreator
*)), SLOT(calendarCreated(CalendarCreator
*)));
206 connect(creator
, SIGNAL(creating(QString
)), SLOT(creatingCalendar(QString
)));
207 mCalendarsPending
<< creator
;
208 creator
->createAgent(QLatin1String("akonadi_kalarm_resource"), this);
210 creator
= new CalendarCreator(KAlarm::CalEvent::ARCHIVED
, QLatin1String("expired.ics"), i18nc("@info/plain", "Archived Alarms"));
211 connect(creator
, SIGNAL(finished(CalendarCreator
*)), SLOT(calendarCreated(CalendarCreator
*)));
212 connect(creator
, SIGNAL(creating(QString
)), SLOT(creatingCalendar(QString
)));
213 mCalendarsPending
<< creator
;
214 creator
->createAgent(QLatin1String("akonadi_kalarm_resource"), this);
216 creator
= new CalendarCreator(KAlarm::CalEvent::TEMPLATE
, QLatin1String("template.ics"), i18nc("@info/plain", "Alarm Templates"));
217 connect(creator
, SIGNAL(finished(CalendarCreator
*)), SLOT(calendarCreated(CalendarCreator
*)));
218 connect(creator
, SIGNAL(creating(QString
)), SLOT(creatingCalendar(QString
)));
219 mCalendarsPending
<< creator
;
220 creator
->createAgent(QLatin1String("akonadi_kalarm_resource"), this);
223 if (mCalendarsPending
.isEmpty())
227 /******************************************************************************
228 * Called when a calendar resource is about to be created.
229 * Emits the 'creating' signal.
231 void CalendarMigrator::creatingCalendar(const QString
& path
)
233 emit
creating(path
, false);
236 /******************************************************************************
237 * Called when creation of a migrated or new default calendar resource has
238 * completed or failed.
240 void CalendarMigrator::calendarCreated(CalendarCreator
* creator
)
242 int i
= mCalendarsPending
.indexOf(creator
);
244 return; // calendar already finished
246 emit
creating(creator
->path(), true);
248 if (!creator
->errorMessage().isEmpty())
250 QString errmsg
= creator
->newCalendar()
251 ? i18nc("@info/plain", "Failed to create default calendar <resource>%1</resource>", creator
->resourceName())
252 : i18nc("@info/plain 'Import Alarms' is the name of a menu option",
253 "Failed to convert old configuration for calendar <resource>%1</resource>. "
254 "Please use Import Alarms to load its alarms into a new or existing calendar.", creator
->resourceName());
255 QString locn
= i18nc("@info/plain File path or URL", "Location: %1", creator
->path());
256 if (creator
->errorMessage().isEmpty())
257 errmsg
= i18nc("@info", "<para>%1</para><para>%2</para>", errmsg
, locn
);
259 errmsg
= i18nc("@info", "<para>%1</para><para>%2<nl/>(%3)</para>", errmsg
, locn
, creator
->errorMessage());
260 KAMessageBox::error(MainWindow::mainMainWindow(), errmsg
);
262 creator
->deleteLater();
264 mCalendarsPending
.removeAt(i
); // remove it from the pending list
265 if (mCalendarsPending
.isEmpty())
269 /******************************************************************************
270 * If an existing Akonadi resource calendar can be converted to the current
271 * KAlarm format, prompt the user whether to convert it, and if yes, tell the
272 * Akonadi resource to update the backend storage to the current format.
273 * The CollectionAttribute's KeepFormat property will be updated if the user
274 * chooses not to update the calendar.
276 * Note: the collection should be up to date: use AkonadiModel::refresh() before
277 * calling this function.
279 void CalendarMigrator::updateToCurrentFormat(const Collection
& collection
, bool ignoreKeepFormat
, QWidget
* parent
)
281 kDebug() << collection
.id();
282 AgentInstance agent
= AgentManager::self()->instance(collection
.resource());
283 const QString id
= agent
.type().identifier();
285 if (id
== QLatin1String("akonadi_kalarm_resource"))
287 else if (id
== QLatin1String("akonadi_kalarm_dir_resource"))
291 kError() << "Invalid agent type" << id
;
294 CalendarUpdater
* updater
= new CalendarUpdater(collection
, dirResource
, ignoreKeepFormat
, false, parent
);
295 QTimer::singleShot(0, updater
, SLOT(update()));
299 CalendarUpdater::CalendarUpdater(const Collection
& collection
, bool dirResource
,
300 bool ignoreKeepFormat
, bool newCollection
, QObject
* parent
)
301 : mCollection(collection
),
303 mDirResource(dirResource
),
304 mIgnoreKeepFormat(ignoreKeepFormat
),
305 mNewCollection(newCollection
)
309 bool CalendarUpdater::update()
311 kDebug() << mCollection
.id() << (mDirResource
? "directory" : "file");
313 if (mCollection
.hasAttribute
<CompatibilityAttribute
>())
315 const CompatibilityAttribute
* compatAttr
= mCollection
.attribute
<CompatibilityAttribute
>();
316 KAlarm::Calendar::Compat compatibility
= compatAttr
->compatibility();
317 if ((compatibility
& ~KAlarm::Calendar::Converted
)
318 // The calendar isn't in the current KAlarm format
319 && !(compatibility
& ~(KAlarm::Calendar::Convertible
| KAlarm::Calendar::Converted
))
320 // The calendar format is convertible to the current KAlarm format
321 && (mIgnoreKeepFormat
322 || !mCollection
.hasAttribute
<CollectionAttribute
>()
323 || !mCollection
.attribute
<CollectionAttribute
>()->keepFormat()))
325 // The user hasn't previously said not to convert it
326 QString versionString
= KAlarm::getVersionString(compatAttr
->version());
327 QString msg
= KAlarm::Calendar::conversionPrompt(mCollection
.name(), versionString
, false);
328 kDebug() << "Version" << versionString
;
329 if (KAMessageBox::warningYesNo(qobject_cast
<QWidget
*>(mParent
), msg
) == KMessageBox::Yes
)
331 // Tell the resource to update the backend storage format
335 // Refetch the collection's details because anything could
336 // have happened since the prompt was first displayed.
337 if (!AkonadiModel::instance()->refresh(mCollection
))
338 errmsg
= i18nc("@info/plain", "Invalid collection");
340 if (errmsg
.isEmpty())
342 AgentInstance agent
= AgentManager::self()->instance(mCollection
.resource());
344 CalendarMigrator::updateStorageFormat
<OrgKdeAkonadiKAlarmDirSettingsInterface
>(agent
, errmsg
, mParent
);
346 CalendarMigrator::updateStorageFormat
<OrgKdeAkonadiKAlarmSettingsInterface
>(agent
, errmsg
, mParent
);
348 if (!errmsg
.isEmpty())
350 KAMessageBox::error(MainWindow::mainMainWindow(),
351 i18nc("@info", "%1<nl/>(%2)",
352 i18nc("@info/plain", "Failed to update format of calendar <resource>%1</resource>", mCollection
.name()),
358 // The user chose not to update the calendar
362 QModelIndex ix
= AkonadiModel::instance()->collectionIndex(mCollection
);
363 AkonadiModel::instance()->setData(ix
, true, AkonadiModel::KeepFormatRole
);
372 /******************************************************************************
373 * Tell an Akonadi resource to update the backend storage format to the current
375 * Reply = true if success; if false, 'errorMessage' contains the error message.
377 template <class Interface
> bool CalendarMigrator::updateStorageFormat(const AgentInstance
& agent
, QString
& errorMessage
, QObject
* parent
)
380 Interface
* iface
= getAgentInterface
<Interface
>(agent
, errorMessage
, parent
);
383 kDebug() << errorMessage
;
386 iface
->setUpdateStorageFormat(true);
387 iface
->writeConfig();
393 /******************************************************************************
394 * Create a D-Bus interface to an Akonadi resource.
395 * Reply = interface if success
396 * = 0 if error: 'errorMessage' contains the error message.
398 template <class Interface
> Interface
* CalendarMigrator::getAgentInterface(const AgentInstance
& agent
, QString
& errorMessage
, QObject
* parent
)
400 Interface
* iface
= new Interface("org.freedesktop.Akonadi.Resource." + agent
.identifier(),
401 "/Settings", QDBusConnection::sessionBus(), parent
);
402 if (!iface
->isValid())
404 errorMessage
= iface
->lastError().message();
405 kDebug() << "D-Bus error accessing resource:" << errorMessage
;
413 /******************************************************************************
414 * Constructor to migrate a KResources calendar, using its parameters.
416 CalendarCreator::CalendarCreator(const QString
& resourceType
, const KConfigGroup
& config
)
417 : mAlarmType(KAlarm::CalEvent::EMPTY
),
421 // Read the resource configuration parameters from the config
422 const char* pathKey
= 0;
423 if (resourceType
== QLatin1String("file"))
425 mResourceType
= LocalFile
;
426 pathKey
= "CalendarURL";
428 else if (resourceType
== QLatin1String("dir"))
430 mResourceType
= LocalDir
;
431 pathKey
= "CalendarURL";
433 else if (resourceType
== QLatin1String("remote"))
435 mResourceType
= RemoteFile
;
436 pathKey
= "DownloadUrl";
440 kError() << "Invalid resource type:" << resourceType
;
443 mPath
= config
.readPathEntry(pathKey
, "");
444 switch (config
.readEntry("AlarmType", (int)0))
446 case 1: mAlarmType
= KAlarm::CalEvent::ACTIVE
; break;
447 case 2: mAlarmType
= KAlarm::CalEvent::ARCHIVED
; break;
448 case 4: mAlarmType
= KAlarm::CalEvent::TEMPLATE
; break;
450 kError() << "Invalid alarm type for resource";
453 mName
= config
.readEntry("ResourceName", QString());
454 mColour
= config
.readEntry("Color", QColor());
455 mReadOnly
= config
.readEntry("ResourceIsReadOnly", true);
456 mEnabled
= config
.readEntry("ResourceIsActive", false);
457 mStandard
= config
.readEntry("Standard", false);
458 kDebug() << "Migrating:" << mName
<< ", type=" << mAlarmType
<< ", path=" << mPath
;
461 /******************************************************************************
462 * Constructor to create a new default local file resource.
463 * This is created as enabled, read-write, and standard for its alarm type.
465 CalendarCreator::CalendarCreator(KAlarm::CalEvent::Type alarmType
, const QString
& file
, const QString
& name
)
466 : mAlarmType(alarmType
),
467 mResourceType(LocalFile
),
476 mPath
= KStandardDirs::locateLocal("appdata", file
);
477 kDebug() << "New:" << mName
<< ", type=" << mAlarmType
<< ", path=" << mPath
;
480 /******************************************************************************
481 * Create the Akonadi agent for this calendar.
483 void CalendarCreator::createAgent(const QString
& agentType
, QObject
* parent
)
485 emit
creating(mPath
);
486 AgentInstanceCreateJob
* job
= new AgentInstanceCreateJob(agentType
, parent
);
487 connect(job
, SIGNAL(result(KJob
*)), SLOT(agentCreated(KJob
*)));
491 /******************************************************************************
492 * Called when the agent creation job for this resource has completed.
493 * Applies the calendar resource configuration to the Akonadi agent.
495 void CalendarCreator::agentCreated(KJob
* j
)
499 mErrorMessage
= j
->errorString();
500 kError() << "AgentInstanceCreateJob error:" << mErrorMessage
;
505 // Configure the Akonadi Agent
507 AgentInstanceCreateJob
* job
= static_cast<AgentInstanceCreateJob
*>(j
);
508 mAgent
= job
->instance();
509 mAgent
.setName(mName
);
511 switch (mResourceType
)
514 ok
= migrateLocalFile();
517 ok
= migrateLocalDirectory();
520 ok
= migrateRemoteFile();
523 kError() << "Invalid resource type";
531 mAgent
.reconfigure(); // notify the agent that its configuration has been changed
533 // Wait for the resource to create its collection.
534 ResourceSynchronizationJob
* sjob
= new ResourceSynchronizationJob(mAgent
);
535 connect(sjob
, SIGNAL(result(KJob
*)), SLOT(resourceSynchronised(KJob
*)));
536 sjob
->start(); // this is required (not an Akonadi::Job)
539 /******************************************************************************
540 * Called when a resource synchronisation job has completed.
541 * Fetches the collection which this agent manages.
543 void CalendarCreator::resourceSynchronised(KJob
* j
)
548 // Don't give up on error - we can still try to fetch the collection
549 kError() << "ResourceSynchronizationJob error: " << j
->errorString();
551 mCollectionFetchRetryCount
= 0;
555 /******************************************************************************
556 * Find the collection which this agent manages.
558 void CalendarCreator::fetchCollection()
560 CollectionFetchJob
* job
= new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel
);
561 job
->fetchScope().setResource(mAgent
.identifier());
562 connect(job
, SIGNAL(result(KJob
*)), SLOT(collectionFetchResult(KJob
*)));
566 bool CalendarCreator::migrateLocalFile()
568 OrgKdeAkonadiKAlarmSettingsInterface
* iface
= migrateBasic
<OrgKdeAkonadiKAlarmSettingsInterface
>();
571 iface
->setMonitorFile(true);
572 iface
->writeConfig(); // save the Agent config changes
577 bool CalendarCreator::migrateLocalDirectory()
579 OrgKdeAkonadiKAlarmDirSettingsInterface
* iface
= migrateBasic
<OrgKdeAkonadiKAlarmDirSettingsInterface
>();
582 iface
->setMonitorFiles(true);
583 iface
->writeConfig(); // save the Agent config changes
588 bool CalendarCreator::migrateRemoteFile()
590 OrgKdeAkonadiKAlarmSettingsInterface
* iface
= migrateBasic
<OrgKdeAkonadiKAlarmSettingsInterface
>();
593 iface
->setMonitorFile(true);
594 iface
->writeConfig(); // save the Agent config changes
599 template <class Interface
> Interface
* CalendarCreator::migrateBasic()
601 Interface
* iface
= CalendarMigrator::getAgentInterface
<Interface
>(mAgent
, mErrorMessage
, this);
604 iface
->setReadOnly(mReadOnly
);
605 iface
->setDisplayName(mName
);
606 iface
->setPath(mPath
);
607 iface
->setAlarmTypes(KAlarm::CalEvent::mimeTypes(mAlarmType
));
608 iface
->setUpdateStorageFormat(false);
613 /******************************************************************************
614 * Called when a collection fetch job has completed.
615 * Obtains the collection handled by the agent, and configures it.
617 void CalendarCreator::collectionFetchResult(KJob
* j
)
622 mErrorMessage
= j
->errorString();
623 kError() << "CollectionFetchJob error: " << mErrorMessage
;
627 CollectionFetchJob
* job
= static_cast<CollectionFetchJob
*>(j
);
628 Collection::List collections
= job
->collections();
629 if (collections
.isEmpty())
631 if (++mCollectionFetchRetryCount
>= 10)
633 mErrorMessage
= i18nc("@info/plain", "New configuration timed out");
634 kError() << "Timeout fetching collection for resource";
638 // Need to wait a bit longer until the resource has initialised and
639 // created its collection. Retry after 200ms.
640 kDebug() << "Retrying";
641 QTimer::singleShot(200, this, SLOT(fetchCollection()));
644 if (collections
.count() > 1)
646 mErrorMessage
= i18nc("@info/plain", "New configuration was corrupt");
647 kError() << "Wrong number of collections for this resource:" << collections
.count();
652 // Set Akonadi Collection attributes
653 Collection collection
= collections
[0];
654 collection
.setContentMimeTypes(KAlarm::CalEvent::mimeTypes(mAlarmType
));
655 EntityDisplayAttribute
* dattr
= collection
.attribute
<EntityDisplayAttribute
>(Collection::AddIfMissing
);
656 dattr
->setIconName("kalarm");
657 CollectionAttribute
* attr
= collection
.attribute
<CollectionAttribute
>(Entity::AddIfMissing
);
658 attr
->setEnabled(mEnabled
? mAlarmType
: KAlarm::CalEvent::EMPTY
);
660 attr
->setStandard(mAlarmType
);
661 if (mColour
.isValid())
662 attr
->setBackgroundColor(mColour
);
664 // Update the calendar to the current KAlarm format if necessary,
665 // and if the user agrees.
666 bool dirResource
= false;
667 switch (mResourceType
)
676 Q_ASSERT(0); // Invalid resource type
679 CalendarUpdater
* updater
= new CalendarUpdater(collection
, dirResource
, false, true, this);
680 if (!updater
->update()) // note that 'updater' will auto-delete when finished
682 // Record that the user chose not to update the calendar
683 attr
->setKeepFormat(true);
686 // Update the collection's attributes in the Akonadi database
687 CollectionModifyJob
* cmjob
= new CollectionModifyJob(collection
, this);
688 connect(cmjob
, SIGNAL(result(KJob
*)), this, SLOT(modifyCollectionJobDone(KJob
*)));
691 /******************************************************************************
692 * Called when a collection modification job has completed.
693 * Checks for any error.
695 void CalendarCreator::modifyCollectionJobDone(KJob
* j
)
697 Collection collection
= static_cast<CollectionModifyJob
*>(j
)->collection();
700 mErrorMessage
= j
->errorString();
701 kError() << "CollectionFetchJob error: " << mErrorMessage
;
706 kDebug() << "Completed:" << mName
;
711 /******************************************************************************
712 * Emit the finished() signal. If 'cleanup' is true, delete the newly created
713 * but incomplete Agent.
715 void CalendarCreator::finish(bool cleanup
)
720 AgentManager::self()->removeInstance(mAgent
);
726 #include "calendarmigrator.moc"
727 #include "moc_calendarmigrator.cpp"