1/*
2 Copyright (c) 2006 - 2007 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 "itemmodifyjob.h"
21#include "itemmodifyjob_p.h"
22
23#include "changemediator_p.h"
24#include "collection.h"
25#include "conflicthandling/conflicthandler_p.h"
26#include "entity_p.h"
27#include "imapparser_p.h"
28#include "item_p.h"
29#include "itemserializer_p.h"
30#include "job_p.h"
31#include "protocolhelper_p.h"
32#include "gid/gidextractor_p.h"
33
34#include <kdebug.h>
35
36using namespace Akonadi;
37
38ItemModifyJobPrivate::ItemModifyJobPrivate(ItemModifyJob *parent)
39 : JobPrivate(parent)
40 , mRevCheck(true)
41 , mIgnorePayload(false)
42 , mAutomaticConflictHandlingEnabled(true)
43 , mSilent(false)
44{
45}
46
47void ItemModifyJobPrivate::setClean()
48{
49 mOperations.insert(Dirty);
50}
51
52QByteArray ItemModifyJobPrivate::nextPartHeader()
53{
54 QByteArray command;
55 if (!mParts.isEmpty()) {
56 QSetIterator<QByteArray> it(mParts);
57 const QByteArray label = it.next();
58 mParts.remove(label);
59
60 mPendingData.clear();
61 int version = 0;
62 ItemSerializer::serialize(mItems.first(), label, mPendingData, version);
63 command += ' ' + ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartPayload, label, version);
64 if (mPendingData.size() > 0) {
65 command += " {" + QByteArray::number(mPendingData.size()) + "}\n";
66 } else {
67 if (mPendingData.isNull()) {
68 command += " NIL";
69 } else {
70 command += " \"\"";
71 }
72 command += nextPartHeader();
73 }
74 } else {
75 command += ")\n";
76 }
77 return command;
78}
79
80void ItemModifyJobPrivate::conflictResolved()
81{
82 Q_Q(ItemModifyJob);
83
84 q->setError(KJob::NoError);
85 q->setErrorText(QString());
86 q->emitResult();
87}
88
89void ItemModifyJobPrivate::conflictResolveError(const QString &message)
90{
91 Q_Q(ItemModifyJob);
92
93 q->setErrorText(q->errorText() + message);
94 q->emitResult();
95}
96
97void ItemModifyJobPrivate::doUpdateItemRevision(Akonadi::Item::Id itemId, int oldRevision, int newRevision)
98{
99 Item::List::iterator it = std::find_if(mItems.begin(), mItems.end(), boost::bind(&Item::id, _1) == itemId);
100 if (it != mItems.end() && (*it).revision() == oldRevision) {
101 (*it).setRevision(newRevision);
102 }
103}
104
105QString ItemModifyJobPrivate::jobDebuggingString() const
106{
107 try {
108 return QString::fromUtf8(fullCommand());
109 } catch (const Exception &e) {
110 return QString::fromUtf8(e.what());
111 }
112}
113
114void ItemModifyJobPrivate::setSilent( bool silent )
115{
116 mSilent = silent;
117}
118
119ItemModifyJob::ItemModifyJob(const Item &item, QObject *parent)
120 : Job(new ItemModifyJobPrivate(this), parent)
121{
122 Q_D(ItemModifyJob);
123
124 d->mItems.append(item);
125 d->mParts = item.loadedPayloadParts();
126
127 d->mOperations.insert(ItemModifyJobPrivate::RemoteId);
128 d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision);
129}
130
131ItemModifyJob::ItemModifyJob(const Akonadi::Item::List &items, QObject *parent)
132 : Job(new ItemModifyJobPrivate(this), parent)
133{
134 Q_ASSERT(!items.isEmpty());
135 Q_D(ItemModifyJob);
136 d->mItems = items;
137
138 // same as single item ctor
139 if (d->mItems.size() == 1) {
140 d->mParts = items.first().loadedPayloadParts();
141 d->mOperations.insert(ItemModifyJobPrivate::RemoteId);
142 d->mOperations.insert(ItemModifyJobPrivate::RemoteRevision);
143 } else {
144 d->mIgnorePayload = true;
145 d->mRevCheck = false;
146 }
147}
148
149ItemModifyJob::~ItemModifyJob()
150{
151}
152
153QByteArray ItemModifyJobPrivate::fullCommand() const
154{
155 const Akonadi::Item item = mItems.first();
156 QList<QByteArray> changes;
157 foreach (int op, mOperations) {
158 switch (op) {
159 case ItemModifyJobPrivate::RemoteId:
160 if (!item.remoteId().isNull()) {
161 changes << "REMOTEID";
162 changes << ImapParser::quote(item.remoteId().toUtf8());
163 }
164 break;
165 case ItemModifyJobPrivate::Gid: {
166 const QString gid = GidExtractor::getGid(item);
167 if (!gid.isNull()) {
168 changes << "GID";
169 changes << ImapParser::quote(gid.toUtf8());
170 }
171 break;
172 }
173 case ItemModifyJobPrivate::RemoteRevision:
174 if (!item.remoteRevision().isNull()) {
175 changes << "REMOTEREVISION";
176 changes << ImapParser::quote(item.remoteRevision().toUtf8());
177 }
178 break;
179 case ItemModifyJobPrivate::Dirty:
180 changes << "DIRTY";
181 changes << "false";
182 break;
183 }
184 }
185
186 if (item.d_func()->mClearPayload) {
187 changes << "INVALIDATECACHE";
188 }
189 if ( mSilent ) {
190 changes << "SILENT";
191 }
192
193 if (item.d_func()->mFlagsOverwritten) {
194 changes << "FLAGS";
195 changes << '(' + ImapParser::join(item.flags(), " ") + ')';
196 } else {
197 if (!item.d_func()->mAddedFlags.isEmpty()) {
198 changes << "+FLAGS";
199 changes << '(' + ImapParser::join(item.d_func()->mAddedFlags, " ") + ')';
200 }
201 if (!item.d_func()->mDeletedFlags.isEmpty()) {
202 changes << "-FLAGS";
203 changes << '(' + ImapParser::join(item.d_func()->mDeletedFlags, " ") + ')';
204 }
205 }
206
207 if (item.d_func()->mTagsOverwritten) {
208 changes << "TAGS";
209 changes << ' ' + ProtocolHelper::tagSetToImapSequenceSet(item.tags());
210 } else {
211 if (!item.d_func()->mAddedTags.isEmpty()) {
212 changes << "+TAGS";
213 changes << ' ' + ProtocolHelper::tagSetToImapSequenceSet(item.d_func()->mAddedTags);
214 }
215 if (!item.d_func()->mDeletedTags.isEmpty()) {
216 changes << "-TAGS";
217 changes << ' ' + ProtocolHelper::tagSetToImapSequenceSet(item.d_func()->mDeletedTags);
218 }
219 }
220
221 if (!item.d_func()->mDeletedAttributes.isEmpty()) {
222 changes << "-PARTS";
223 QList<QByteArray> attrs;
224 foreach (const QByteArray &attr, item.d_func()->mDeletedAttributes) {
225 attrs << ProtocolHelper::encodePartIdentifier(ProtocolHelper::PartAttribute, attr);
226 }
227 changes << '(' + ImapParser::join(attrs, " ") + ')';
228 }
229
230 // nothing to do
231 if (changes.isEmpty() && mParts.isEmpty() && item.attributes().isEmpty()) {
232 return QByteArray();
233 }
234
235 QByteArray command;
236 command += ProtocolHelper::entitySetToByteArray(mItems, "STORE"); // can throw an exception
237 command += ' ';
238 if (!mRevCheck || item.revision() < 0) {
239 command += "NOREV ";
240 } else {
241 command += "REV " + QByteArray::number(item.revision()) + ' ';
242 }
243
244 if (item.d_func()->mSizeChanged) {
245 command += "SIZE " + QByteArray::number(item.size());
246 }
247
248 command += " (" + ImapParser::join(changes, " ");
249 const QByteArray attrs = ProtocolHelper::attributesToByteArray(item, true);
250 if (!attrs.isEmpty()) {
251 command += ' ' + attrs;
252 }
253 return command;
254}
255
256void ItemModifyJob::doStart()
257{
258 Q_D(ItemModifyJob);
259
260 QByteArray command;
261 try {
262 command = d->fullCommand();
263 } catch (const Exception &e) {
264 setError(Job::Unknown);
265 setErrorText(QString::fromUtf8(e.what()));
266 emitResult();
267 return;
268 }
269 if (command.isEmpty()) {
270 emitResult();
271 return;
272 }
273
274 d->mTag = d->newTag();
275 command.prepend(d->mTag);
276
277 command += d->nextPartHeader();
278
279 d->writeData(command);
280 d->newTag(); // hack to circumvent automatic response handling
281}
282
283void ItemModifyJob::doHandleResponse(const QByteArray &_tag, const QByteArray &data)
284{
285 Q_D(ItemModifyJob);
286
287 if (_tag == "+") { // ready for literal data
288 if (data.startsWith("STREAM")) {
289 QByteArray error;
290 if (!ProtocolHelper::streamPayloadToFile(data, d->mPendingData, error)) {
291 d->writeData("* NO " + error);
292 return;
293 }
294 } else {
295 d->writeData(d->mPendingData);
296 }
297 d->writeData(d->nextPartHeader());
298 return;
299 }
300
301 if (_tag == d->mTag) {
302 if (data.startsWith("OK")) { //krazy:exclude=strings
303 QDateTime modificationDateTime;
304 int dateTimePos = data.indexOf("DATETIME");
305 if (dateTimePos != -1) {
306 int resultPos = ImapParser::parseDateTime(data, modificationDateTime, dateTimePos + 8);
307 if (resultPos == (dateTimePos + 8)) {
308 kDebug() << "Invalid DATETIME response to STORE command: " << _tag << data;
309 }
310 }
311
312 Item &item = d->mItems.first();
313 item.setModificationTime(modificationDateTime);
314 item.d_ptr->resetChangeLog();
315 } else {
316 setError(Unknown);
317 setErrorText(QString::fromUtf8(data));
318
319 if (data.contains("[LLCONFLICT]")) {
320 if (d->mAutomaticConflictHandlingEnabled) {
321 ConflictHandler *handler = new ConflictHandler(ConflictHandler::LocalLocalConflict, this);
322 handler->setConflictingItems(d->mItems.first(), d->mItems.first());
323 connect(handler, SIGNAL(conflictResolved()), SLOT(conflictResolved()));
324 connect(handler, SIGNAL(error(QString)), SLOT(conflictResolveError(QString)));
325
326 QMetaObject::invokeMethod(handler, "start", Qt::QueuedConnection);
327 return;
328 }
329 }
330 }
331
332 foreach (const Item &item, d->mItems) {
333 ChangeMediator::invalidateItem(item);
334 }
335
336 emitResult();
337 return;
338 }
339
340 if (_tag == "*") {
341 Akonadi::Item::Id id;
342 ImapParser::parseNumber(data, id);
343 int pos = data.indexOf('(');
344 if (pos <= 0 || id <= 0) {
345 kDebug() << "Ignoring strange response: " << _tag << data;
346 return;
347 }
348 Item::List::iterator it = std::find_if(d->mItems.begin(), d->mItems.end(), boost::bind(&Item::id, _1) == id);
349 if (it == d->mItems.end()) {
350 kDebug() << "Received STORE response for an item we did not modify: " << _tag << data;
351 return;
352 }
353 QList<QByteArray> attrs;
354 ImapParser::parseParenthesizedList(data, attrs, pos);
355 for (int i = 0; i < attrs.size() - 1; i += 2) {
356 const QByteArray key = attrs.at(i);
357 if (key == "REV") {
358 const int newRev = attrs.at(i + 1).toInt();
359 const int oldRev = (*it).revision();
360 if (newRev < oldRev || newRev < 0) {
361 continue;
362 }
363 d->itemRevisionChanged((*it).id(), oldRev, newRev);
364 (*it).setRevision(newRev);
365 }
366 }
367 return;
368 }
369
370 kDebug() << "Unhandled response: " << _tag << data;
371}
372
373void ItemModifyJob::setIgnorePayload(bool ignore)
374{
375 Q_D(ItemModifyJob);
376
377 if (d->mIgnorePayload == ignore) {
378 return;
379 }
380
381 d->mIgnorePayload = ignore;
382 if (d->mIgnorePayload) {
383 d->mParts = QSet<QByteArray>();
384 } else {
385 Q_ASSERT(!d->mItems.first().mimeType().isEmpty());
386 d->mParts = d->mItems.first().loadedPayloadParts();
387 }
388}
389
390bool ItemModifyJob::ignorePayload() const
391{
392 Q_D(const ItemModifyJob);
393
394 return d->mIgnorePayload;
395}
396
397void ItemModifyJob::setUpdateGid(bool update)
398{
399 Q_D(ItemModifyJob);
400 if (update && !updateGid()) {
401 d->mOperations.insert(ItemModifyJobPrivate::Gid);
402 } else {
403 d->mOperations.remove(ItemModifyJobPrivate::Gid);
404 }
405}
406
407bool ItemModifyJob::updateGid() const
408{
409 Q_D(const ItemModifyJob);
410 return d->mOperations.contains(ItemModifyJobPrivate::Gid);
411}
412
413void ItemModifyJob::disableRevisionCheck()
414{
415 Q_D(ItemModifyJob);
416
417 d->mRevCheck = false;
418}
419
420void ItemModifyJob::disableAutomaticConflictHandling()
421{
422 Q_D(ItemModifyJob);
423
424 d->mAutomaticConflictHandlingEnabled = false;
425}
426
427Item ItemModifyJob::item() const
428{
429 Q_D(const ItemModifyJob);
430 Q_ASSERT(d->mItems.size() == 1);
431
432 return d->mItems.first();
433}
434
435Item::List ItemModifyJob::items() const
436{
437 Q_D(const ItemModifyJob);
438 return d->mItems;
439}
440
441#include "moc_itemmodifyjob.cpp"
442