1/*
2 Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "specialcollectionshelperjobs_p.h"
21
22#include "dbusconnectionpool.h"
23#include "specialcollectionattribute_p.h"
24#include "specialcollections.h"
25#include "servermanager.h"
26
27#include <akonadi/agentinstance.h>
28#include <akonadi/agentinstancecreatejob.h>
29#include <akonadi/agentmanager.h>
30#include <akonadi/collectionfetchjob.h>
31#include <akonadi/collectionfetchscope.h>
32#include <akonadi/collectionmodifyjob.h>
33#include <akonadi/entitydisplayattribute.h>
34#include <akonadi/resourcesynchronizationjob.h>
35
36#include <KDebug>
37#include <KLocalizedString>
38#include <kcoreconfigskeleton.h>
39
40#include <QtDBus/QDBusConnectionInterface>
41#include <QtDBus/QDBusInterface>
42#include <QtDBus/QDBusServiceWatcher>
43#include <QtCore/QMetaMethod>
44#include <QtCore/QTime>
45#include <QtCore/QTimer>
46
47#define LOCK_WAIT_TIMEOUT_SECONDS 10
48
49using namespace Akonadi;
50
51// convenient methods to get/set the default resource id
52static void setDefaultResourceId(KCoreConfigSkeleton *settings, const QString &value)
53{
54 KConfigSkeletonItem *item = settings->findItem(QLatin1String("DefaultResourceId"));
55 Q_ASSERT(item);
56 item->setProperty(value);
57}
58
59static QString defaultResourceId(KCoreConfigSkeleton *settings)
60{
61 const KConfigSkeletonItem *item = settings->findItem(QLatin1String("DefaultResourceId"));
62 Q_ASSERT(item);
63 return item->property().toString();
64}
65
66static QString dbusServiceName()
67{
68 QString service = QString::fromLatin1("org.kde.pim.SpecialCollections");
69 if (ServerManager::hasInstanceIdentifier()) {
70 return service + ServerManager::instanceIdentifier();
71 }
72 return service;
73}
74
75static QVariant::Type argumentType(const QMetaObject *mo, const QString &method)
76{
77 QMetaMethod m;
78 for (int i = 0; i < mo->methodCount(); ++i) {
79 const QString signature = QString::fromLatin1(mo->method(i).signature());
80 if (signature.startsWith(method)) {
81 m = mo->method(i);
82 }
83 }
84
85 if (!m.signature()) {
86 return QVariant::Invalid;
87 }
88
89 const QList<QByteArray> argTypes = m.parameterTypes();
90 if (argTypes.count() != 1) {
91 return QVariant::Invalid;
92 }
93
94 return QVariant::nameToType(argTypes.first());
95}
96
97// ===================== ResourceScanJob ============================
98
99/**
100 @internal
101*/
102class Akonadi::ResourceScanJob::Private
103{
104public:
105 Private(KCoreConfigSkeleton *settings, ResourceScanJob *qq);
106
107 void fetchResult(KJob *job); // slot
108
109 ResourceScanJob *const q;
110
111 // Input:
112 QString mResourceId;
113 KCoreConfigSkeleton *mSettings;
114
115 // Output:
116 Collection mRootCollection;
117 Collection::List mSpecialCollections;
118};
119
120ResourceScanJob::Private::Private(KCoreConfigSkeleton *settings, ResourceScanJob *qq)
121 : q(qq)
122 , mSettings(settings)
123{
124}
125
126void ResourceScanJob::Private::fetchResult(KJob *job)
127{
128 if (job->error()) {
129 kWarning() << job->errorText();
130 return;
131 }
132
133 CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>(job);
134 Q_ASSERT(fetchJob);
135
136 Q_ASSERT(!mRootCollection.isValid());
137 Q_ASSERT(mSpecialCollections.isEmpty());
138 foreach (const Collection &collection, fetchJob->collections()) {
139 if (collection.parentCollection() == Collection::root()) {
140 if (mRootCollection.isValid()) {
141 kWarning() << "Resource has more than one root collection. I don't know what to do.";
142 } else {
143 mRootCollection = collection;
144 }
145 }
146
147 if (collection.hasAttribute<SpecialCollectionAttribute>()) {
148 mSpecialCollections.append(collection);
149 }
150 }
151
152 kDebug() << "Fetched root collection" << mRootCollection.id()
153 << "and" << mSpecialCollections.count() << "local folders"
154 << "(total" << fetchJob->collections().count() << "collections).";
155
156 if (!mRootCollection.isValid()) {
157 q->setError(Unknown);
158 q->setErrorText(i18n("Could not fetch root collection of resource %1.", mResourceId));
159 q->emitResult();
160 return;
161 }
162
163 // We are done!
164 q->emitResult();
165}
166
167ResourceScanJob::ResourceScanJob(const QString &resourceId, KCoreConfigSkeleton *settings, QObject *parent)
168 : Job(parent)
169 , d(new Private(settings, this))
170{
171 setResourceId(resourceId);
172}
173
174ResourceScanJob::~ResourceScanJob()
175{
176 delete d;
177}
178
179QString ResourceScanJob::resourceId() const
180{
181 return d->mResourceId;
182}
183
184void ResourceScanJob::setResourceId(const QString &resourceId)
185{
186 d->mResourceId = resourceId;
187}
188
189Akonadi::Collection ResourceScanJob::rootResourceCollection() const
190{
191 return d->mRootCollection;
192}
193
194Akonadi::Collection::List ResourceScanJob::specialCollections() const
195{
196 return d->mSpecialCollections;
197}
198
199void ResourceScanJob::doStart()
200{
201 if (d->mResourceId.isEmpty()) {
202 kError() << "No resource ID given.";
203 setError(Job::Unknown);
204 setErrorText(i18n("No resource ID given."));
205 emitResult();
206 return;
207 }
208
209 CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(),
210 CollectionFetchJob::Recursive, this);
211 fetchJob->fetchScope().setResource(d->mResourceId);
212 fetchJob->fetchScope().setIncludeStatistics(true);
213 connect(fetchJob, SIGNAL(result(KJob*)), this, SLOT(fetchResult(KJob*)));
214}
215
216// ===================== DefaultResourceJob ============================
217
218/**
219 @internal
220*/
221class Akonadi::DefaultResourceJobPrivate
222{
223public:
224 DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq);
225
226 void tryFetchResource();
227 void resourceCreateResult(KJob *job); // slot
228 void resourceSyncResult(KJob *job); // slot
229 void collectionFetchResult(KJob *job); // slot
230 void collectionModifyResult(KJob *job); // slot
231
232 DefaultResourceJob *const q;
233 KCoreConfigSkeleton *mSettings;
234 bool mResourceWasPreexisting;
235 int mPendingModifyJobs;
236 QString mDefaultResourceType;
237 QVariantMap mDefaultResourceOptions;
238 QList<QByteArray> mKnownTypes;
239 QMap<QByteArray, QString> mNameForTypeMap;
240 QMap<QByteArray, QString> mIconForTypeMap;
241};
242
243DefaultResourceJobPrivate::DefaultResourceJobPrivate(KCoreConfigSkeleton *settings, DefaultResourceJob *qq)
244 : q(qq)
245 , mSettings(settings)
246 , mResourceWasPreexisting(true /* for safety, so as not to accidentally delete data */)
247 , mPendingModifyJobs(0)
248{
249}
250
251void DefaultResourceJobPrivate::tryFetchResource()
252{
253 // Get the resourceId from config. Another instance might have changed it in the meantime.
254 mSettings->readConfig();
255
256 const QString resourceId = defaultResourceId(mSettings);
257
258 kDebug() << "Read defaultResourceId" << resourceId << "from config.";
259
260 const AgentInstance resource = AgentManager::self()->instance(resourceId);
261 if (resource.isValid()) {
262 // The resource exists; scan it.
263 mResourceWasPreexisting = true;
264 kDebug() << "Found resource" << resourceId;
265 q->setResourceId(resourceId);
266
267 CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q);
268 fetchJob->fetchScope().setResource(resourceId);
269 fetchJob->fetchScope().setIncludeStatistics(true);
270 q->connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*)));
271 } else {
272 // Try harder: maybe the default resource has been removed and another one added
273 // without updating the config file, in this case search for a resource
274 // of the same type and the default name
275 const AgentInstance::List resources = AgentManager::self()->instances();
276 foreach (const AgentInstance &resource, resources) {
277 if (resource.type().identifier() == mDefaultResourceType) {
278 if (resource.name() == mDefaultResourceOptions.value(QLatin1String("Name")).toString()) {
279 // found a matching one...
280 setDefaultResourceId(mSettings, resource.identifier());
281 mSettings->writeConfig();
282 mResourceWasPreexisting = true;
283 kDebug() << "Found resource" << resource.identifier();
284 q->setResourceId(resource.identifier());
285 q->ResourceScanJob::doStart();
286 return;
287 }
288 }
289 }
290
291 // Create the resource.
292 mResourceWasPreexisting = false;
293 kDebug() << "Creating maildir resource.";
294 const AgentType type = AgentManager::self()->type(mDefaultResourceType);
295 AgentInstanceCreateJob *job = new AgentInstanceCreateJob(type, q);
296 QObject::connect(job, SIGNAL(result(KJob*)), q, SLOT(resourceCreateResult(KJob*)));
297 job->start(); // non-Akonadi::Job
298 }
299}
300
301void DefaultResourceJobPrivate::resourceCreateResult(KJob *job)
302{
303 if (job->error()) {
304 kWarning() << job->errorText();
305 //fail( i18n( "Failed to create the default resource (%1).", job->errorString() ) );
306 q->setError(job->error());
307 q->setErrorText(job->errorText());
308 q->emitResult();
309 return;
310 }
311
312 AgentInstance agent;
313
314 // Get the resource instance.
315 {
316 AgentInstanceCreateJob *createJob = qobject_cast<AgentInstanceCreateJob *>(job);
317 Q_ASSERT(createJob);
318 agent = createJob->instance();
319 setDefaultResourceId(mSettings, agent.identifier());
320 kDebug() << "Created maildir resource with id" << defaultResourceId(mSettings);
321 }
322
323 const QString defaultId = defaultResourceId(mSettings);
324
325 // Configure the resource.
326 {
327 agent.setName(mDefaultResourceOptions.value(QLatin1String("Name")).toString());
328
329 QDBusInterface conf(QString::fromLatin1("org.freedesktop.Akonadi.Resource.") + defaultId,
330 QString::fromLatin1("/Settings"), QString());
331
332 if (!conf.isValid()) {
333 q->setError(-1);
334 q->setErrorText(i18n("Invalid resource identifier '%1'", defaultId));
335 q->emitResult();
336 return;
337 }
338
339 QMapIterator<QString, QVariant> it(mDefaultResourceOptions);
340 while (it.hasNext()) {
341 it.next();
342
343 if (it.key() == QLatin1String("Name")) {
344 continue;
345 }
346
347 const QString methodName = QString::fromLatin1("set%1").arg(it.key());
348 const QVariant::Type argType = argumentType(conf.metaObject(), methodName);
349 if (argType == QVariant::Invalid) {
350 q->setError(Job::Unknown);
351 q->setErrorText(i18n("Failed to configure default resource via D-Bus."));
352 q->emitResult();
353 return;
354 }
355
356 QDBusReply<void> reply = conf.call(methodName, it.value());
357 if (!reply.isValid()) {
358 q->setError(Job::Unknown);
359 q->setErrorText(i18n("Failed to configure default resource via D-Bus."));
360 q->emitResult();
361 return;
362 }
363 }
364
365 conf.call(QLatin1String("writeConfig"));
366
367 agent.reconfigure();
368 }
369
370 // Sync the resource.
371 {
372 ResourceSynchronizationJob *syncJob = new ResourceSynchronizationJob(agent, q);
373 QObject::connect(syncJob, SIGNAL(result(KJob*)), q, SLOT(resourceSyncResult(KJob*)));
374 syncJob->start(); // non-Akonadi
375 }
376}
377
378void DefaultResourceJobPrivate::resourceSyncResult(KJob *job)
379{
380 if (job->error()) {
381 kWarning() << job->errorText();
382 //fail( i18n( "ResourceSynchronizationJob failed (%1).", job->errorString() ) );
383 return;
384 }
385
386 // Fetch the collections of the resource.
387 kDebug() << "Fetching maildir collections.";
388 CollectionFetchJob *fetchJob = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive, q);
389 fetchJob->fetchScope().setResource(defaultResourceId(mSettings));
390 QObject::connect(fetchJob, SIGNAL(result(KJob*)), q, SLOT(collectionFetchResult(KJob*)));
391}
392
393void DefaultResourceJobPrivate::collectionFetchResult(KJob *job)
394{
395 if (job->error()) {
396 kWarning() << job->errorText();
397 //fail( i18n( "Failed to fetch the root maildir collection (%1).", job->errorString() ) );
398 return;
399 }
400
401 CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>(job);
402 Q_ASSERT(fetchJob);
403
404 const Collection::List collections = fetchJob->collections();
405 kDebug() << "Fetched" << collections.count() << "collections.";
406
407 // Find the root maildir collection.
408 Collection::List toRecover;
409 Collection resourceCollection;
410 foreach (const Collection &collection, collections) {
411 if (collection.parentCollection() == Collection::root()) {
412 resourceCollection = collection;
413 toRecover.append(collection);
414 break;
415 }
416 }
417
418 if (!resourceCollection.isValid()) {
419 q->setError(Job::Unknown);
420 q->setErrorText(i18n("Failed to fetch the resource collection."));
421 q->emitResult();
422 return;
423 }
424
425 // Find all children of the resource collection.
426 foreach (const Collection &collection, collections) {
427 if (collection.parentCollection() == resourceCollection) {
428 toRecover.append(collection);
429 }
430 }
431
432 QHash<QString, QByteArray> typeForName;
433 foreach (const QByteArray &type, mKnownTypes) {
434 const QString displayName = mNameForTypeMap.value(type);
435 typeForName[displayName] = type;
436 }
437
438 // These collections have been created by the maildir resource, when it
439 // found the folders on disk. So give them the necessary attributes now.
440 Q_ASSERT(mPendingModifyJobs == 0);
441 foreach (Collection collection, toRecover) { // krazy:exclude=foreach
442
443 if (collection.hasAttribute<SpecialCollectionAttribute>()) {
444 continue;
445 }
446
447 // Find the type for the collection.
448 const QString name = collection.displayName();
449 const QByteArray type = typeForName.value(name);
450
451 if (!type.isEmpty()) {
452 kDebug() << "Recovering collection" << name;
453 setCollectionAttributes(collection, type, mNameForTypeMap, mIconForTypeMap);
454
455 CollectionModifyJob *modifyJob = new CollectionModifyJob(collection, q);
456 QObject::connect(modifyJob, SIGNAL(result(KJob*)), q, SLOT(collectionModifyResult(KJob*)));
457 mPendingModifyJobs++;
458 } else {
459 kDebug() << "Searching for names: " << typeForName.keys();
460 kDebug() << "Unknown collection name" << name << "-- not recovering.";
461 }
462 }
463
464 if (mPendingModifyJobs == 0) {
465 // Scan the resource.
466 q->setResourceId(defaultResourceId(mSettings));
467 q->ResourceScanJob::doStart();
468 }
469}
470
471void DefaultResourceJobPrivate::collectionModifyResult(KJob *job)
472{
473 if (job->error()) {
474 kWarning() << job->errorText();
475 //fail( i18n( "Failed to modify the root maildir collection (%1).", job->errorString() ) );
476 return;
477 }
478
479 Q_ASSERT(mPendingModifyJobs > 0);
480 mPendingModifyJobs--;
481 kDebug() << "pendingModifyJobs now" << mPendingModifyJobs;
482 if (mPendingModifyJobs == 0) {
483 // Write the updated config.
484 kDebug() << "Writing defaultResourceId" << defaultResourceId(mSettings) << "to config.";
485 mSettings->writeConfig();
486
487 // Scan the resource.
488 q->setResourceId(defaultResourceId(mSettings));
489 q->ResourceScanJob::doStart();
490 }
491}
492
493DefaultResourceJob::DefaultResourceJob(KCoreConfigSkeleton *settings, QObject *parent)
494 : ResourceScanJob(QString(), settings, parent)
495 , d(new DefaultResourceJobPrivate(settings, this))
496{
497}
498
499DefaultResourceJob::~DefaultResourceJob()
500{
501 delete d;
502}
503
504void DefaultResourceJob::setDefaultResourceType(const QString &type)
505{
506 d->mDefaultResourceType = type;
507}
508
509void DefaultResourceJob::setDefaultResourceOptions(const QVariantMap &options)
510{
511 d->mDefaultResourceOptions = options;
512}
513
514void DefaultResourceJob::setTypes(const QList<QByteArray> &types)
515{
516 d->mKnownTypes = types;
517}
518
519void DefaultResourceJob::setNameForTypeMap(const QMap<QByteArray, QString> &map)
520{
521 d->mNameForTypeMap = map;
522}
523
524void DefaultResourceJob::setIconForTypeMap(const QMap<QByteArray, QString> &map)
525{
526 d->mIconForTypeMap = map;
527}
528
529void DefaultResourceJob::doStart()
530{
531 d->tryFetchResource();
532}
533
534void DefaultResourceJob::slotResult(KJob *job)
535{
536 if (job->error()) {
537 kWarning() << job->errorText();
538 // Do some cleanup.
539 if (!d->mResourceWasPreexisting) {
540 // We only removed the resource instance if we have created it.
541 // Otherwise we might lose the user's data.
542 const AgentInstance resource = AgentManager::self()->instance(defaultResourceId(d->mSettings));
543 kDebug() << "Removing resource" << resource.identifier();
544 AgentManager::self()->removeInstance(resource);
545 }
546 }
547
548 Job::slotResult(job);
549}
550
551// ===================== GetLockJob ============================
552
553class Akonadi::GetLockJob::Private
554{
555public:
556 Private(GetLockJob *qq);
557
558 void doStart(); // slot
559 void serviceOwnerChanged(const QString &name, const QString &oldOwner,
560 const QString &newOwner); // slot
561 void timeout(); // slot
562
563 GetLockJob *const q;
564 QTimer *mSafetyTimer;
565};
566
567GetLockJob::Private::Private(GetLockJob *qq)
568 : q(qq)
569 , mSafetyTimer(0)
570{
571}
572
573void GetLockJob::Private::doStart()
574{
575 // Just doing registerService() and checking its return value is not sufficient,
576 // since we may *already* own the name, and then registerService() returns true.
577
578 QDBusConnection bus = DBusConnectionPool::threadConnection();
579 const bool alreadyLocked = bus.interface()->isServiceRegistered(dbusServiceName());
580 const bool gotIt = bus.registerService(dbusServiceName());
581
582 if (gotIt && !alreadyLocked) {
583 //kDebug() << "Got lock immediately.";
584 q->emitResult();
585 } else {
586 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(dbusServiceName(), DBusConnectionPool::threadConnection(),
587 QDBusServiceWatcher::WatchForOwnerChange, q);
588 //kDebug() << "Waiting for lock.";
589 connect(watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)),
590 q, SLOT(serviceOwnerChanged(QString,QString,QString)));
591
592 mSafetyTimer = new QTimer(q);
593 mSafetyTimer->setSingleShot(true);
594 mSafetyTimer->setInterval(LOCK_WAIT_TIMEOUT_SECONDS * 1000);
595 mSafetyTimer->start();
596 connect(mSafetyTimer, SIGNAL(timeout()), q, SLOT(timeout()));
597 }
598}
599
600void GetLockJob::Private::serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
601{
602 if (newOwner.isEmpty()) {
603 const bool gotIt = DBusConnectionPool::threadConnection().registerService(dbusServiceName());
604 if (gotIt) {
605 mSafetyTimer->stop();
606 q->emitResult();
607 }
608 }
609}
610
611void GetLockJob::Private::timeout()
612{
613 kWarning() << "Timeout trying to get lock. Check who has acquired the name" << dbusServiceName() << "on DBus, using qdbus or qdbusviewer.";
614 q->setError(Job::Unknown);
615 q->setErrorText(i18n("Timeout trying to get lock."));
616 q->emitResult();
617}
618
619GetLockJob::GetLockJob(QObject *parent)
620 : KJob(parent)
621 , d(new Private(this))
622{
623}
624
625GetLockJob::~GetLockJob()
626{
627 delete d;
628}
629
630void GetLockJob::start()
631{
632 QTimer::singleShot(0, this, SLOT(doStart()));
633}
634
635void Akonadi::setCollectionAttributes(Akonadi::Collection &collection, const QByteArray &type,
636 const QMap<QByteArray, QString> &nameForType,
637 const QMap<QByteArray, QString> &iconForType)
638{
639 {
640 EntityDisplayAttribute *attr = new EntityDisplayAttribute;
641 attr->setIconName(iconForType.value(type));
642 attr->setDisplayName(nameForType.value(type));
643 collection.addAttribute(attr);
644 }
645
646 {
647 SpecialCollectionAttribute *attr = new SpecialCollectionAttribute;
648 attr->setCollectionType(type);
649 collection.addAttribute(attr);
650 }
651}
652
653bool Akonadi::releaseLock()
654{
655 return DBusConnectionPool::threadConnection().unregisterService(dbusServiceName());
656}
657
658#include "moc_specialcollectionshelperjobs_p.cpp"
659