1/*
2 Copyright (c) 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 "changerecorder_p.h"
21
22#include <QtCore/QFile>
23#include <QtCore/QDir>
24#include <QtCore/QSettings>
25#include <QtCore/QFileInfo>
26
27using namespace Akonadi;
28
29ChangeRecorderPrivate::ChangeRecorderPrivate(ChangeNotificationDependenciesFactory *dependenciesFactory_,
30 ChangeRecorder *parent)
31 : MonitorPrivate(dependenciesFactory_, parent)
32 , settings(0)
33 , enableChangeRecording(true)
34 , m_lastKnownNotificationsCount(0)
35 , m_startOffset(0)
36 , m_needFullSave(true)
37{
38}
39
40int ChangeRecorderPrivate::pipelineSize() const
41{
42 if (enableChangeRecording) {
43 return 0; // we fill the pipeline ourselves when using change recording
44 }
45 return MonitorPrivate::pipelineSize();
46}
47
48void ChangeRecorderPrivate::slotNotify(const Akonadi::NotificationMessageV3::List &msgs)
49{
50 Q_Q(ChangeRecorder);
51 const int oldChanges = pendingNotifications.size();
52 // with change recording disabled this will automatically take care of dispatching notification messages and saving
53 MonitorPrivate::slotNotify(msgs);
54 if (enableChangeRecording && pendingNotifications.size() != oldChanges) {
55 emit q->changesAdded();
56 }
57}
58
59// The QSettings object isn't actually used anymore, except for migrating old data
60// and it gives us the base of the filename to use. This is all historical.
61QString ChangeRecorderPrivate::notificationsFileName() const
62{
63 return settings->fileName() + QLatin1String("_changes.dat");
64}
65
66void ChangeRecorderPrivate::loadNotifications()
67{
68 pendingNotifications.clear();
69 Q_ASSERT(pipeline.isEmpty());
70 pipeline.clear();
71
72 const QString changesFileName = notificationsFileName();
73
74 /**
75 * In an older version we recorded changes inside the settings object, however
76 * for performance reasons we changed that to store them in a separated file.
77 * If this file doesn't exists, it means we run the new version the first time,
78 * so we have to read in the legacy list of changes first.
79 */
80 if (!QFile::exists(changesFileName)) {
81 QStringList list;
82 settings->beginGroup(QLatin1String("ChangeRecorder"));
83 const int size = settings->beginReadArray(QLatin1String("change"));
84
85 for (int i = 0; i < size; ++i) {
86 settings->setArrayIndex(i);
87 NotificationMessageV3 msg;
88 msg.setSessionId(settings->value(QLatin1String("sessionId")).toByteArray());
89 msg.setType((NotificationMessageV2::Type)settings->value(QLatin1String("type")).toInt());
90 msg.setOperation((NotificationMessageV2::Operation)settings->value(QLatin1String("op")).toInt());
91 msg.addEntity(settings->value(QLatin1String("uid")).toLongLong(),
92 settings->value(QLatin1String("rid")).toString(),
93 QString(),
94 settings->value(QLatin1String("mimeType")).toString());
95 msg.setResource(settings->value(QLatin1String("resource")).toByteArray());
96 msg.setParentCollection(settings->value(QLatin1String("parentCol")).toLongLong());
97 msg.setParentDestCollection(settings->value(QLatin1String("parentDestCol")).toLongLong());
98 list = settings->value(QLatin1String("itemParts")).toStringList();
99 QSet<QByteArray> itemParts;
100 Q_FOREACH (const QString &entry, list) {
101 itemParts.insert(entry.toLatin1());
102 }
103 msg.setItemParts(itemParts);
104 pendingNotifications << msg;
105 }
106
107 settings->endArray();
108
109 // save notifications to the new file...
110 saveNotifications();
111
112 // ...delete the legacy list...
113 settings->remove(QString());
114 settings->endGroup();
115
116 // ...and continue as usually
117 }
118
119 QFile file(changesFileName);
120 if (file.open(QIODevice::ReadOnly)) {
121 m_needFullSave = false;
122 pendingNotifications = loadFrom(&file);
123 } else {
124 m_needFullSave = true;
125 }
126 notificationsLoaded();
127}
128
129static const quint64 s_currentVersion = Q_UINT64_C(0x000300000000);
130static const quint64 s_versionMask = Q_UINT64_C(0xFFFF00000000);
131static const quint64 s_sizeMask = Q_UINT64_C(0x0000FFFFFFFF);
132
133QQueue<NotificationMessageV3> ChangeRecorderPrivate::loadFrom(QIODevice *device)
134{
135 QDataStream stream(device);
136 stream.setVersion(QDataStream::Qt_4_6);
137
138 QByteArray sessionId, resource, destinationResource;
139 int type, operation, entityCnt;
140 quint64 uid, parentCollection, parentDestCollection;
141 QString remoteId, mimeType, remoteRevision;
142 QSet<QByteArray> itemParts, addedFlags, removedFlags;
143 QSet<qint64> addedTags, removedTags;
144
145 QQueue<NotificationMessageV3> list;
146
147 quint64 sizeAndVersion;
148 stream >> sizeAndVersion;
149
150 const quint64 size = sizeAndVersion & s_sizeMask;
151 const quint64 version = (sizeAndVersion & s_versionMask) >> 32;
152
153 quint64 startOffset = 0;
154 if (version >= 1) {
155 stream >> startOffset;
156 }
157
158 // If we skip the first N items, then we'll need to rewrite the file on saving.
159 // Also, if the file is old, it needs to be rewritten.
160 m_needFullSave = startOffset > 0 || version == 0;
161
162 for (quint64 i = 0; i < size && !stream.atEnd(); ++i) {
163 NotificationMessageV2 msg;
164
165 if (version == 1) {
166 stream >> sessionId;
167 stream >> type;
168 stream >> operation;
169 stream >> uid;
170 stream >> remoteId;
171 stream >> resource;
172 stream >> parentCollection;
173 stream >> parentDestCollection;
174 stream >> mimeType;
175 stream >> itemParts;
176
177 if (i < startOffset) {
178 continue;
179 }
180
181 msg.setSessionId(sessionId);
182 msg.setType(static_cast<NotificationMessageV2::Type>(type));
183 msg.setOperation(static_cast<NotificationMessageV2::Operation>(operation));
184 msg.addEntity(uid, remoteId, QString(), mimeType);
185 msg.setResource(resource);
186 msg.setParentCollection(parentCollection);
187 msg.setParentDestCollection(parentDestCollection);
188 msg.setItemParts(itemParts);
189
190 } else if (version == 2 || version == 3) {
191
192 NotificationMessageV3 msg;
193
194 stream >> sessionId;
195 stream >> type;
196 stream >> operation;
197 stream >> entityCnt;
198 for (int j = 0; j < entityCnt; ++j) {
199 stream >> uid;
200 stream >> remoteId;
201 stream >> remoteRevision;
202 stream >> mimeType;
203 msg.addEntity(uid, remoteId, remoteRevision, mimeType);
204 }
205 stream >> resource;
206 stream >> destinationResource;
207 stream >> parentCollection;
208 stream >> parentDestCollection;
209 stream >> itemParts;
210 stream >> addedFlags;
211 stream >> removedFlags;
212 if (version == 3) {
213 stream >> addedTags;
214 stream >> removedTags;
215 }
216
217 if (i < startOffset) {
218 continue;
219 }
220
221 msg.setSessionId(sessionId);
222 msg.setType(static_cast<NotificationMessageV2::Type>(type));
223 msg.setOperation(static_cast<NotificationMessageV2::Operation>(operation));
224 msg.setResource(resource);
225 msg.setDestinationResource(destinationResource);
226 msg.setParentCollection(parentCollection);
227 msg.setParentDestCollection(parentDestCollection);
228 msg.setItemParts(itemParts);
229 msg.setAddedFlags(addedFlags);
230 msg.setRemovedFlags(removedFlags);
231 msg.setAddedTags(addedTags);
232 msg.setRemovedTags(removedTags);
233
234 list << msg;
235 }
236 }
237
238 return list;
239}
240
241QString ChangeRecorderPrivate::dumpNotificationListToString() const
242{
243 if (!settings) {
244 return QString::fromLatin1("No settings set in ChangeRecorder yet.");
245 }
246 QString result;
247 const QString changesFileName = notificationsFileName();
248 QFile file(changesFileName);
249 if (!file.open(QIODevice::ReadOnly)) {
250 return QString::fromLatin1("Error reading ") + changesFileName;
251 }
252
253 QDataStream stream(&file);
254 stream.setVersion(QDataStream::Qt_4_6);
255
256 QByteArray sessionId, resource, destResource;
257 int type, operation, entityCnt;
258 quint64 parentCollection, parentDestCollection;
259 QString remoteId, remoteRevision, mimeType;
260 QSet<QByteArray> itemParts, addedFlags, removedFlags;
261 QSet<qint64> addedTags, removedTags;
262 QVariantList items;
263
264 QStringList list;
265
266 quint64 sizeAndVersion;
267 stream >> sizeAndVersion;
268
269 const quint64 size = sizeAndVersion & s_sizeMask;
270 const quint64 version = (sizeAndVersion & s_versionMask) >> 32;
271
272 quint64 startOffset = 0;
273 if (version >= 1) {
274 stream >> startOffset;
275 }
276
277 for (quint64 i = 0; i < size && !stream.atEnd(); ++i) {
278 stream >> sessionId;
279 stream >> type;
280 stream >> operation;
281 stream >> entityCnt;
282 for (int j = 0; j < entityCnt; ++j) {
283 QVariantMap map;
284 stream >> map[QLatin1String("uid")];
285 stream >> map[QLatin1String("remoteId")];
286 stream >> map[QLatin1String("remoteRevision")];
287 stream >> map[QLatin1String("mimeType")];
288 items << map;
289 }
290 stream >> resource;
291 stream >> destResource;
292 stream >> parentCollection;
293 stream >> parentDestCollection;
294 stream >> itemParts;
295 stream >> addedFlags;
296 stream >> removedFlags;
297 if (version == 3) {
298 stream >> addedTags;
299 stream >> removedTags;
300 }
301
302 if (i < startOffset) {
303 continue;
304 }
305
306 QString typeString;
307 switch (type) {
308 case NotificationMessageV2::Collections:
309 typeString = QLatin1String("Collections");
310 break;
311 case NotificationMessageV2::Items:
312 typeString = QLatin1String("Items");
313 break;
314 case NotificationMessageV2::Tags:
315 typeString = QLatin1String("Tags");
316 break;
317 default:
318 typeString = QLatin1String("InvalidType");
319 break;
320 };
321
322 QString operationString;
323 switch (operation) {
324 case NotificationMessageV2::Add:
325 operationString = QLatin1String("Add");
326 break;
327 case NotificationMessageV2::Modify:
328 operationString = QLatin1String("Modify");
329 break;
330 case NotificationMessageV2::ModifyFlags:
331 operationString = QLatin1String("ModifyFlags");
332 break;
333 case NotificationMessageV2::ModifyTags:
334 operationString = QLatin1String("ModifyTags");
335 break;
336 case NotificationMessageV2::Move:
337 operationString = QLatin1String("Move");
338 break;
339 case NotificationMessageV2::Remove:
340 operationString = QLatin1String("Remove");
341 break;
342 case NotificationMessageV2::Link:
343 operationString = QLatin1String("Link");
344 break;
345 case NotificationMessageV2::Unlink:
346 operationString = QLatin1String("Unlink");
347 break;
348 case NotificationMessageV2::Subscribe:
349 operationString = QLatin1String("Subscribe");
350 break;
351 case NotificationMessageV2::Unsubscribe:
352 operationString = QLatin1String("Unsubscribe");
353 break;
354 default:
355 operationString = QLatin1String("InvalidOp");
356 break;
357 };
358
359 QStringList itemPartsList, addedFlagsList, removedFlagsList, addedTagsList, removedTagsList;
360 foreach (const QByteArray &b, itemParts) {
361 itemPartsList.push_back(QString::fromLatin1(b));
362 }
363 foreach (const QByteArray &b, addedFlags) {
364 addedFlagsList.push_back(QString::fromLatin1(b));
365 }
366 foreach (const QByteArray &b, removedFlags) {
367 removedFlagsList.push_back(QString::fromLatin1(b));
368 }
369 foreach (qint64 id, addedTags) {
370 addedTagsList.push_back(QString::number(id));
371 }
372 foreach (qint64 id, removedTags) {
373 removedTagsList.push_back(QString::number(id)) ;
374 }
375
376 const QString entry = QString::fromLatin1("session=%1 type=%2 operation=%3 items=%4 resource=%5 destResource=%6 parentCollection=%7 parentDestCollection=%8 itemParts=%9 addedFlags=%10 removedFlags=%11 addedTags=%12 removedTags=%13")
377 .arg(QString::fromLatin1(sessionId))
378 .arg(typeString)
379 .arg(operationString)
380 .arg(QVariant(items).toString())
381 .arg(QString::fromLatin1(resource))
382 .arg(QString::fromLatin1(destResource))
383 .arg(parentCollection)
384 .arg(parentDestCollection)
385 .arg(itemPartsList.join(QLatin1String(", ")))
386 .arg(addedFlagsList.join(QLatin1String(", ")))
387 .arg(removedFlagsList.join(QLatin1String(", ")))
388 .arg(addedTagsList.join(QLatin1String(", ")))
389 .arg(removedTagsList.join(QLatin1String(", ")));
390
391 result += entry + QLatin1Char('\n');
392 }
393
394 return result;
395}
396
397void ChangeRecorderPrivate::addToStream(QDataStream &stream, const NotificationMessageV3 &msg)
398{
399 stream << msg.sessionId();
400 stream << int(msg.type());
401 stream << int(msg.operation());
402 stream << msg.entities().count();
403 Q_FOREACH (const NotificationMessageV2::Entity &entity, msg.entities()) {
404 stream << quint64(entity.id);
405 stream << entity.remoteId;
406 stream << entity.remoteRevision;
407 stream << entity.mimeType;
408 }
409 stream << msg.resource();
410 stream << msg.destinationResource();
411 stream << quint64(msg.parentCollection());
412 stream << quint64(msg.parentDestCollection());
413 stream << msg.itemParts();
414 stream << msg.addedFlags();
415 stream << msg.removedFlags();
416 stream << msg.addedTags();
417 stream << msg.removedTags();
418}
419
420void ChangeRecorderPrivate::writeStartOffset()
421{
422 if (!settings) {
423 return;
424 }
425
426 QFile file(notificationsFileName());
427 if (!file.open(QIODevice::ReadWrite)) {
428 qWarning() << "Could not update notifications in file" << file.fileName();
429 return;
430 }
431
432 // Skip "countAndVersion"
433 file.seek(8);
434
435 //kDebug() << "Writing start offset=" << m_startOffset;
436
437 QDataStream stream(&file);
438 stream.setVersion(QDataStream::Qt_4_6);
439 stream << static_cast<quint64>(m_startOffset);
440
441 // Everything else stays unchanged
442}
443
444void ChangeRecorderPrivate::saveNotifications()
445{
446 if (!settings) {
447 return;
448 }
449
450 QFile file(notificationsFileName());
451 QFileInfo info(file);
452 if (!QFile::exists(info.absolutePath())) {
453 QDir dir;
454 dir.mkpath(info.absolutePath());
455 }
456 if (!file.open(QIODevice::WriteOnly)) {
457 qWarning() << "Could not save notifications to file" << file.fileName();
458 return;
459 }
460 saveTo(&file);
461 m_needFullSave = false;
462 m_startOffset = 0;
463}
464
465void ChangeRecorderPrivate::saveTo(QIODevice *device)
466{
467 // Version 0 of this file format was writing a quint64 count, followed by the notifications.
468 // Version 1 bundles a version number into that quint64, to be able to detect a version number at load time.
469
470 const quint64 countAndVersion = static_cast<quint64>(pendingNotifications.count()) | s_currentVersion;
471
472 QDataStream stream(device);
473 stream.setVersion(QDataStream::Qt_4_6);
474
475 stream << countAndVersion;
476 stream << quint64(0); // no start offset
477
478 //kDebug() << "Saving" << pendingNotifications.count() << "notifications (full save)";
479
480 for (int i = 0; i < pendingNotifications.count(); ++i) {
481 const NotificationMessageV3 msg = pendingNotifications.at(i);
482 addToStream(stream, msg);
483 }
484}
485
486void ChangeRecorderPrivate::notificationsEnqueued(int count)
487{
488 // Just to ensure the contract is kept, and these two methods are always properly called.
489 if (enableChangeRecording) {
490 m_lastKnownNotificationsCount += count;
491 if (m_lastKnownNotificationsCount != pendingNotifications.count()) {
492 kWarning() << this << "The number of pending notifications changed without telling us! Expected"
493 << m_lastKnownNotificationsCount << "but got" << pendingNotifications.count()
494 << "Caller just added" << count;
495 Q_ASSERT(pendingNotifications.count() == m_lastKnownNotificationsCount);
496 }
497
498 saveNotifications();
499 }
500}
501
502void ChangeRecorderPrivate::dequeueNotification()
503{
504 pendingNotifications.dequeue();
505
506 if (enableChangeRecording) {
507
508 Q_ASSERT(pendingNotifications.count() == m_lastKnownNotificationsCount - 1);
509 --m_lastKnownNotificationsCount;
510
511 if (m_needFullSave || pendingNotifications.isEmpty()) {
512 saveNotifications();
513 } else {
514 ++m_startOffset;
515 writeStartOffset();
516 }
517 }
518}
519
520void ChangeRecorderPrivate::notificationsErased()
521{
522 if (enableChangeRecording) {
523 m_lastKnownNotificationsCount = pendingNotifications.count();
524 m_needFullSave = true;
525 saveNotifications();
526 }
527}
528
529void ChangeRecorderPrivate::notificationsLoaded()
530{
531 m_lastKnownNotificationsCount = pendingNotifications.count();
532 m_startOffset = 0;
533}
534