From fa7a5f0747b525cbee68c4ac53178720b1273111 Mon Sep 17 00:00:00 2001
From: Tristan Cavelier <tristan.cavelier@tiolive.com>
Date: Wed, 16 Jan 2013 12:59:45 +0100
Subject: [PATCH] replicaterevisionstorage.js post command added + tests

---
 src/jio.storage/replicaterevisionstorage.js | 210 ++++++++++++++++++++
 test/jiotests.js                            | 194 ++++++++++++++++++
 2 files changed, 404 insertions(+)
 create mode 100644 src/jio.storage/replicaterevisionstorage.js

diff --git a/src/jio.storage/replicaterevisionstorage.js b/src/jio.storage/replicaterevisionstorage.js
new file mode 100644
index 0000000..ba981e7
--- /dev/null
+++ b/src/jio.storage/replicaterevisionstorage.js
@@ -0,0 +1,210 @@
+/*jslint indent: 2, maxlen: 80, nomen: true */
+/*global jIO: true */
+/**
+ * JIO Replicate Revision Storage.
+ * It manages storages that manage revisions and conflicts.
+ * Description:
+ * {
+ *     "type": "replicaterevision",
+ *     "storage_list": [
+ *         <sub storage description>,
+ *         ...
+ *     ]
+ * }
+ */
+jIO.addStorageType('replicaterevision', function (spec, my) {
+  "use strict";
+  var that, priv = {};
+  spec = spec || {};
+  that = my.basicStorage(spec, my);
+
+  priv.storage_list_key = "storage_list";
+  priv.storage_list = spec[priv.storage_list_key];
+  my.env = my.env || spec.env || {};
+
+  that.specToStore = function () {
+    var o = {};
+    o[priv.storage_list_key] = priv.storage_list;
+    o.env = my.env;
+    return o;
+  };
+
+  /**
+   * Generate a new uuid
+   * @method generateUuid
+   * @return {string} The new uuid
+   */
+  priv.generateUuid = function () {
+    var S4 = function () {
+      var i, string = Math.floor(
+        Math.random() * 0x10000 /* 65536 */
+      ).toString(16);
+      for (i = string.length; i < 4; i += 1) {
+        string = "0" + string;
+      }
+      return string;
+    };
+    return S4() + S4() + "-" +
+      S4() + "-" +
+      S4() + "-" +
+      S4() + "-" +
+      S4() + S4() + S4();
+  };
+
+  /**
+   * Generates a hash code of a string
+   * @method hashCode
+   * @return {string} The next revision
+   */
+  priv.getNextRevision = function (docid) {
+    my.env[docid].id += 1;
+    return my.env[docid].id.toString();
+  };
+
+  /**
+   * Checks a revision format
+   * @method checkRevisionFormat
+   * @param  {string} revision The revision string
+   * @return {boolean} True if ok, else false
+   */
+  priv.checkRevisionFormat = function (revision) {
+    return (/^[0-9a-zA-Z_]+$/.test(revision));
+  };
+
+  /**
+   * Initalize document environment object
+   * @method initEnv
+   * @param  {string} docid The document id
+   * @return {object} The reference to the environment
+   */
+  priv.initEnv = function (docid) {
+    my.env[docid] = {
+      "id": 0,
+      "distant_revisions": {},
+      "my_revisions": {},
+      "last_revisions": []
+    };
+    return my.env[docid];
+  };
+
+  /**
+   * Post the document metadata to all sub storages
+   * @method post
+   * @param  {object} command The JIO command
+   */
+  that.post = function (command) {
+    var functions = {}, doc_env, revs_info, doc, my_rev;
+    functions.begin = function () {
+      doc = command.cloneDoc();
+
+      if (typeof doc._rev === "string" && !priv.checkRevisionFormat(doc._rev)) {
+        that.error({
+          "status": 31,
+          "statusText": "Wrong Revision Format",
+          "error": "wrong_revision_format",
+          "message": "The document previous revision does not match " +
+            "^[0-9]+-[0-9a-zA-Z]+$",
+          "reason": "Previous revision is wrong"
+        });
+        return;
+      }
+      if (typeof doc._id !== "string") {
+        doc._id = priv.generateUuid();
+      }
+      if (priv.update_doctree_allowed === undefined) {
+        priv.update_doctree_allowed = true;
+      }
+      doc_env = my.env[doc._id];
+      if (doc_env && doc_env.id) {
+        if (!priv.update_doctree_allowed) {
+          that.error({
+            "status": 409,
+            "statusText": "Conflict",
+            "error": "conflict",
+            "message": "Cannot update a document",
+            "reason": "Document update conflict"
+          });
+          return;
+        }
+      } else {
+        doc_env = priv.initEnv(doc._id);
+      }
+      my_rev = priv.getNextRevision(doc._id);
+      functions.sendDocument();
+    };
+    functions.sendDocumentIndex = function (method, index, callback) {
+      var wrapped_callback_success, wrapped_callback_error;
+      wrapped_callback_success = function (response) {
+        callback(method, index, undefined, response);
+      };
+      wrapped_callback_error = function (err) {
+        callback(method, index, err, undefined);
+      };
+      if (typeof doc._rev === "string" &&
+          doc_env.my_revisions[doc._rev] !== undefined) {
+        doc._rev = doc_env.my_revisions[doc._rev][index];
+      }
+      that.addJob(
+        method,
+        priv.storage_list[index],
+        doc,
+        command.cloneOption(),
+        wrapped_callback_success,
+        wrapped_callback_error
+      );
+    };
+    functions.sendDocument = function () {
+      var i;
+      doc_env.my_revisions[my_rev] = doc_env.my_revisions[my_rev] || [];
+      doc_env.my_revisions[my_rev].length = priv.storage_list.length;
+      for (i = 0; i < priv.storage_list.length; i += 1) {
+        functions.sendDocumentIndex(
+          doc_env.last_revisions[i] === "unique_" + i ? "put" : "post",
+          i,
+          functions.checkSendResult
+        );
+      }
+    };
+    functions.checkSendResult = function (method, index, err, response) {
+      if (err) {
+        if (err.status === 409) {
+          if (method !== "put") {
+            functions.sendDocumentIndex(
+              "put",
+              index,
+              functions.checkSendResult
+            );
+            return;
+          }
+        }
+        functions.updateEnv(index, undefined);
+        functions.error(err);
+        return;
+      }
+      // success
+      functions.updateEnv(index, response.rev || "unique_" + index);
+      functions.success({"ok": true, "id": doc._id, "rev": my_rev});
+    };
+    functions.updateEnv = function (index, revision) {
+      doc_env.last_revisions[index] = revision;
+      doc_env.my_revisions[my_rev][index] = revision;
+      doc_env.distant_revisions[revision] = my_rev;
+    };
+    functions.success = function (response) {
+      if (!functions.success_called_once) {
+        functions.success_called_once = true;
+        that.success(response);
+      }
+    };
+    functions.error_count = 0;
+    functions.error = function (err) {
+      functions.error_count += 1;
+      if (functions.error_count === priv.storage_list.length) {
+        that.error(err);
+      }
+    };
+    functions.begin();
+  };
+
+  return that;
+});
diff --git a/test/jiotests.js b/test/jiotests.js
index bedc30b..309fb03 100644
--- a/test/jiotests.js
+++ b/test/jiotests.js
@@ -1813,6 +1813,200 @@ test ("Scenario", function(){
 
 });
 
+  module ("JIO Replicate Revision Storage");
+
+  var testReplicateRevisionStorageGenerator = function (
+    sinon, jio_description, document_name_have_revision
+  ) {
+
+    var o = generateTools(sinon), leavesAction, generateLocalPath;
+
+    o.jio = JIO.newJio(jio_description);
+
+    generateLocalPath = function (storage_description) {
+      return "jio/localstorage/" + storage_description.username + "/" +
+        storage_description.application_name;
+    };
+
+    leavesAction = function (action, storage_description, param) {
+      var i;
+      if (param === undefined) {
+        param = {};
+      } else {
+        param = clone(param);
+      }
+      if (storage_description.storage_list !== undefined) {
+        // it is the replicate revision storage tree
+        for (i = 0; i < storage_description.storage_list.length; i += 1) {
+          leavesAction(action, storage_description.storage_list[i], param);
+        }
+      } else if (storage_description.sub_storage !== undefined) {
+        // it is the revision storage tree
+        param.revision = true;
+        leavesAction(action, storage_description.sub_storage, param);
+      } else {
+        // it is the storage tree leaf
+        param[storage_description.type] = true;
+        action(storage_description, param);
+      }
+    };
+    o.leavesAction = function (action) {
+      leavesAction(action, jio_description);
+    };
+
+    // post a new document without id
+    o.doc = {"title": "post document without id"};
+    o.revision = {"start": 0, "ids": []};
+    o.spy(o, "status", undefined, "Post document (without id)");
+    o.jio.post(o.doc, function (err, response) {
+      o.f.apply(arguments);
+      o.response_rev = (err || response).rev;
+      if (isUuid((err || response).id)) {
+        ok(true, "Uuid format");
+        o.uuid = (err || response).id;
+      } else {
+        deepEqual((err || response).id,
+                  "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "Uuid format");
+      }
+    });
+    o.tick(o);
+
+    // check document
+    o.doc._id = o.uuid;
+    o.rev = "1";
+    o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision);
+    o.leavesAction(function (storage_description, param) {
+      var suffix = "", doc = clone(o.doc);
+      if (param.revision) {
+        deepEqual(o.response_rev, o.rev, "Check revision");
+        doc._id += "." + o.local_rev;
+        suffix = "." + o.local_rev;
+      }
+      deepEqual(
+        localstorage.getItem(generateLocalPath(storage_description) +
+                             "/" + o.uuid + suffix),
+        doc, "Check document"
+      );
+    });
+
+    // post a new document with id
+    o.doc = {"_id": "post1", "title": "post new doc with id"};
+    o.rev = "1"
+    o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev},
+          "Post document (with id)");
+    o.jio.post(o.doc, o.f);
+    o.tick(o);
+
+    // check document
+    o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision);
+    o.leavesAction(function (storage_description, param) {
+      var suffix = "", doc = clone(o.doc);
+      if (param.revision) {
+        doc._id += "." + o.local_rev;
+        suffix = "." + o.local_rev;
+      }
+      deepEqual(
+        localstorage.getItem(generateLocalPath(storage_description) +
+                             "/post1" + suffix),
+        doc, "Check document"
+      );
+    });
+
+    // post same document without revision
+    o.doc = {"_id": "post1", "title": "post same document without revision"};
+    o.rev = "2";
+    o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev},
+          "Post same document (without revision)");
+    o.jio.post(o.doc, o.f);
+    o.tick(o);
+
+    // check document
+    o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision);
+    o.leavesAction(function (storage_description, param) {
+      var suffix = "", doc = clone(o.doc);
+      if (param.revision) {
+        doc._id += "." + o.local_rev;
+        suffix = "." + o.local_rev;
+      }
+      deepEqual(
+        localstorage.getItem(generateLocalPath(storage_description) +
+                             "/post1" + suffix),
+        doc, "Check document"
+      );
+    });
+
+    // post a new revision
+    o.doc = {"_id": "post1", "title": "post new revision", "_rev": o.rev};
+    o.rev = "3";
+    o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev},
+          "Post document (with revision)");
+    o.jio.post(o.doc, o.f);
+    o.tick(o);
+
+    // check document
+    o.revision.start += 1;
+    o.revision.ids.unshift(o.local_rev.split("-").slice(1).join("-"));
+    o.doc._rev = o.local_rev;
+    o.local_rev = "2-" + generateRevisionHash(o.doc, o.revision);
+    o.leavesAction(function (storage_description, param) {
+      var suffix = "", doc = clone(o.doc);
+      delete doc._rev;
+      if (param.revision) {
+        doc._id += "." + o.local_rev;
+        suffix = "." + o.local_rev;
+      }
+      deepEqual(
+        localstorage.getItem(generateLocalPath(storage_description) +
+                             "/post1" + suffix),
+        doc, "Check document"
+      );
+    });
+
+    o.jio.stop();
+
+  };
+
+  test ("[Local Storage] Scenario", function () {
+    testReplicateRevisionStorageGenerator(this, {
+      "type": "replicaterevision",
+      "storage_list": [{
+        "type": "local",
+        "username": "ureploc",
+        "application_name": "areploc"
+      }]
+    });
+  });
+  test ("[Revision + Local Storage] Scenario", function () {
+    testReplicateRevisionStorageGenerator(this, {
+      "type": "replicaterevision",
+      "storage_list": [{
+        "type": "revision",
+        "sub_storage": {
+          "type": "local",
+          "username": "ureprevloc",
+          "application_name": "areprevloc"
+        }
+      }]
+    });
+  });
+  test ("[Revision + Local Storage, Local Storage] Scenario", function () {
+    testReplicateRevisionStorageGenerator(this, {
+      "type": "replicaterevision",
+      "storage_list": [{
+        "type": "revision",
+        "sub_storage": {
+          "type": "local",
+          "username": "ureprevlocloc",
+          "application_name": "areprevlocloc"
+        }
+      },{
+        "type": "local",
+        "username": "ureprevlocloc2",
+        "application_name": "areprevlocloc2"
+      }]
+    });
+  });
+
 /*
 module ('Jio DAVStorage');
 
-- 
2.30.9