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 | |
43 | using namespace Akonadi; |
44 | |
45 | class TrashRestoreJob::TrashRestoreJobPrivate : public JobPrivate |
46 | { |
47 | public: |
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 | |
75 | void 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 | |
89 | void 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 | |
151 | void 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 | |
198 | void 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 | |
239 | void 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 | |
257 | void 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 | |
274 | TrashRestoreJob::TrashRestoreJob(const Item &item, QObject *parent) |
275 | : Job(new TrashRestoreJobPrivate(this), parent) |
276 | { |
277 | Q_D(TrashRestoreJob); |
278 | d->mItems << item; |
279 | } |
280 | |
281 | TrashRestoreJob::TrashRestoreJob(const Item::List &items, QObject *parent) |
282 | : Job(new TrashRestoreJobPrivate(this), parent) |
283 | { |
284 | Q_D(TrashRestoreJob); |
285 | d->mItems = items; |
286 | } |
287 | |
288 | TrashRestoreJob::TrashRestoreJob(const Collection &collection, QObject *parent) |
289 | : Job(new TrashRestoreJobPrivate(this), parent) |
290 | { |
291 | Q_D(TrashRestoreJob); |
292 | d->mCollection = collection; |
293 | } |
294 | |
295 | TrashRestoreJob::~TrashRestoreJob() |
296 | { |
297 | } |
298 | |
299 | void TrashRestoreJob::setTargetCollection(const Akonadi::Collection collection) |
300 | { |
301 | Q_D(TrashRestoreJob); |
302 | d->mTargetCollection = collection; |
303 | } |
304 | |
305 | Item::List TrashRestoreJob::items() const |
306 | { |
307 | Q_D(const TrashRestoreJob); |
308 | return d->mItems; |
309 | } |
310 | |
311 | void 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 | |