summaryrefslogtreecommitdiffstats
path: root/src/partition/jsondbobject.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/partition/jsondbobject.cpp')
-rw-r--r--src/partition/jsondbobject.cpp610
1 files changed, 610 insertions, 0 deletions
diff --git a/src/partition/jsondbobject.cpp b/src/partition/jsondbobject.cpp
new file mode 100644
index 0000000..e61d6eb
--- /dev/null
+++ b/src/partition/jsondbobject.cpp
@@ -0,0 +1,610 @@
+/****************************************************************************
+**
+** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies).
+** Contact: http://www.qt-project.org/
+**
+** This file is part of the QtAddOn.JsonDb module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** GNU Lesser General Public License Usage
+** This file may be used under the terms of the GNU Lesser General Public
+** License version 2.1 as published by the Free Software Foundation and
+** appearing in the file LICENSE.LGPL included in the packaging of this
+** file. Please review the following information to ensure the GNU Lesser
+** General Public License version 2.1 requirements will be met:
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Nokia gives you certain additional
+** rights. These rights are described in the Nokia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU General
+** Public License version 3.0 as published by the Free Software Foundation
+** and appearing in the file LICENSE.GPL included in the packaging of this
+** file. Please review the following information to ensure the GNU General
+** Public License version 3.0 requirements will be met:
+** http://www.gnu.org/copyleft/gpl.html.
+**
+** Other Usage
+** Alternatively, this file may be used in accordance with the terms and
+** conditions contained in a signed written agreement between you and Nokia.
+**
+**
+**
+**
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "jsondbobject.h"
+
+#include <QJSValue>
+#include <QJSValueIterator>
+#include <QStringBuilder>
+#include <QStringList>
+#include <QCryptographicHash>
+
+#include <qjsondocument.h>
+
+#include "jsondbstrings.h"
+#include "jsondbproxy.h"
+
+QT_BEGIN_NAMESPACE_JSONDB_PARTITION
+
+JsonDbObject::JsonDbObject()
+{
+}
+
+JsonDbObject::JsonDbObject(const QJsonObject &object)
+ : QJsonObject(object)
+{
+}
+
+JsonDbObject::~JsonDbObject()
+{
+}
+
+QByteArray JsonDbObject::toBinaryData() const
+{
+ return QJsonDocument(*this).toBinaryData();
+}
+
+QUuid JsonDbObject::uuid() const
+{
+ return QUuid(value(JsonDbString::kUuidStr).toString());
+}
+
+QString JsonDbObject::version() const
+{
+ return value(JsonDbString::kVersionStr).toString();
+}
+
+QString JsonDbObject::type() const
+{
+ return value(JsonDbString::kTypeStr).toString();
+}
+
+bool JsonDbObject::isDeleted() const
+{
+ QJsonValue deleted = value(JsonDbString::kDeletedStr);
+
+ if (deleted.isUndefined() || (deleted.isBool() && deleted.toBool() == false))
+ return false;
+
+ return true;
+}
+
+void JsonDbObject::markDeleted()
+{
+ insert(JsonDbString::kDeletedStr, true);
+}
+
+struct Uuid
+{
+ uint data1;
+ ushort data2;
+ ushort data3;
+ uchar data4[8];
+};
+
+// copied from src/client/qjsondbobject.cpp:
+static const Uuid JsonDbNamespace = {0x6ba7b810, 0x9dad, 0x11d1, { 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8} };
+
+/*!
+ Returns deterministic uuid that can be used to identify given \a identifier.
+
+ The uuid is generated using QtJsonDb UUID namespace on a value of the
+ given \a identifier.
+
+ \sa QJsonDbObject::createUuidFromString(), QJsonDbObject::createUuid()
+*/
+QUuid JsonDbObject::createUuidFromString(const QString &identifier)
+{
+ const QUuid ns(JsonDbNamespace.data1, JsonDbNamespace.data2, JsonDbNamespace.data3,
+ JsonDbNamespace.data4[0], JsonDbNamespace.data4[1], JsonDbNamespace.data4[2],
+ JsonDbNamespace.data4[3], JsonDbNamespace.data4[4], JsonDbNamespace.data4[5],
+ JsonDbNamespace.data4[6], JsonDbNamespace.data4[7]);
+ return QUuid::createUuidV3(ns, identifier);
+}
+
+void JsonDbObject::generateUuid()
+{
+ QLatin1String idStr("_id");
+ if (contains(idStr)) {
+ QUuid uuid(createUuidFromString(value(idStr).toString()));
+ insert(JsonDbString::kUuidStr, uuid.toString());
+ } else {
+ QUuid uuid(QUuid::createUuid());
+ insert(JsonDbString::kUuidStr, uuid.toString());
+ }
+}
+
+/*!
+ * \brief JsonDbObject::computeVersion mostly for legacy reasons.
+ * Same as calling updateVersionOptimistic(*this, &versionWritten)
+ *
+ * \sa updateVersionOptimistic(), version()
+ * \return the new version, which is also written to the object
+ */
+QString JsonDbObject::computeVersion()
+{
+ QString versionWritten;
+ updateVersionOptimistic(*this, &versionWritten);
+ return versionWritten;
+}
+
+/*!
+ * \brief JsonDbObject::updateVersionOptimistic implement an optimisticWrite
+ * \param other the object containing the update to be written. Do NOT call computeVersion()
+ * on the other object before passing it in! other._meta.history is assumed untrusted.
+ * \param versionWritten contains the version string of the write upon return
+ * \return true if the passed object is a valid write. As this version can operate
+ * on conflicts too, version() and versionWritten can differ.
+ */
+bool JsonDbObject::updateVersionOptimistic(const JsonDbObject &other, QString *versionWrittenOut)
+{
+ QString versionWritten;
+ // this is trusted and expected to contain a _meta object with book keeping info
+ QJsonObject meta = value(JsonDbString::kMetaStr).toObject();
+
+ // an array of all versions this object has replaced
+ QJsonArray history = meta.value(QStringLiteral("history")).toArray();
+
+ // all known conflicts
+ QJsonArray conflicts = meta.value(JsonDbString::kConflictsStr).toArray();
+
+ QString replacedVersion = other.version();
+
+ int replacedCount;
+ QString replacedHash = tokenizeVersion(replacedVersion, &replacedCount);
+
+ int updateCount = replacedCount;
+ QString hash = replacedHash;
+
+ // we don't trust other._meta.history, so other._version must be replacedVersion
+ // if other.computeVersion() was called before updateVersionOptimistic(), other can at max be a replay
+ // as we lost which version other is replacing.
+ bool isReplay = !other.computeVersion(replacedCount, replacedHash, &updateCount, &hash);
+
+ bool isValidWrite = false;
+
+ // first we check if this version can eliminate a conflict
+ for (QJsonArray::const_iterator ii = conflicts.begin(); ii < conflicts.end(); ii++) {
+
+ JsonDbObject conflict((*ii).toObject());
+ if (conflict.version() == replacedVersion) {
+ if (!isReplay)
+ conflicts.removeAt(ii.i);
+ if (!isValidWrite) {
+ addAncestor(&history, updateCount, hash);
+ versionWritten = versionAsString(updateCount, hash);
+ }
+ isValidWrite = true;
+ }
+ }
+
+ // now we check if this version can progress the head
+ if (version() == replacedVersion) {
+ if (!isReplay)
+ *this = other;
+ if (!isValidWrite)
+ versionWritten = versionAsString(updateCount, hash);
+ insert(JsonDbString::kVersionStr, versionWritten);
+ isValidWrite = true;
+ }
+
+ // make sure we can resurrect a tombstone
+ // Issue: Recreating a _uuid must have a updateCount higher than the tombstone
+ // otherwise it is considered a conflict.
+ if (!isValidWrite && isDeleted()) {
+ if (!isReplay) {
+ addAncestor(&history, replacedCount, replacedHash);
+ isReplay = false;
+ }
+
+ replacedHash = tokenizeVersion(version(), &replacedCount);
+ updateCount = replacedCount + 1;
+ versionWritten = versionAsString(updateCount, hash);
+
+ *this = other;
+ insert(JsonDbString::kVersionStr, versionWritten);
+ isValidWrite = true;
+ }
+
+ // update the book keeping of what versions we have replaced in this version branch
+ if (isValidWrite && !isReplay) {
+ addAncestor(&history, replacedCount, replacedHash);
+
+ meta = QJsonObject();
+
+ if (history.size())
+ meta.insert(QStringLiteral("history"), history);
+ if (conflicts.size())
+ meta.insert(JsonDbString::kConflictsStr, history);
+
+ if (!meta.isEmpty())
+ insert(JsonDbString::kMetaStr, meta);
+ }
+
+ // last chance for a valid write: other is a replay from history
+ if (!isValidWrite && isAncestorOf(history, updateCount, hash)) {
+ isValidWrite = true;
+ versionWritten = versionAsString(updateCount, hash);
+ }
+
+ if (versionWrittenOut)
+ *versionWrittenOut = versionWritten;
+
+ return isValidWrite;
+}
+
+/*!
+ * \brief JsonDbObject::updateVersionReplicating implements a replicatedWrite
+ * \param other the (remote) object to include into this one.
+ * \return if the passed object was a valid replication
+ */
+bool JsonDbObject::updateVersionReplicating(const JsonDbObject &other)
+{
+ // these two will be the final _meta content
+ QJsonArray history;
+ QJsonArray conflicts;
+
+ // let's go thru all version, i.e. this, this._conflicts, other, and other._conflicts
+ {
+ // thanks to the operator <, documents will sort and remove duplicates
+ // the value is just for show, QSet is based on QHash, which does not sort
+ QMap<JsonDbObject,bool> documents;
+
+ QUuid id = uuid();
+ populateMerge(&documents, id, *this);
+ if (!populateMerge(&documents, id, other, true))
+ return false;
+
+ // now we have all versions sorted and duplicates removed
+ // let's figure out what to keep, what to toss
+ // this is O(n^2) but should be fine in real world situations
+ for (QMap<JsonDbObject,bool>::const_iterator ii = documents.begin(); ii != documents.end(); ii++) {
+ bool alive = !ii.key().isDeleted();
+ for (QMap<JsonDbObject,bool>::const_iterator jj = ii + 1; alive && jj != documents.end(); jj++)
+ if (ii.key().isAncestorOf(jj.key()))
+ alive = false;
+
+ if (ii+1 == documents.end()) {
+ // last element, so found the winner,
+ // assigning to *this, which is head
+ *this = ii.key();
+ populateHistory(&history, *this, false);
+ } else if (alive) {
+ // this is a conflict, strip _meta and keep it
+ JsonDbObject conflict(ii.key());
+ conflict.remove(JsonDbString::kMetaStr);
+ conflicts.append(conflict);
+ } else {
+ // this version was replaced, just keep history
+ populateHistory(&history, ii.key(), true);
+ }
+ }
+ }
+
+ // let's write a new _meta into head
+ if (history.size() || conflicts.size()) {
+ QJsonObject meta;
+ if (history.size())
+ meta.insert(QStringLiteral("history"), history);
+ if (conflicts.size())
+ meta.insert(JsonDbString::kConflictsStr, conflicts);
+ insert(JsonDbString::kMetaStr, meta);
+ } else {
+ // this is really just for sanity reason, but it feels better to have it
+ // aka: this branch should never be reached in real world situations
+ remove(JsonDbString::kMetaStr);
+ }
+
+ return true;
+}
+
+bool JsonDbObject::populateMerge(QMap<JsonDbObject,bool> *documents, const QUuid &id, const JsonDbObject &source, bool validateSource, bool recurse) const
+{
+ // is this the same uuid?
+ bool valid = source.uuid() == id;
+
+ if (valid && validateSource) {
+ // validate that the version is actually correct
+ int count;
+ QString hash = tokenizeVersion(source.version(), &count);
+ if (count == 0 || source.computeVersion(count, hash, 0, 0))
+ valid = false;
+ }
+
+ // there is source._meta.conflicts to explore
+ if (recurse && source.contains(JsonDbString::kMetaStr)) {
+ QJsonArray conflicts = source.value(JsonDbString::kMetaStr).toObject().value(JsonDbString::kConflictsStr).toArray();
+ for (int ii = 0; ii < conflicts.size(); ii++)
+ if (!populateMerge(documents, id, conflicts.at(ii).toObject(), validateSource, false))
+ valid = false;
+ }
+
+ if (valid && documents)
+ documents->insert(source,true);
+
+ return valid;
+}
+
+void JsonDbObject::populateHistory(QJsonArray *history, const JsonDbObject &doc, bool includeCurrent) const
+{
+ QJsonArray versions = doc.value(JsonDbString::kMetaStr).toObject().value(QStringLiteral("history")).toArray();
+
+ for (int ii = 0; ii < versions.size(); ii++) {
+ QJsonValue hash = versions.at(ii);
+ if (hash.isString()) {
+ addAncestor(history, ii + 1, hash.toString());
+ } else if (hash.isArray()) {
+ QJsonArray hashArray = hash.toArray();
+ for (QJsonArray::const_iterator jj = hashArray.begin(); jj != hashArray.end(); jj++) {
+ if ((*jj).isString())
+ addAncestor(history, ii + 1, (*jj).toString());
+ }
+ }
+ }
+
+ if (includeCurrent) {
+ int updateCount;
+ QString hash = tokenizeVersion(doc.version(), &updateCount);
+ addAncestor(history, updateCount, hash);
+ }
+}
+
+QString JsonDbObject::tokenizeVersion(const QString &versionIn, int *updateCountOut) const
+{
+ int updateCount;
+ QString hash;
+
+ if (versionIn.isEmpty()) {
+ updateCount = 0;
+ } else {
+ QStringList splitUp = versionIn.split(QChar('-'));
+ if (splitUp.size() == 2) {
+ updateCount = qMax(1, splitUp.at(0).toInt());
+ hash = splitUp.at(1);
+ } else {
+ updateCount = 1;
+ hash = versionIn;
+ }
+ }
+
+ if (updateCountOut)
+ *updateCountOut = updateCount;
+
+ return hash;
+}
+
+QString JsonDbObject::versionAsString(const int updateCount, const QString &hash) const
+{
+ return QString::number(updateCount) % QStringLiteral("-") % hash;
+}
+
+
+bool JsonDbObject::computeVersion(const int oldUpdateCount, const QString& oldHash, int *newUpdateCount, QString *newHash) const
+{
+ QCryptographicHash md5(QCryptographicHash::Md5);
+
+ for (const_iterator ii = begin(); ii != end(); ii++) {
+ QString key = ii.key();
+ if (key == JsonDbString::kUuidStr || key == JsonDbString::kVersionStr || key == JsonDbString::kMetaStr)
+ continue;
+
+ md5.addData((char *) key.constData(), key.size() * 2);
+
+ char kar = ii.value().type();
+ md5.addData((char *) &kar, 1);
+
+ switch (ii.value().type()) {
+ case QJsonValue::Bool:
+ kar = ii.value().toBool() ? '1' : '0';
+ md5.addData((char *) &kar, 1);
+ break;
+ case QJsonValue::Double: {
+ double value = ii.value().toDouble();
+ md5.addData((char *) &value, sizeof(double));
+ break;
+ }
+ case QJsonValue::String: {
+ QString value = ii.value().toString();
+ md5.addData((char *) value.constData(), value.size() * 2);
+ break;
+ }
+ case QJsonValue::Array: {
+ QJsonDocument doc(ii.value().toArray());
+ int size;
+ const char *data = doc.rawData(&size);
+ md5.addData(data, size);
+ break;
+ }
+ case QJsonValue::Object: {
+ QJsonDocument doc(ii.value().toObject());
+ int size;
+ const char *data = doc.rawData(&size);
+ md5.addData(data, size);
+ break;
+ }
+ default:;
+ // do nothing
+ }
+ }
+
+ QString computedHash = QString::fromLatin1(md5.result().toHex().constData(), 10);
+
+ if (computedHash != oldHash) {
+ if (newUpdateCount)
+ *newUpdateCount = oldUpdateCount + 1;
+ if (newHash)
+ *newHash = computedHash;
+ return true;
+ }
+
+ return false;
+}
+
+/*!
+ * \brief JsonDbObject::isAncestorOf tests if this JsonDbObject contains an ancestor version
+ * of the passed JsonDbObject. It does NOT take _uuid into account, it works on version()
+ * only.
+ *
+ * For this method to return a valid answer, the passed object needs to have an intact
+ * _meta object.
+ *
+ * \param other the object to check ancestorship
+ * \return true if this object is an ancestor version of the passed object
+ */
+bool JsonDbObject::isAncestorOf(const JsonDbObject &other) const
+{
+ QJsonArray history = other.value(JsonDbString::kMetaStr).toObject().value(QStringLiteral("history")).toArray();
+
+ int updateCount;
+ QString hash = tokenizeVersion(version(), &updateCount);
+
+ return isAncestorOf(history, updateCount, hash);
+}
+
+bool JsonDbObject::isAncestorOf(const QJsonArray &history, const int updateCount, const QString &hash) const
+{
+ if (updateCount < 1 || history.size() < updateCount)
+ return false;
+
+ QJsonValue knownHashes = history.at(updateCount - 1);
+ if (knownHashes.isString())
+ return knownHashes.toString() == hash;
+ else if (knownHashes.isArray())
+ return knownHashes.toArray().contains(hash);
+ else
+ return false;
+}
+
+void JsonDbObject::addAncestor(QJsonArray *history, const int updateCount, const QString &hash) const
+{
+ if (updateCount < 1 || !history)
+ return;
+
+ int pos = updateCount - 1;
+ for (int ii = history->size(); ii < updateCount; ii++)
+ history->append(QJsonValue::Null);
+
+ QJsonValue old = history->at(pos);
+ if (old.isArray()) {
+ QJsonArray multi = old.toArray();
+ for (int ii = multi.size(); ii-- > 0;) {
+ QString oldHash = multi.at(ii).toString();
+ if (oldHash == hash) {
+ return;
+ } else if (oldHash < hash) {
+ multi.insert(ii + 1, hash);
+ history->replace(pos, multi);
+ return;
+ }
+ }
+ multi.prepend(hash);
+ history->replace(pos, multi);
+ } else if (!old.isString()) {
+ history->replace(pos, hash);
+ } else {
+ QString oldHash = old.toString();
+ if (oldHash == hash)
+ return;
+
+ QJsonArray multi;
+ if (oldHash < hash) {
+ multi.append(oldHash);
+ multi.append(hash);
+ } else if (oldHash > hash) {
+ multi.append(hash);
+ multi.append(oldHash);
+ }
+ history->replace(pos, multi);
+ }
+}
+
+/*!
+ * \brief JsonDbObject::operator < only operates based on version number.
+ * Versions are sorted by update count first, then by string comparing
+ * the hash. This operator does NOT sort by _uuid.
+ *
+ * \sa computeVersion(), version()
+ * \param other the JsonDbObject to compare it to.
+ * \return bool when left side is considered to be an early version
+ */
+bool JsonDbObject::operator <(const JsonDbObject &other) const
+{
+ int myCount;
+ QString myHash = tokenizeVersion(version(), &myCount);
+ int otherCount;
+ QString otherHash = tokenizeVersion(other.version(), &otherCount);
+
+ if (myCount != otherCount)
+ return myCount < otherCount;
+
+ return myHash < otherHash;
+}
+
+
+QJsonValue JsonDbObject::propertyLookup(const QString &path) const
+{
+ return propertyLookup(path.split('.'));
+}
+
+QJsonValue JsonDbObject::propertyLookup(const QStringList &path) const
+{
+ if (!path.size()) {
+ qCritical() << "JsonDb::propertyLookup empty path";
+ abort();
+ return QJsonValue(QJsonValue::Undefined);
+ }
+ // TODO: one malloc here
+ QJsonValue value(*this);
+ for (int i = 0; i < path.size(); i++) {
+ const QString &key = path.at(i);
+ // this part of the property is a list
+ if (value.isArray()) {
+ QJsonArray objectList = value.toArray();
+ bool ok = false;
+ int index = key.toInt(&ok);
+ if (ok && (index >= 0) && (objectList.size() > index))
+ value = objectList.at(index);
+ else
+ value = QJsonValue(QJsonValue::Undefined);
+ } else if (value.isObject()) {
+ QJsonObject o = value.toObject();
+ if (o.contains(key))
+ value = o.value(key);
+ else
+ value = QJsonValue(QJsonValue::Undefined);
+ } else {
+ value = QJsonValue(QJsonValue::Undefined);
+ }
+ }
+ return value;
+}
+
+QT_END_NAMESPACE_JSONDB_PARTITION