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 | |
36 | using namespace Akonadi; |
37 | |
38 | ItemModifyJobPrivate::ItemModifyJobPrivate(ItemModifyJob *parent) |
39 | : JobPrivate(parent) |
40 | , mRevCheck(true) |
41 | , mIgnorePayload(false) |
42 | , mAutomaticConflictHandlingEnabled(true) |
43 | , mSilent(false) |
44 | { |
45 | } |
46 | |
47 | void ItemModifyJobPrivate::setClean() |
48 | { |
49 | mOperations.insert(Dirty); |
50 | } |
51 | |
52 | QByteArray ItemModifyJobPrivate::() |
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 | |
80 | void ItemModifyJobPrivate::conflictResolved() |
81 | { |
82 | Q_Q(ItemModifyJob); |
83 | |
84 | q->setError(KJob::NoError); |
85 | q->setErrorText(QString()); |
86 | q->emitResult(); |
87 | } |
88 | |
89 | void ItemModifyJobPrivate::conflictResolveError(const QString &message) |
90 | { |
91 | Q_Q(ItemModifyJob); |
92 | |
93 | q->setErrorText(q->errorText() + message); |
94 | q->emitResult(); |
95 | } |
96 | |
97 | void 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 | |
105 | QString 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 | |
114 | void ItemModifyJobPrivate::setSilent( bool silent ) |
115 | { |
116 | mSilent = silent; |
117 | } |
118 | |
119 | ItemModifyJob::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 | |
131 | ItemModifyJob::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 | |
149 | ItemModifyJob::~ItemModifyJob() |
150 | { |
151 | } |
152 | |
153 | QByteArray 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 | |
256 | void 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 | |
283 | void 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 | |
373 | void 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 | |
390 | bool ItemModifyJob::ignorePayload() const |
391 | { |
392 | Q_D(const ItemModifyJob); |
393 | |
394 | return d->mIgnorePayload; |
395 | } |
396 | |
397 | void 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 | |
407 | bool ItemModifyJob::updateGid() const |
408 | { |
409 | Q_D(const ItemModifyJob); |
410 | return d->mOperations.contains(ItemModifyJobPrivate::Gid); |
411 | } |
412 | |
413 | void ItemModifyJob::disableRevisionCheck() |
414 | { |
415 | Q_D(ItemModifyJob); |
416 | |
417 | d->mRevCheck = false; |
418 | } |
419 | |
420 | void ItemModifyJob::disableAutomaticConflictHandling() |
421 | { |
422 | Q_D(ItemModifyJob); |
423 | |
424 | d->mAutomaticConflictHandlingEnabled = false; |
425 | } |
426 | |
427 | Item ItemModifyJob::item() const |
428 | { |
429 | Q_D(const ItemModifyJob); |
430 | Q_ASSERT(d->mItems.size() == 1); |
431 | |
432 | return d->mItems.first(); |
433 | } |
434 | |
435 | Item::List ItemModifyJob::items() const |
436 | { |
437 | Q_D(const ItemModifyJob); |
438 | return d->mItems; |
439 | } |
440 | |
441 | #include "moc_itemmodifyjob.cpp" |
442 | |