1/*
2 Copyright (c) 2008 Volker Krause <vkrause@kde.org>
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 "pastehelper_p.h"
21
22#include "collectioncopyjob.h"
23#include "collectionmovejob.h"
24#include "collectionfetchjob.h"
25#include "item.h"
26#include "itemcreatejob.h"
27#include "itemcopyjob.h"
28#include "itemmodifyjob.h"
29#include "itemmovejob.h"
30#include "linkjob.h"
31#include "transactionsequence.h"
32#include "session.h"
33#include "unlinkjob.h"
34
35#include <KDebug>
36#include <KUrl>
37
38#include <QtCore/QByteArray>
39#include <QtCore/QMimeData>
40#include <QtCore/QStringList>
41#include <QtCore/QMutexLocker>
42
43#include <boost/bind.hpp>
44
45using namespace Akonadi;
46
47class PasteHelperJob: public Akonadi::TransactionSequence
48{
49 Q_OBJECT
50
51public:
52 explicit PasteHelperJob(Qt::DropAction action, const Akonadi::Item::List &items,
53 const Akonadi::Collection::List &collections,
54 const Akonadi::Collection &destination,
55 QObject *parent = 0);
56 virtual ~PasteHelperJob();
57
58private Q_SLOTS:
59 void onDragSourceCollectionFetched(KJob *job);
60
61private:
62 void runActions();
63 void runItemsActions();
64 void runCollectionsActions();
65
66private:
67 Qt::DropAction mAction;
68 Akonadi::Item::List mItems;
69 Akonadi::Collection::List mCollections;
70 Akonadi::Collection mDestCollection;
71};
72
73PasteHelperJob::PasteHelperJob(Qt::DropAction action, const Item::List &items,
74 const Collection::List &collections,
75 const Collection &destination,
76 QObject *parent)
77 : TransactionSequence(parent)
78 , mAction(action)
79 , mItems(items)
80 , mCollections(collections)
81 , mDestCollection(destination)
82{
83 //FIXME: The below code disables transactions in otder to avoid data loss due to nested
84 //transactions (copy and colcopy in the server doesn't see the items retrieved into the cache and copies empty payloads).
85 //Remove once this is fixed properly, see the other FIXME comments.
86 setProperty("transactionsDisabled", true);
87
88 Collection dragSourceCollection;
89 if (!items.isEmpty() && items.first().parentCollection().isValid()) {
90 // Check if all items have the same parent collection ID
91 const Collection parent = items.first().parentCollection();
92 if (std::find_if(items.constBegin(), items.constEnd(),
93 boost::bind(&Entity::operator!=, boost::bind(static_cast<Collection (Item::*)() const>(&Item::parentCollection), _1), parent))
94 == items.constEnd())
95 {
96 dragSourceCollection = parent;
97 }
98 }
99
100 kDebug() << items.first().parentCollection().id() << dragSourceCollection.id();
101
102 if (dragSourceCollection.isValid()) {
103 // Disable autocommitting, because starting a Link/Unlink/Copy/Move job
104 // after the transaction has ended leaves the job hanging
105 setAutomaticCommittingEnabled(false);
106
107 CollectionFetchJob *fetch = new CollectionFetchJob(dragSourceCollection,
108 CollectionFetchJob::Base,
109 this);
110 QObject::connect(fetch, SIGNAL(finished(KJob*)),
111 this, SLOT(onDragSourceCollectionFetched(KJob*)));
112 } else {
113 runActions();
114 }
115}
116
117PasteHelperJob::~PasteHelperJob()
118{
119}
120
121void PasteHelperJob::onDragSourceCollectionFetched(KJob *job)
122{
123 CollectionFetchJob *fetch = qobject_cast<CollectionFetchJob*>(job);
124 kDebug() << fetch->error() << fetch->collections().count();
125 if (fetch->error() || fetch->collections().count() != 1) {
126 runActions();
127 commit();
128 return;
129 }
130
131
132 // If the source collection is virtual, treat copy and move actions differently
133 const Collection sourceCollection = fetch->collections().first();
134 kDebug() << "FROM: " << sourceCollection.id() << sourceCollection.name() << sourceCollection.isVirtual();
135 kDebug() << "DEST: " << mDestCollection.id() << mDestCollection.name() << mDestCollection.isVirtual();
136 kDebug() << "ACTN:" << mAction;
137 if (sourceCollection.isVirtual()) {
138 switch (mAction) {
139 case Qt::CopyAction:
140 if (mDestCollection.isVirtual()) {
141 new LinkJob(mDestCollection, mItems, this);
142 } else {
143 new ItemCopyJob(mItems, mDestCollection, this);
144 }
145 break;
146 case Qt::MoveAction:
147 new UnlinkJob(sourceCollection, mItems, this);
148 if (mDestCollection.isVirtual()) {
149 new LinkJob(mDestCollection, mItems, this);
150 } else {
151 new ItemCopyJob(mItems, mDestCollection, this);
152 }
153 break;
154 case Qt::LinkAction:
155 new LinkJob(mDestCollection, mItems, this);
156 break;
157 default:
158 Q_ASSERT(false);
159 }
160 runCollectionsActions();
161 commit();
162 } else {
163 runActions();
164 }
165
166 commit();
167}
168
169void PasteHelperJob::runActions()
170{
171 runItemsActions();
172 runCollectionsActions();
173}
174
175void PasteHelperJob::runItemsActions()
176{
177 if (mItems.isEmpty()) {
178 return;
179 }
180
181 switch (mAction) {
182 case Qt::CopyAction:
183 new ItemCopyJob(mItems, mDestCollection, this);
184 break;
185 case Qt::MoveAction:
186 new ItemMoveJob(mItems, mDestCollection, this);
187 break;
188 case Qt::LinkAction:
189 new LinkJob(mDestCollection, mItems, this);
190 break;
191 default:
192 Q_ASSERT(false); // WTF?!
193 }
194}
195
196void PasteHelperJob::runCollectionsActions()
197{
198 if (mCollections.isEmpty()) {
199 return;
200 }
201
202 switch (mAction) {
203 case Qt::CopyAction:
204 foreach (const Collection &col, mCollections) { // FIXME: remove once we have a batch job for collections as well
205 new CollectionCopyJob(col, mDestCollection, this);
206 }
207 break;
208 case Qt::MoveAction:
209 foreach (const Collection &col, mCollections) { // FIXME: remove once we have a batch job for collections as well
210 new CollectionMoveJob(col, mDestCollection, this);
211 }
212 break;
213 case Qt::LinkAction:
214 // Not supported for collections
215 break;
216 default:
217 Q_ASSERT(false); // WTF?!
218 }
219}
220
221
222
223bool PasteHelper::canPaste(const QMimeData *mimeData, const Collection &collection)
224{
225 if (!mimeData || !collection.isValid()) {
226 return false;
227 }
228
229 // check that the target collection has the rights to
230 // create the pasted items resp. collections
231 Collection::Rights neededRights = Collection::ReadOnly;
232 if (KUrl::List::canDecode(mimeData)) {
233 const KUrl::List urls = KUrl::List::fromMimeData(mimeData);
234 foreach (const KUrl &url, urls) {
235 if (url.hasQueryItem(QLatin1String("item"))) {
236 neededRights |= Collection::CanCreateItem;
237 } else if (url.hasQueryItem(QLatin1String("collection"))) {
238 neededRights |= Collection::CanCreateCollection;
239 }
240 }
241
242 if ((collection.rights() & neededRights) == 0) {
243 return false;
244 }
245
246 // check that the target collection supports the mime types of the
247 // items/collections that shall be pasted
248 bool supportsMimeTypes = true;
249 foreach (const KUrl &url, urls) {
250 // collections do not provide mimetype information, so ignore this check
251 if (url.hasQueryItem(QLatin1String("collection"))) {
252 continue;
253 }
254
255 const QString mimeType = url.queryItemValue(QLatin1String("type"));
256 if (!collection.contentMimeTypes().contains(mimeType)) {
257 supportsMimeTypes = false;
258 break;
259 }
260 }
261
262 if (!supportsMimeTypes) {
263 return false;
264 }
265
266 return true;
267 }
268
269 return false;
270}
271
272KJob *PasteHelper::paste(const QMimeData *mimeData, const Collection &collection, bool copy, Session *session)
273{
274 if (!canPaste(mimeData, collection)) {
275 return 0;
276 }
277
278 // we try to drop data not coming with the akonadi:// url
279 // find a type the target collection supports
280 foreach (const QString &type, mimeData->formats()) {
281 if (!collection.contentMimeTypes().contains(type)) {
282 continue;
283 }
284
285 QByteArray item = mimeData->data(type);
286 // HACK for some unknown reason the data is sometimes 0-terminated...
287 if (!item.isEmpty() && item.at(item.size() - 1) == 0) {
288 item.resize(item.size() - 1);
289 }
290
291 Item it;
292 it.setMimeType(type);
293 it.setPayloadFromData(item);
294
295 ItemCreateJob *job = new ItemCreateJob(it, collection);
296 return job;
297 }
298
299 if (!KUrl::List::canDecode(mimeData)) {
300 return 0;
301 }
302
303 // data contains an url list
304 return pasteUriList(mimeData, collection, copy ? Qt::CopyAction : Qt::MoveAction, session);
305}
306
307KJob *PasteHelper::pasteUriList(const QMimeData *mimeData, const Collection &destination, Qt::DropAction action, Session *session)
308{
309 if (!KUrl::List::canDecode(mimeData)) {
310 return 0;
311 }
312
313 if (!canPaste(mimeData, destination)) {
314 return 0;
315 }
316
317 const KUrl::List urls = KUrl::List::fromMimeData(mimeData);
318 Collection::List collections;
319 Item::List items;
320 foreach (const KUrl &url, urls) {
321 const Collection collection = Collection::fromUrl(url);
322 if (collection.isValid()) {
323 collections.append(collection);
324 }
325 Item item = Item::fromUrl(url);
326 if (url.hasQueryItem(QLatin1String("parent"))) {
327 item.setParentCollection(Collection(url.queryItem(QLatin1String("parent")).toLongLong()));
328 }
329 if (item.isValid()) {
330 items.append(item);
331 }
332 // TODO: handle non Akonadi URLs?
333 }
334
335
336 PasteHelperJob *job = new PasteHelperJob(action, items,
337 collections, destination,
338 session);
339
340 return job;
341}
342
343#include "pastehelper.moc"
344