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 | |
43 | using namespace Akonadi; |
44 | |
45 | class TrashJob::TrashJobPrivate : public JobPrivate |
46 | { |
47 | public: |
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 | |
88 | void 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 | |
102 | void 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 | |
127 | void 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 | |
157 | void 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 | |
186 | void 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 | |
214 | void 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 | |
253 | void 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 | |
297 | TrashJob::TrashJob(const Item &item, QObject *parent) |
298 | : Job(new TrashJobPrivate(this), parent) |
299 | { |
300 | Q_D(TrashJob); |
301 | d->mItems << item; |
302 | } |
303 | |
304 | TrashJob::TrashJob(const Item::List &items, QObject *parent) |
305 | : Job(new TrashJobPrivate(this), parent) |
306 | { |
307 | Q_D(TrashJob); |
308 | d->mItems = items; |
309 | } |
310 | |
311 | TrashJob::TrashJob(const Collection &collection, QObject *parent) |
312 | : Job(new TrashJobPrivate(this), parent) |
313 | { |
314 | Q_D(TrashJob); |
315 | d->mCollection = collection; |
316 | } |
317 | |
318 | TrashJob::~TrashJob() |
319 | { |
320 | } |
321 | |
322 | Item::List TrashJob::items() const |
323 | { |
324 | Q_D(const TrashJob); |
325 | return d->mItems; |
326 | } |
327 | |
328 | void TrashJob::setTrashCollection(const Akonadi::Collection &collection) |
329 | { |
330 | Q_D(TrashJob); |
331 | d->mTrashCollection = collection; |
332 | } |
333 | |
334 | void TrashJob::keepTrashInCollection(bool enable) |
335 | { |
336 | Q_D(TrashJob); |
337 | d->mKeepTrashInCollection = enable; |
338 | } |
339 | |
340 | void TrashJob::deleteIfInTrash(bool enable) |
341 | { |
342 | Q_D(TrashJob); |
343 | d->mDeleteIfInTrash = enable; |
344 | } |
345 | |
346 | void 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 | |