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 "trashjob.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/itemfetchscope.h>
37#include <akonadi/collectionfetchscope.h>
38#include <akonadi/itemfetchjob.h>
39#include <akonadi/collectionfetchjob.h>
40
41#include <QHash>
42
43using namespace Akonadi;
44
45class TrashJob::TrashJobPrivate : public JobPrivate
46{
47public:
48 TrashJobPrivate(TrashJob *parent)
49 : JobPrivate(parent)
50 , mKeepTrashInCollection(false)
51 , mSetRestoreCollection(false)
52 , mDeleteIfInTrash(false)
53 {
54 }
55//4.
56 void selectResult(KJob *job);
57//3.
58 //Helper functions to recursivly set the attribute on deleted collections
59 void setAttribute(const Akonadi::Collection::List &);
60 void setAttribute(const Akonadi::Item::List &);
61 //Set attributes after ensuring that move job was successful
62 void setAttribute(KJob *job);
63
64//2.
65 //called after parent of the trashed item was fetched (needed to see in which resource the item is in)
66 void parentCollectionReceived(const Akonadi::Collection::List &);
67
68//1.
69 //called after initial fetch of trashed items
70 void itemsReceived(const Akonadi::Item::List &);
71 //called after initial fetch of trashed collection
72 void collectionsReceived(const Akonadi::Collection::List &);
73
74 Q_DECLARE_PUBLIC(TrashJob)
75
76 Item::List mItems;
77 Collection mCollection;
78 Collection mRestoreCollection;
79 Collection mTrashCollection;
80 bool mKeepTrashInCollection;
81 bool mSetRestoreCollection; //only set restore collection when moved to trash collection (not in place)
82 bool mDeleteIfInTrash;
83 QHash<Collection, Item::List> mCollectionItems; //list of trashed items sorted according to parent collection
84 QHash<Entity::Id, Collection> mParentCollections; //fetched parent collcetion of items (containing the resource name)
85
86};
87
88void TrashJob::TrashJobPrivate::selectResult(KJob *job)
89{
90 Q_Q(TrashJob);
91 if (job->error()) {
92 kWarning() << job->objectName();
93 kWarning() << job->errorString();
94 return; // KCompositeJob takes care of errors
95 }
96
97 if (!q->hasSubjobs() || (q->subjobs().contains(static_cast<KJob *>(q->sender())) && q->subjobs().size() == 1)) {
98 q->emitResult();
99 }
100}
101
102void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Collection::List &list)
103{
104 Q_Q(TrashJob);
105 QListIterator<Collection> i(list);
106 while (i.hasNext()) {
107 const Collection &col = i.next();
108 EntityDeletedAttribute *eda = new EntityDeletedAttribute();
109 if (mSetRestoreCollection) {
110 Q_ASSERT(mRestoreCollection.isValid());
111 eda->setRestoreCollection(mRestoreCollection);
112 }
113
114 Collection modCol(col.id()); //really only modify attribute (forget old remote ids, etc.), otherwise we have an error because of the move
115 modCol.addAttribute(eda);
116
117 CollectionModifyJob *job = new CollectionModifyJob(modCol, q);
118 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
119
120 ItemFetchJob *itemFetchJob = new ItemFetchJob(col, q);
121 //TODO not sure if it is guaranteed that itemsReceived is always before result (otherwise the result is emitted before the attributes are set)
122 q->connect(itemFetchJob, SIGNAL(itemsReceived(Akonadi::Item::List)), SLOT(setAttribute(Akonadi::Item::List)));
123 q->connect(itemFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
124 }
125}
126
127void TrashJob::TrashJobPrivate::setAttribute(const Akonadi::Item::List &list)
128{
129 Q_Q(TrashJob);
130 Item::List items = list;
131 QMutableListIterator<Item> i(items);
132 while (i.hasNext()) {
133 const Item &item = i.next();
134 EntityDeletedAttribute *eda = new EntityDeletedAttribute();
135 if (mSetRestoreCollection) {
136 //When deleting a collection, we want to restore the deleted collection's items restored to the deleted collection's parent, not the items parent
137 if (mRestoreCollection.isValid()) {
138 eda->setRestoreCollection(mRestoreCollection);
139 } else {
140 Q_ASSERT(mParentCollections.contains(item.parentCollection().id()));
141 eda->setRestoreCollection(mParentCollections.value(item.parentCollection().id()));
142 }
143 }
144
145 Item modItem(item.id()); //really only modify attribute (forget old remote ids, etc.)
146 modItem.addAttribute(eda);
147 ItemModifyJob *job = new ItemModifyJob(modItem, q);
148 job->setIgnorePayload(true);
149 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
150 }
151
152 //For some reason it is not possible to apply this change to multiple items at once
153 /*ItemModifyJob *job = new ItemModifyJob(items, q);
154 q->connect( job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)) );*/
155}
156
157void TrashJob::TrashJobPrivate::setAttribute(KJob *job)
158{
159 Q_Q(TrashJob);
160 if (job->error()) {
161 kWarning() << job->objectName();
162 kWarning() << job->errorString();
163 q->setError(Job::Unknown);
164 q->setErrorText(i18n("Move to trash collection failed, aborting trash operation"));
165 return;
166 }
167
168 //For Items
169 const QVariant var = job->property("MovedItems");
170 if (var.isValid()) {
171 int id = var.toInt();
172 Q_ASSERT(id >= 0);
173 setAttribute(mCollectionItems.value(Collection(id)));
174 return;
175 }
176
177 //For a collection
178 Q_ASSERT(mCollection.isValid());
179 setAttribute(Collection::List() << mCollection);
180 //Set the attribute on all subcollections and items
181 CollectionFetchJob *colFetchJob = new CollectionFetchJob(mCollection, CollectionFetchJob::Recursive, q);
182 q->connect(colFetchJob, SIGNAL(collectionsReceived(Akonadi::Collection::List)), SLOT(setAttribute(Akonadi::Collection::List)));
183 q->connect(colFetchJob, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
184}
185
186void TrashJob::TrashJobPrivate::parentCollectionReceived(const Akonadi::Collection::List &collections)
187{
188 Q_Q(TrashJob);
189 Q_ASSERT(collections.size() == 1);
190 const Collection &parentCollection = collections.first();
191
192 //store attribute
193 Q_ASSERT(!parentCollection.resource().isEmpty());
194 Collection trashCollection = mTrashCollection;
195 if (!mTrashCollection.isValid()) {
196 trashCollection = TrashSettings::getTrashCollection(parentCollection.resource());
197 }
198 if (!mKeepTrashInCollection && trashCollection.isValid()) { //Only set the restore collection if the item is moved to trash
199 mSetRestoreCollection = true;
200 }
201
202 mParentCollections.insert(parentCollection.id(), parentCollection);
203
204 if (trashCollection.isValid()) { //Move the items to the correct collection if available
205 ItemMoveJob *job = new ItemMoveJob(mCollectionItems.value(parentCollection), trashCollection, q);
206 job->setProperty("MovedItems", parentCollection.id());
207 q->connect(job, SIGNAL(result(KJob*)), SLOT(setAttribute(KJob*))); //Wait until the move finished to set the attirbute
208 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
209 } else {
210 setAttribute(mCollectionItems.value(parentCollection));
211 }
212}
213
214void TrashJob::TrashJobPrivate::itemsReceived(const Akonadi::Item::List &items)
215{
216 Q_Q(TrashJob);
217 if (items.isEmpty()) {
218 q->setError(Job::Unknown);
219 q->setErrorText(i18n("Invalid items passed"));
220 q->emitResult();
221 return;
222 }
223
224 Item::List toDelete;
225
226 QListIterator<Item> i(items);
227 while (i.hasNext()) {
228 const Item &item = i.next();
229 if (item.hasAttribute<EntityDeletedAttribute>()) {
230 toDelete.append(item);
231 continue;
232 }
233 Q_ASSERT(item.parentCollection().isValid());
234 mCollectionItems[item.parentCollection()].append(item); //Sort by parent col ( = restore collection)
235 }
236
237 foreach (const Collection &col, mCollectionItems.keys()) { //krazy:exclude=foreach
238 CollectionFetchJob *job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base, q);
239 q->connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)),
240 SLOT(parentCollectionReceived(Akonadi::Collection::List)));
241 }
242
243 if (mDeleteIfInTrash && !toDelete.isEmpty()) {
244 ItemDeleteJob *job = new ItemDeleteJob(toDelete, q);
245 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
246 } else if (mCollectionItems.isEmpty()) { //No job started, so we abort the job
247 kWarning() << "Nothing to do";
248 q->emitResult();
249 }
250
251}
252
253void TrashJob::TrashJobPrivate::collectionsReceived(const Akonadi::Collection::List &collections)
254{
255 Q_Q(TrashJob);
256 if (collections.isEmpty()) {
257 q->setError(Job::Unknown);
258 q->setErrorText(i18n("Invalid collection passed"));
259 q->emitResult();
260 return;
261 }
262 Q_ASSERT(collections.size() == 1);
263 mCollection = collections.first();
264
265 if (mCollection.hasAttribute<EntityDeletedAttribute>()) { //marked as deleted
266 if (mDeleteIfInTrash) {
267 CollectionDeleteJob *job = new CollectionDeleteJob(mCollection, q);
268 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
269 } else {
270 kWarning() << "Nothing to do";
271 q->emitResult();
272 }
273 return;
274 }
275
276 Collection trashCollection = mTrashCollection;
277 if (!mTrashCollection.isValid()) {
278 trashCollection = TrashSettings::getTrashCollection(mCollection.resource());
279 }
280 if (!mKeepTrashInCollection && trashCollection.isValid()) { //only set the restore collection if the item is moved to trash
281 mSetRestoreCollection = true;
282 Q_ASSERT(mCollection.parentCollection().isValid());
283 mRestoreCollection = mCollection.parentCollection();
284 mRestoreCollection.setResource(mCollection.resource()); //The parent collection doesn't contain the resource, so we have to set it manually
285 }
286
287 if (trashCollection.isValid()) {
288 CollectionMoveJob *job = new CollectionMoveJob(mCollection, trashCollection, q);
289 q->connect(job, SIGNAL(result(KJob*)), SLOT(setAttribute(KJob*)));
290 q->connect(job, SIGNAL(result(KJob*)), SLOT(selectResult(KJob*)));
291 } else {
292 setAttribute(Collection::List() << mCollection);
293 }
294
295}
296
297TrashJob::TrashJob(const Item &item, QObject *parent)
298 : Job(new TrashJobPrivate(this), parent)
299{
300 Q_D(TrashJob);
301 d->mItems << item;
302}
303
304TrashJob::TrashJob(const Item::List &items, QObject *parent)
305 : Job(new TrashJobPrivate(this), parent)
306{
307 Q_D(TrashJob);
308 d->mItems = items;
309}
310
311TrashJob::TrashJob(const Collection &collection, QObject *parent)
312 : Job(new TrashJobPrivate(this), parent)
313{
314 Q_D(TrashJob);
315 d->mCollection = collection;
316}
317
318TrashJob::~TrashJob()
319{
320}
321
322Item::List TrashJob::items() const
323{
324 Q_D(const TrashJob);
325 return d->mItems;
326}
327
328void TrashJob::setTrashCollection(const Akonadi::Collection &collection)
329{
330 Q_D(TrashJob);
331 d->mTrashCollection = collection;
332}
333
334void TrashJob::keepTrashInCollection(bool enable)
335{
336 Q_D(TrashJob);
337 d->mKeepTrashInCollection = enable;
338}
339
340void TrashJob::deleteIfInTrash(bool enable)
341{
342 Q_D(TrashJob);
343 d->mDeleteIfInTrash = enable;
344}
345
346void TrashJob::doStart()
347{
348 Q_D(TrashJob);
349
350 //Fetch items first to ensure that the EntityDeletedAttribute is available
351 if (!d->mItems.isEmpty()) {
352 ItemFetchJob *job = new ItemFetchJob(d->mItems, this);
353 job->fetchScope().setAncestorRetrieval(Akonadi::ItemFetchScope::Parent); //so we have access to the resource
354 //job->fetchScope().setCacheOnly(true);
355 job->fetchScope().fetchAttribute<EntityDeletedAttribute>(true);
356 connect(job, SIGNAL(itemsReceived(Akonadi::Item::List)), this, SLOT(itemsReceived(Akonadi::Item::List)));
357
358 } else if (d->mCollection.isValid()) {
359 CollectionFetchJob *job = new CollectionFetchJob(d->mCollection, CollectionFetchJob::Base, this);
360 job->fetchScope().setAncestorRetrieval(Akonadi::CollectionFetchScope::Parent);
361 connect(job, SIGNAL(collectionsReceived(Akonadi::Collection::List)), this, SLOT(collectionsReceived(Akonadi::Collection::List)));
362
363 } else {
364 kWarning() << "No valid collection or empty itemlist";
365 setError(Job::Unknown);
366 setErrorText(i18n("No valid collection or empty itemlist"));
367 emitResult();
368 }
369}
370
371#include "moc_trashjob.cpp"
372