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 | |
27 | using namespace Akonadi; |
28 | |
29 | ChangeRecorderPrivate::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 | |
40 | int 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 | |
48 | void 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. |
61 | QString ChangeRecorderPrivate::notificationsFileName() const |
62 | { |
63 | return settings->fileName() + QLatin1String("_changes.dat" ); |
64 | } |
65 | |
66 | void 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 | |
129 | static const quint64 s_currentVersion = Q_UINT64_C(0x000300000000); |
130 | static const quint64 s_versionMask = Q_UINT64_C(0xFFFF00000000); |
131 | static const quint64 s_sizeMask = Q_UINT64_C(0x0000FFFFFFFF); |
132 | |
133 | QQueue<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 | |
241 | QString 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 | |
397 | void 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 | |
420 | void 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 | |
444 | void 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 | |
465 | void 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 | |
486 | void 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 | |
502 | void 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 | |
520 | void ChangeRecorderPrivate::notificationsErased() |
521 | { |
522 | if (enableChangeRecording) { |
523 | m_lastKnownNotificationsCount = pendingNotifications.count(); |
524 | m_needFullSave = true; |
525 | saveNotifications(); |
526 | } |
527 | } |
528 | |
529 | void ChangeRecorderPrivate::notificationsLoaded() |
530 | { |
531 | m_lastKnownNotificationsCount = pendingNotifications.count(); |
532 | m_startOffset = 0; |
533 | } |
534 | |