/* * JIO extension for resource global identifier management. * Copyright (C) 2013 Nexedi SA * * This library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*global jIO: true, localStorage: true, setTimeout: true, complex_queries: true */ /** * JIO GID Storage. Type = 'gid'. * Identifies document with their global identifier représentation * * Sub storages must support complex queries. * * Storage Description: * * { * "type": "gid", * "sub_storage": {<storage description>}, * "constraints": { * "default": { * "identifier": "list", // ['a', 1] * "type": "DCMIType", // 'Text' * "title": "string" // 'something blue' * }, * "Text": { * "format": "contentType" // contains 'text/plain;charset=utf-8' * }, * "Image": { * "version": "json" // value as is * } * } * } */ (function () { var dcmi_types, metadata_actions, content_type_re; dcmi_types = { 'Collection': 'Collection', 'Dataset': 'Dataset', 'Event': 'Event', 'Image': 'Image', 'InteractiveResource': 'InteractiveResource', 'MovingImage': 'MovingImage', 'PhysicalObject': 'PhysicalObject', 'Service': 'Service', 'Software': 'Software', 'Sound': 'Sound', 'StillImage': 'StillImage', 'Text': 'Text' }; metadata_actions = { json: function (value) { return value; }, string: function (value) { if (typeof value === 'string') { return value; } }, list: function (value) { var i, new_value = []; if (Array.isArray(value)) { for (i = 0; i < value.length; i += 1) { if (typeof value[i] === 'object') { new_value[new_value.length] = value[i].content; } else { new_value[new_value.length] = value[i]; } } } else if (value !== undefined) { value = [value]; } return value; }, DCMIType: function (value) { return dcmi_types[value]; }, contentType: function (value) { var i; if (!Array.isArray(value)) { value = [value]; } for (i = 0; i < value.length; i += 1) { if (value[i] === 'object') { if (content_type_re.test(value[i].content)) { return value[i].content; } } else { if (content_type_re.test(value[i])) { return value[i]; } } } } }; content_type_re = /^([a-z]+\/[a-zA-Z0-9\+\-\.]+)(?:\s*;\s*charset\s*=\s*([a-zA-Z0-9\-]+))?$/; function Metadata(metadata) { if (typeof metadata === 'object' && !Array.isArray(metadata)) { this.metadata = metadata; } else { this.metadata = {}; } Metadata.prototype.update.call(this, metadata); } Metadata.prototype.update = function (metadata) { var k; for (k in metadata) { if (metadata.hasOwnProperty(k)) { if (metadata[k] !== undefined) { if (k[0] === '_') { this.metadata[k] = JSON.parse(JSON.stringify(metadata[k])); } else { this.metadata[k] = Metadata.normalizeValue(metadata[k]); } } } } return this; }; Metadata.prototype.get = function (key) { return this.metadata[key]; }; Metadata.prototype.set = function (key, value) { if (value === undefined) { delete this.metadata[key]; return this; } if (key[0] === '_') { this.metadata[key] = value; } else { this.metadata[key] = Metadata.normalizeValue(value); } return this; }; Metadata.normalizeArray = function (value) { var i; if (value.length === 0) { return; } value = value.slice(); i = 0; while (i < value.length) { if (typeof value[i] === 'object' && !Array.isArray(value[i])) { value[i] = Metadata.normalizeObject(value[i]); if (value[i] === undefined) { value.splice(i, 1); } else { i += 1; } } else if ((typeof value[i] === 'string') || (isNaN(value[i]) && typeof value[i] === 'number')) { i += 1; } else { value.splice(i, 1); } } if (value.length === 1) { return value[0]; } return value; }; Metadata.normalizeObject = function (value) { var i, count = 0, new_value = {}; for (i in value) { if (value.hasOwnProperty(i)) { if ((typeof value[i] === 'string') || (isNaN(value[i]) && typeof value[i] === 'number')) { new_value[i] = value[i]; count += 1; } } } if (new_value.content === undefined) { return; } if (count === 1) { return new_value.content; } return new_value; }; Metadata.normalizeValue = function (value) { if ((typeof value === 'string') || (isNaN(value) && typeof value === 'number')) { return value; } if (Array.isArray(value)) { return Metadata.normalizeArray(value); } if (typeof value === 'object') { return Metadata.normalizeObject(value); } }; function gidFormat(metadata, constraints) { var types, i, meta_key, result = {}, tmp; types = ['default', metadata.type]; for (i = 0; i < types.length; i += 1) { for (meta_key in constraints[types[i]]) { if (constraints[types[i]].hasOwnProperty(meta_key)) { tmp = metadata_actions[ constraints[types[i]][meta_key] ](metadata[meta_key]); if (tmp === undefined) { return; } result[meta_key] = tmp; } } } return JSON.stringify(result); } function gidToComplexQuery(gid, contraints) { var k, i, result = [], meta, content; if (typeof gid === 'string') { gid = JSON.parse(gid); } for (k in gid) { if (gid.hasOwnProperty(k)) { meta = gid[k]; if (!Array.isArray(meta)) { meta = [meta]; } for (i = 0; i < meta.length; i += 1) { content = meta[i]; if (typeof content === 'object') { content = content.content; } result[result.length] = { "type": "simple", "operator": "=", "key": k, "value": content }; } } } return { "type": "complex", "operator": "AND", "query_list": result }; } function gidParse(gid, constraints) { var object; try { object = JSON.parse(gid); } catch (e) { return; } if (gid !== gidFormat(object, constraints)) { return; } return object; } function gidStorage(spec, my) { var that = my.basicStorage(spec, my), priv = {}; priv.sub_storage = spec.sub_storage; priv.constraints = spec.constraints || { "default": { "identifier": "list", "type": "DCMIType" } }; that.specToStore = function () { return { "sub_storage": priv.sub_storage, "constraints": priv.constraints }; }; that.post = function (command) { setTimeout(function () { var gid, complex_query, doc = command.cloneDoc(); gid = gidFormat(doc, priv.constraints); if (gid === undefined || (doc._id && gid !== doc._id)) { return that.error({ "status": 409, "statusText": "Conflict", "error": "conflict", "message": "Cannot post document", "reason": "metadata should respect constraints" }); } complex_query = gidToComplexQuery(gid); that.addJob('allDocs', priv.sub_storage, {}, { "query": complex_query, "wildcard_character": null }, function (response) { var doc; if (response.total_rows !== 0) { return that.error({ "status": 409, "statusText": "Conflict", "error": "conflict", "message": "Cannot post document", "reason": "Document already exist" }); } doc = command.cloneDoc(); delete doc._id; that.addJob('post', priv.sub_storage, doc, { }, function (response) { response.id = gid; that.success(response); }, function (err) { err.message = "Cannot post document"; that.error(err); }); }, function (err) { err.message = "Cannot post document"; that.error(err); }); }); }; that.get = function (command) { setTimeout(function () { var gid_object, complex_query; gid_object = gidParse(command.cloneDoc()._id, priv.constraints); if (gid_object === undefined) { return that.error({ "status": 409, "statusText": "Conflict", "error": "conflict", "message": "Cannot get document", "reason": "metadata should respect constraints" }); } complex_query = gidToComplexQuery(gid_object); that.addJob('allDocs', priv.sub_storage, {}, { "query": complex_query, "wildcard_character": null, "include_docs": true }, function (response) { if (response.total_rows === 0) { return that.error({ "status": 404, "statusText": "Not Found", "error": "not_found", "message": "Cannot get document", "reason": "missing" }); } return that.success(response.rows[0].doc); }, function (err) { err.message = "Cannot get document"; return that.error(err); }); }); }; that.allDocs = function (command) { setTimeout(function () { var options = command.cloneOption(), include_docs; include_docs = options.include_docs; options.include_docs = true; that.addJob('allDocs', priv.sub_storage, { }, options, function (response) { var result = [], doc_gids = {}, i, row, gid; while ((row = response.rows.shift()) !== undefined) { if ((gid = gidFormat(row.doc, priv.constraints)) !== undefined) { if (!doc_gids[gid]) { doc_gids[gid] = true; row.id = gid; delete row.key; result[result.length] = row; if (include_docs === true) { row.doc._id = gid; } else { delete row.doc; } } } } doc_gids = undefined; // free memory row = undefined; that.success({"total_rows": result.length, "rows": result}); }, function (err) { err.message = "Cannot get all documents"; return that.error(err); }); }); }; return that; } jIO.addStorageType('gid', gidStorage); }());