1/*
2 * Copyright (c) 2011 Christian Mollekopf <chrigi_1@fastmail.fm>
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 "trashrestorejob.h"
21
22#include "collection.h"
23#include "entitydeletedattribute.h"
24#include "item.h"
25#include "job_p.h"
26#include "trashsettings.h"
27
28#include <KLocalizedString>
29
30#include <akonadi/itemdeletejob.h>
31#include <akonadi/collectiondeletejob.h>
32#include <akonadi/itemmovejob.h>
33#include <akonadi/collectionmovejob.h>
34#include <akonadi/itemmodifyjob.h>
35#include <akonadi/collectionmodifyjob.h>
36#include <akonadi/collectionfetchjob.h>
37#include <akonadi/itemfetchjob.h>
38#include <akonadi/collectionfetchscope.h>
39#include <akonadi/itemfetchscope.h>
40
41#include <QHash>
42
43using namespace Akonadi;
44
45class TrashRestoreJob::TrashRestoreJobPrivate : public JobPrivate
46{
47public:
48 TrashRestoreJobPrivate(TrashRestoreJob *parent)
49 : JobPrivate(parent)
50 {
51 }
52
53 void selectResult(KJob *job);
54
55 //Called when the target collection was fetched,
56 //will issue the move and the removal of the attributes if collection is valid
57 void targetCollectionFetched(KJob *job);
58
59 void removeAttribute(const Akonadi::Item::List &list);
60 void removeAttribute(const Akonadi::Collection::List &list);
61
62 //Called after initial fetch of items, issues fetch of target collection or removes attributes for in place restore
63 void itemsReceived(const Akonadi::Item::List &items);
64 void collectionsReceived(const Akonadi::Collection::List &collections);
65
66 Q_DECLARE_PUBLIC(TrashRestoreJob)
67
68 Item::List mItems;
69 Collection mCollection;
70 Collection mTargetCollection;
71 QHash<Collection, Item::List> restoreCollections; //groups items to target restore collections
72
73};
74
75void TrashRestoreJob::TrashRestoreJobPrivate::selectResult(KJob *job)
76{
77 Q_Q(TrashRestoreJob);
78 if (job->error()) {
79 kWarning() << job->errorString();
80 return; // KCompositeJob takes care of errors
81 }
82
83 if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) {
84 //kWarning() << "trash restore finished";
85 q->emitResult();
86 }
87}
88
89void TrashRestoreJob::TrashRestoreJobPrivate::targetCollectionFetched(KJob *job)
90{
91 Q_Q(TrashRestoreJob);
92
93 CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *> (job);
94 Q_ASSERT(fetchJob);
95 const Collection::List &list = fetchJob->collections();
96
97 if (list.isEmpty() || !list.first().isValid() || list.first().hasAttribute<Akonadi::EntityDeletedAttribute>()) { //target collection is invalid/not existing
98
99 const QString res = fetchJob->property("Resource").toString();
100 if (res.isEmpty()) { //There is no fallback
101 q->setError(Job::Unknown);
102 q->setErrorText(i18n("Could not find restore collection and restore resource is not available"));
103 q->emitResult();
104 //FAIL
105 kWarning() << "restore collection not available";
106 return;
107 }
108
109 //Try again with the root collection of the resource as fallback
110 CollectionFetchJob *resRootFetch = new CollectionFetchJob(Collection::root(), CollectionFetchJob::FirstLevel, q);
111 resRootFetch->fetchScope().setResource(res);
112 const QVariant &var = fetchJob->property("Items");
113 if (var.isValid()) {
114 resRootFetch->setProperty("Items", var.toInt());
115 }
116 q->connect(resRootFetch, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*)));
117 q->connect(resRootFetch, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
118 return;
119 }
120 Q_ASSERT(list.size() == 1);
121 //SUCCESS
122 //We know where to move the entity, so remove the attributes and move them to the right location
123 if (!mItems.isEmpty()) {
124 const QVariant &var = fetchJob->property("Items");
125 Q_ASSERT(var.isValid());
126 const Item::List &items = restoreCollections[Collection(var.toInt())];
127
128 //store removed attribute if destination collection is valid or the item doesn't have a restore collection
129 //TODO only remove the attribute if the move job was successful (although it is unlikely that it fails since we already fetched the collection)
130 removeAttribute(items);
131 if (items.first().parentCollection() != list.first()) {
132 ItemMoveJob *job = new ItemMoveJob(items, list.first(), q);
133 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
134 }
135 } else {
136 Q_ASSERT(mCollection.isValid());
137 //TODO only remove the attribute if the move job was successful
138 removeAttribute(Collection::List() << mCollection);
139 CollectionFetchJob *collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
140 q->connect(collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
141 q->connect(collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List)));
142
143 if (mCollection.parentCollection() != list.first()) {
144 CollectionMoveJob *job = new CollectionMoveJob(mCollection, list.first(), q);
145 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
146 }
147 }
148
149}
150
151void TrashRestoreJob::TrashRestoreJobPrivate::itemsReceived(const Akonadi::Item::List &items)
152{
153 Q_Q(TrashRestoreJob);
154 if (items.isEmpty()) {
155 q->setError(Job::Unknown);
156 q->setErrorText(i18n("Invalid items passed"));
157 q->emitResult();
158 return;
159 }
160 mItems = items;
161
162 //Sort by restore collection
163 foreach (const Item &item, mItems) {
164 if (!item.hasAttribute<Akonadi::EntityDeletedAttribute>()) {
165 continue;
166 }
167 //If the restore collection is invalid we restore the item in place, so we don't need to know its restore resource => we can put those cases in the same list
168 restoreCollections[item.attribute<Akonadi::EntityDeletedAttribute>()->restoreCollection()].append(item);
169 }
170
171 foreach (const Collection &col, restoreCollections.keys()) { //krazy:exclude=foreach
172 const Item &first = restoreCollections.value(col).first();
173 //Move the items to the correct collection if available
174 Collection targetCollection = col;
175 const QString restoreResource = first.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
176
177 //Restore in place if no restore collection is set
178 if (!targetCollection.isValid()) {
179 removeAttribute(restoreCollections.value(col));
180 return;
181 }
182
183 //Explicit target overrides the resource
184 if (mTargetCollection.isValid()) {
185 targetCollection = mTargetCollection;
186 }
187
188 //Try to fetch the target resource to see if it is available
189 CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, Akonadi::CollectionFetchJob::Base, q);
190 if (!mTargetCollection.isValid()) { //explicit targets don't have a fallback
191 fetchJob->setProperty("Resource", restoreResource);
192 }
193 fetchJob->setProperty("Items", col.id()); //to find the items in restore collections again
194 q->connect(fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*)));
195 }
196}
197
198void TrashRestoreJob::TrashRestoreJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
199{
200 Q_Q(TrashRestoreJob);
201 if (collections.isEmpty()) {
202 q->setError(Job::Unknown);
203 q->setErrorText(i18n("Invalid collection passed"));
204 q->emitResult();
205 return;
206 }
207 Q_ASSERT(collections.size() == 1);
208 mCollection = collections.first();
209
210 if (!mCollection.hasAttribute<Akonadi::EntityDeletedAttribute>()) {
211 return;
212 }
213
214 const QString restoreResource = mCollection.attribute<Akonadi::EntityDeletedAttribute>()->restoreResource();
215 Collection targetCollection = mCollection.attribute<EntityDeletedAttribute>()->restoreCollection();
216
217 //Restore in place if no restore collection/resource is set
218 if (!targetCollection.isValid()) {
219 removeAttribute(Collection::List() << mCollection);
220 CollectionFetchJob *collectionFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
221 q->connect(collectionFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
222 q->connect(collectionFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(removeAttribute(Akonadi::Collection::List)));
223 return;
224 }
225
226 //Explicit target overrides the resource/configured restore collection
227 if (mTargetCollection.isValid()) {
228 targetCollection = mTargetCollection;
229 }
230
231 //Fetch the target collection to check if it's valid
232 CollectionFetchJob *fetchJob = new CollectionFetchJob(targetCollection, CollectionFetchJob::Base, q);
233 if (!mTargetCollection.isValid()) { //explicit targets don't have a fallback
234 fetchJob->setProperty("Resource", restoreResource);
235 }
236 q->connect(fetchJob, SIGNAL(result(KJob*)), SLOT(targetCollectionFetched(KJob*)));
237}
238
239void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Collection::List &list)
240{
241 Q_Q(TrashRestoreJob);
242 QListIterator<Collection> i(list);
243 while (i.hasNext()) {
244 Collection col = i.next();
245 col.removeAttribute<EntityDeletedAttribute>();
246
247 CollectionModifyJob *job = new CollectionModifyJob(col, q);
248 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
249
250 ItemFetchJob *itemFetchJob = new ItemFetchJob(col, q);
251 itemFetchJob->fetchScope().fetchAttribute<EntityDeletedAttribute> (true);
252 q->connect(itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
253 q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(removeAttribute(Akonadi::Item::List)));
254 }
255}
256
257void TrashRestoreJob::TrashRestoreJobPrivate::removeAttribute(const Akonadi::Item::List &list)
258{
259 Q_Q(TrashRestoreJob);
260 Item::List items = list;
261 QMutableListIterator<Item> i(items);
262 while (i.hasNext()) {
263 Item &item = i.next();
264 item.removeAttribute<EntityDeletedAttribute>();
265 ItemModifyJob *job = new ItemModifyJob(item, q);
266 job->setIgnorePayload(true);
267 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
268 }
269 //For some reason it is not possible to apply this change to multiple items at once
270 //ItemModifyJob *job = new ItemModifyJob(items, q);
271 //q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );
272}
273
274TrashRestoreJob::TrashRestoreJob(const Item &item, QObject *parent)
275 : Job(new TrashRestoreJobPrivate(this), parent)
276{
277 Q_D(TrashRestoreJob);
278 d->mItems << item;
279}
280
281TrashRestoreJob::TrashRestoreJob(const Item::List &items, QObject *parent)
282 : Job(new TrashRestoreJobPrivate(this), parent)
283{
284 Q_D(TrashRestoreJob);
285 d->mItems = items;
286}
287
288TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent)
289 : Job(new TrashRestoreJobPrivate(this), parent)
290{
291 Q_D(TrashRestoreJob);
292 d->mCollection = collection;
293}
294
295TrashRestoreJob::~TrashRestoreJob()
296{
297}
298
299void TrashRestoreJob::setTargetCollection(const Akonadi::Collection collection)
300{
301 Q_D(TrashRestoreJob);
302 d->mTargetCollection = collection;
303}
304
305Item::List TrashRestoreJob::items() const
306{
307 Q_D(const TrashRestoreJob);
308 return d->mItems;
309}
310
311void TrashRestoreJob::doStart()
312{
313 Q_D(TrashRestoreJob);
314
315 //We always have to fetch the entities to ensure that the EntityDeletedAttribute is available
316 if (!d->mItems.isEmpty()) {
317 ItemFetchJob *job = new ItemFetchJob(d->mItems, this);
318 job->fetchScope().setCacheOnly(true);
319 job->fetchScope().fetchAttribute<EntityDeletedAttribute> (true);
320 connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List)));
321 } else if (d->mCollection.isValid()) {
322 CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
323 connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List)));
324 } else {
325 kWarning() << "No valid collection or empty itemlist";
326 setError(Job::Unknown);
327 setErrorText(i18n("No valid collection or empty itemlist"));
328 emitResult();
329 }
330
331}
332
333#include "moc_trashrestorejob.cpp"
334