From 5d3af8bd8f566b6b2aa5f0e8ce781d4ae2f1b93e Mon Sep 17 00:00:00 2001 From: Tristan Cavelier <tristan.cavelier@tiolive.com> Date: Fri, 31 May 2013 13:44:10 +0200 Subject: [PATCH] WIP: Great changes on complex queries Change to a module compatible with nodejs, requirejs, web workers and classic browsers. Provides classes Query, SimpleQuery, ComplexQuery. Documentation comming soon. --- src/queries/begin.js | 30 ++- src/queries/complexquery.js | 17 ++ src/queries/end.js | 21 +- src/queries/parser-begin.js | 12 +- src/queries/parser-end.js | 4 +- src/queries/query.js | 433 +++++++++++++++++++++--------------- src/queries/queryfactory.js | 22 ++ src/queries/serializer.js | 40 ++-- src/queries/simplequery.js | 138 ++++++++++++ src/queries/tool.js | 115 ++++++++++ 10 files changed, 617 insertions(+), 215 deletions(-) create mode 100644 src/queries/complexquery.js create mode 100644 src/queries/queryfactory.js create mode 100644 src/queries/simplequery.js create mode 100644 src/queries/tool.js diff --git a/src/queries/begin.js b/src/queries/begin.js index 7a60b32..c7bfd9a 100644 --- a/src/queries/begin.js +++ b/src/queries/begin.js @@ -4,11 +4,27 @@ * http://www.gnu.org/licenses/lgpl.html */ -(function (scope) { +/** + * Provides some function to use complex queries with item list + * + * @module complex_queries + */ +var complex_queries; +(function () { "use strict"; - Object.defineProperty(scope, "ComplexQueries", { - configurable: false, - enumerable: false, - writable: false, - value: {} - }); + var to_export = {}, module_name = "complex_queries"; + /** + * Add a secured (write permission denied) property to an object. + * @method defineProperty + * @param {Object} object The object to fill + * @param {String} key The object key where to store the property + * @param {Any} value The value to store + */ + function _export(key, value) { + Object.defineProperty(to_export, key, { + "configurable": false, + "enumerable": true, + "writable": false, + "value": value + }); + } diff --git a/src/queries/complexquery.js b/src/queries/complexquery.js new file mode 100644 index 0000000..c55d2e7 --- /dev/null +++ b/src/queries/complexquery.js @@ -0,0 +1,17 @@ +// XXX +var ComplexQuery = newClass(Query, function () { + + /** + * Filter the item list only if all the sub queries match this item according + * to the logical operator. + * See {{#crossLink "Query/exec:method"}}{{/crossLink}} + */ + this.exec = function () { + todo + }; +}); +todo + +query_class_dict.complex = ComplexQuery; + +_export("ComplexQuery", ComplexQuery); diff --git a/src/queries/end.js b/src/queries/end.js index 1a0684c..8ebe8eb 100644 --- a/src/queries/end.js +++ b/src/queries/end.js @@ -1,2 +1,21 @@ -}(jIO)); + if (typeof define === "function" && define.amd) { + define(to_export); + } else if (typeof window === "object") { + Object.defineProperty(window, module_name, { + configurable: false, + enumerable: true, + writable: false, + value: to_export + }); + } else if (typeof exports === "object") { + var i; + for (i in to_export) { + if (to_export.hasOwnProperty(i)) { + exports[i] = to_export[i]; + } + } + } else { + complex_queries = to_export; + } +}()); diff --git a/src/queries/parser-begin.js b/src/queries/parser-begin.js index 4593660..633aa38 100644 --- a/src/queries/parser-begin.js +++ b/src/queries/parser-begin.js @@ -1,5 +1,7 @@ -Object.defineProperty(scope.ComplexQueries, "parse", { - configurable: false, - enumerable: false, - writable: false, - value: function (string) { +/** + * Parse a text request to a json query tree + * @method parseStringToObject + * @param {String} string The string to parse + * @return {Object} The json query tree + */ +function parseStringToObject(string) { diff --git a/src/queries/parser-end.js b/src/queries/parser-end.js index 2876c3a..4698002 100644 --- a/src/queries/parser-end.js +++ b/src/queries/parser-end.js @@ -1,4 +1,4 @@ return result; - } +} -}); +}; // parseStringToQuery diff --git a/src/queries/query.js b/src/queries/query.js index 340242d..39e362c 100644 --- a/src/queries/query.js +++ b/src/queries/query.js @@ -1,186 +1,255 @@ -Object.defineProperty(scope.ComplexQueries,"query",{ - configurable:false,enumerable:false,writable:false, - value: function (query, object_list) { - var wildcard_character = typeof query.wildcard_character === 'string' ? - query.wildcard_character : '%', - operator_actions = { - '=': function (value1, value2) { - value1 = '' + value1; - return value1.match (convertToRegexp ( - value2, wildcard_character - )) || false && true; - }, - '!=': function (value1, value2) { - value1 = '' + value1; - return !(value1.match (convertToRegexp ( - value2, wildcard_character - ))); - }, - '<': function (value1, value2) { return value1 < value2; }, - '<=': function (value1, value2) { return value1 <= value2; }, - '>': function (value1, value2) { return value1 > value2; }, - '>=': function (value1, value2) { return value1 >= value2; }, - 'AND': function (item, query_list) { - var i; - for (i=0; i<query_list.length; ++i) { - if (! itemMatchesQuery (item, query_list[i])) { - return false; - } - } - return true; - }, - 'OR': function (item, query_list) { - var i; - for (i=0; i<query_list.length; ++i) { - if (itemMatchesQuery (item, query_list[i])) { - return true; - } - } - return false; - }, - 'NOT': function (item, query_list) { - return !itemMatchesQuery(item, query_list[0]); - } - }, - convertToRegexp = function (string) { - return subString('^' + string.replace( - new RegExp( - '([\\{\\}\\(\\)\\^\\$\\&\\.\\*\\?\\\/\\+\\|\\[\\]\\-\\\\])'. - replace (wildcard_character? - '\\'+wildcard_character:undefined,''), - 'g' - ), - '\\$1' - ) + '$',(wildcard_character||undefined), '.*'); - }, - subString = function (string, substring, newsubstring) { - var res = '', i = 0; - if (substring === undefined) { - return string; - } - while (1) { - var tmp = string.indexOf(substring,i); - if (tmp === -1) { - break; - } - for (; i < tmp; ++i) { - res += string[i]; - } - res += newsubstring; - i += substring.length; - } - for (; i<string.length; ++i) { - res += string[i]; - } - return res; - }, - itemMatchesQuery = function (item, query_object) { - var i; - if (query_object.type === 'complex') { - return operator_actions[query_object.operator]( - item, query_object.query_list - ); - } else { - if (query_object.id) { - if (typeof item[query_object.id] !== 'undefined') { - return operator_actions[query_object.operator]( - item[query_object.id], query_object.value - ); - } else { - return false; - } - } else { - return true; - } - } - }, - select = function (list, select_list) { - var i; - if (select_list.length === 0) { - return; - } - for (i=0; i<list.length; ++i) { - var list_value = {}, k; - for (k=0; k<select_list.length; ++k) { - list_value[select_list[k]] = - list[i][select_list[k]]; - } - list[i] = list_value; - } - }, - sortFunction = function (key, asc) { - if (asc === 'descending') { - return function (a,b) { - return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0; - }; - } - return function (a,b) { - return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0; - }; - }, - mergeList = function (list, list_to_merge, index) { - var i,j; - for (i = index,j = 0; i < list_to_merge.length + index; ++i, ++j) { - list[i] = list_to_merge[j]; - } - }, - sort = function (list, sort_list) { - var i, tmp, key, asc, sortAndMerge = function() { - sort(tmp,sort_list.slice(1)); - mergeList(list,tmp,i-tmp.length); - tmp = [list[i]]; - }; - if (list.length < 2) { - return; - } - if (sort_list.length === 0) { - return; - } - key = sort_list[0][0]; - asc = sort_list[0][1]; - list.sort (sortFunction (key,asc)); - tmp = [list[0]]; - for (i = 1; i < list.length; ++i) { - if (tmp[0][key] === list[i][key]) { - tmp.push(list[i]); - } else { - sortAndMerge(); - } - } - sortAndMerge(); - }, - limit = function (list, limit_list) { - var i; - if (typeof limit_list[0] !== 'undefined') { - if (typeof limit_list[1] !== 'undefined') { - if (list.length > limit_list[1] + limit_list[0]) { - list.length = limit_list[1] + limit_list[0]; - } - list.splice(0,limit_list[0]); - } else { - list.length = limit_list[0]; - } - } - }, - //////////////////////////////////////////////////////////// - result_list = [], result_list_tmp = [], j; - object_list = object_list || []; - if (query.query === undefined) { - result_list = object_list; - } else { - for (j=0; j<object_list.length; ++j) { - if ( itemMatchesQuery ( - object_list[j], scope.ComplexQueries.parse (query.query) - )) { - result_list.push(object_list[j]); - } +/** + * The query to use to filter a list of objects. + * This is an abstract class. + * + * @class Query + * @constructor + */ +var Query = newClass(function() { + /** + * Creates a new item list with matching item only + * + * @method exec + * @param {Array} item_list The list of object + * @param {Object} [option={}] Some operation option + * @param {String} [option.wildcard_character="%"] The wildcard character + * @return {Array} The new item list + */ + this.exec = function (item_list, option) {}; + + /** + * Test if an item matches this query + * @method match + * @param {Object} item The object to test + * @return {Boolean} true if match, false otherwise + */ + this.match = function (item, wildcard_character) {}; + + + /** + * Convert this query to a parsable string. + * @method toString + * @return {String} The string version of this query + */ + this.toString = function () {}; + + /** + * Convert this query to an jsonable object in order to be remake thanks to + * QueryFactory class. + * + * @method serialized + * @return {Object} The jsonable object + */ + this.serialized = function () {}; +}, {"static_methods": { + // XXX + "filterListSelect": function (select_option, list) { + list.forEach(function (item, index) { + var new_item = {}; + select_option.forEach(function (key) { + new_item[key] = item[key]; + }); + list[index] = new_item; + }); + }, + // XXX + "sortOn": function (sort_on_option, list) { + var sort_index; + for (sort_index = sort_on_option.length - 1; sort_index >= 0; + sort_index += 1) { + list.sort(sortFunction( + sort_on_option[sort_index][0], + sort_on_option[sort_index][1] + )); + } + }, + "parseStringToQuery": parseStringToQuery +}}); + +_export("Query", Query); + + +function query(query, object_list) { + var wildcard_character = typeof query.wildcard_character === "string" ? + query.wildcard_character : "%", + // A list of methods according to operators + operator_actions = { + "=": function (value1, value2) { + value1 = value1.toString(); + return value1.match(convertToRegexp( + value2, wildcard_character + )) || false && true; + }, + '!=': function (value1, value2) { + value1 = value1.toString(); + return !(value1.match(convertToRegexp( + value2, wildcard_character + ))); + }, + '<': function (value1, value2) { return value1 < value2; }, + '<=': function (value1, value2) { return value1 <= value2; }, + '>': function (value1, value2) { return value1 > value2; }, + '>=': function (value1, value2) { return value1 >= value2; }, + 'AND': function (item, query_list) { + var i; + for (i=0; i<query_list.length; ++i) { + if (! itemMatchesQuery (item, query_list[i])) { + return false; } } - if (query.filter) { - select(result_list,query.filter.select_list || []); - sort(result_list,query.filter.sort_on || []); - limit(result_list,query.filter.limit || []); + return true; + }, + 'OR': function (item, query_list) { + var i; + for (i=0; i<query_list.length; ++i) { + if (itemMatchesQuery (item, query_list[i])) { + return true; + } + } + return false; + }, + 'NOT': function (item, query_list) { + return !itemMatchesQuery(item, query_list[0]); + } + }, + convertToRegexp = function (string) { + return subString('^' + string.replace( + new RegExp( + '([\\{\\}\\(\\)\\^\\$\\&\\.\\*\\?\\\/\\+\\|\\[\\]\\-\\\\])'. + replace(wildcard_character ? + "\\" + wildcard_character : undefined, ""), + "g" + ), + "\\$1" + ) + '$',(wildcard_character||undefined), '.*'); + }, + subString = function (string, substring, newsubstring) { + var res = '', i = 0; + if (substring === undefined) { + return string; + } + while (true) { + var tmp = string.indexOf(substring, i); + if (tmp === -1) { + break; + } + for (; i < tmp; i += 1) { + res += string[i]; } - return result_list; + res += newsubstring; + i += substring.length; + } + for (; i < string.length; i += 1) { + res += string[i]; + } + return res; + }, + itemMatchesQuery = function (item, query_object) { + var i; + if (query_object.type === 'complex') { + return operator_actions[query_object.operator]( + item, query_object.query_list + ); + } else { + if (query_object.id) { + if (typeof item[query_object.id] !== 'undefined') { + return operator_actions[query_object.operator]( + item[query_object.id], query_object.value + ); + } else { + return false; + } + } else { + return true; + } + } + }, + select = function (list, select_list) { + var i; + if (select_list.length === 0) { + return; + } + for (i=0; i<list.length; ++i) { + var list_value = {}, k; + for (k=0; k<select_list.length; ++k) { + list_value[select_list[k]] = + list[i][select_list[k]]; + } + list[i] = list_value; + } + }, + sortFunction = function (key, asc) { + if (asc === 'descending') { + return function (a,b) { + return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0; + }; + } + return function (a,b) { + return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0; + }; + }, + mergeList = function (list, list_to_merge, index) { + var i,j; + for (i = index,j = 0; i < list_to_merge.length + index; ++i, ++j) { + list[i] = list_to_merge[j]; + } + }, + sort = function (list, sort_list) { + var i, tmp, key, asc, sortAndMerge = function() { + sort(tmp,sort_list.slice(1)); + mergeList(list,tmp,i-tmp.length); + tmp = [list[i]]; + }; + if (list.length < 2) { + return; + } + if (sort_list.length === 0) { + return; + } + key = sort_list[0][0]; + asc = sort_list[0][1]; + list.sort (sortFunction (key,asc)); + tmp = [list[0]]; + for (i = 1; i < list.length; ++i) { + if (tmp[0][key] === list[i][key]) { + tmp.push(list[i]); + } else { + sortAndMerge(); + } + } + sortAndMerge(); + }, + limit = function (list, limit_list) { + var i; + if (typeof limit_list[0] !== 'undefined') { + if (typeof limit_list[1] !== 'undefined') { + if (list.length > limit_list[1] + limit_list[0]) { + list.length = limit_list[1] + limit_list[0]; + } + list.splice(0,limit_list[0]); + } else { + list.length = limit_list[0]; + } + } + }, + //////////////////////////////////////////////////////////// + result_list = [], result_list_tmp = [], j; + object_list = object_list || []; + if (query.query === undefined) { + result_list = object_list; + } else { + for (j=0; j<object_list.length; ++j) { + if ( itemMatchesQuery ( + object_list[j], scope.ComplexQueries.parse (query.query) + )) { + result_list.push(object_list[j]); + } } -}); + } + if (query.filter) { + select(result_list,query.filter.select_list || []); + sort(result_list,query.filter.sort_on || []); + limit(result_list,query.filter.limit || []); + } + return result_list; +} diff --git a/src/queries/queryfactory.js b/src/queries/queryfactory.js new file mode 100644 index 0000000..18584f1 --- /dev/null +++ b/src/queries/queryfactory.js @@ -0,0 +1,22 @@ +/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ +/*global _export: true, ComplexQuery: true, SimpleQuery: true, + newClass: true, + sortFunction: true, convertSearchTextToRegExp: true */ + +// XXX +var query_class_dict = {}, query_factory = {}; + +newClass.apply(query_factory, [{ + "secure_methods": true +}, function () { + // XXX + this.create = function (object) { + if (typeof object.type === "string" && + query_class_dict[object.type]) { + return new query_class_dict[object.type](object); + } + return null; + }; +}]); // end QueryFactory + +_export("factory", query_factory); diff --git a/src/queries/serializer.js b/src/queries/serializer.js index 2b190d7..fceb3ff 100644 --- a/src/queries/serializer.js +++ b/src/queries/serializer.js @@ -1,18 +1,22 @@ -Object.defineProperty(scope.ComplexQueries,"serialize",{ - configurable:false,enumerable:false,writable:false,value:function(query){ - var str_list = [], i; - if (query.type === 'complex') { - str_list.push ( '(' ); - for (i=0; i<query.query_list.length; ++i) { - str_list.push( scope.ComplexQueries.serialize(query.query_list[i]) ); - str_list.push( query.operator ); - } - str_list.length --; - str_list.push ( ')' ); - return str_list.join(' '); - } else if (query.type === 'simple') { - return query.id + (query.id?': ':'') + query.operator + ' "' + query.value + '"'; - } - return query; - } -}); +/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ +/*global _export: true, to_export: true */ + +function objectToSearchText(query) { + var str_list = []; + if (query.type === "complex") { + str_list.push("("); + (query.query_list || []).forEach(function (sub_query) { + str_list.push(objectToSearchText(sub_query)); + str_list.push(query.operator); + }); + str_list.length -= 1; + str_list.push(")"); + return str_list.join(" "); + } + if (query.type === "simple") { + return query.id + (query.id ? ": " : "") + (query.operator || "=") + ' "' + + query.value + '"'; + } + throw new TypeError("This object is not a query"); +} +_export("objectToSearchText", objectToSearchText); diff --git a/src/queries/simplequery.js b/src/queries/simplequery.js new file mode 100644 index 0000000..bc9035a --- /dev/null +++ b/src/queries/simplequery.js @@ -0,0 +1,138 @@ +/** + * The SimpleQuery inherits from Query, and compares one metadata value + * + * @class SimpleQuery + * @param {Object} [spec={}] The specifications + * @param {String} [spec.operator="="] The compare method to use + * @param {String} spec.key The metadata key + * @param {String} spec.value The value of the metadata to compare + * @param {String} [spec.wildcard_character="%"] The wildcard character + */ +var SimpleQuery = newClass(Query, function (spec) { + /** + * Operator to use to compare object values + * + * @property operator + * @type String + * @default "=" + */ + this.operator = spec.operator || "="; + + /** + * Key of the object which refers to the value to compare + * + * @property key + * @type String + */ + this.key = spec.key; + + /** + * Value is used to do the comparison with the object value + * + * @property value + * @type String + */ + this.value = spec.value; + + /** + * The wildcard character used to extend comparison action + * + * @property wildcard_character + * @type String + */ + this.wildcard_character = spec.wildcard_character || "%"; + + /** + * #crossLink "Query/exec:method" + */ + this.exec = function (item_list, option) { + var new_item_list = []; + item_list.forEach(function (item) { + if (!this.match(item, option.wildcard_character)) { + new_item_list.push(item); + } + }); + if (option.sort_on) { + Query.sortOn(option.sort_on, new_item_list); + } + if (option.limit) { + new_item_list = new_item_list.slice( + option.limit[0], + option.limit[1] + option.limit[0] + 1 + ); + } + if (option.select_list) { + Query.filterListSelect(option.select_list, new_item_list); + } + return new_item_list; + }; + + /** + * #crossLink "Query/match:method" + */ + this.match = function (item, wildcard_character) { + this[this.operator](item[this.key], this.value, wildcard_character); + }; + + /** + * #crossLink "Query/toString:method" + */ + this.toString = function () { + return (this.key ? this.key + ": " : "") + (this.operator || "=") + ' "' + + this.value + '"'; + }; + + /** + * #crossLink "Query/serialized:method" + */ + this.serialized = function () { + return { + "type": "simple", + "operator": this.operator, + "key": this.key, + "value": this.value + }; + }; + + // XXX + this["="] = function (object_value, comparison_value, + wildcard_character) { + return convertSearchTextToRegExp( + comparison_value.toString(), + wildcard_character || this.wildcard_character + ).test(object_value.toString()); + }; + + // XXX + this["!="] = function (object_value, comparison_value, + wildcard_character) { + return !convertSearchTextToRegExp( + comparison_value.toString(), + wildcard_character || this.wildcard_character + ).test(object_value.toString()); + }; + + // XXX + this["<"] = function (object_value, comparison_value) { + return object_value < comparison_value; + }; + + // XXX + this["<="] = function (object_value, comparison_value) { + return object_value <= comparison_value; + }; + + // XXX + this[">"] = function (object_value, comparison_value) { + return object_value > comparison_value; + }; + + // XXX + this[">="] = function (object_value, comparison_value) { + return object_value >= comparison_value; + }; +}); + +query_class_dict.simple = SimpleQuery; + +_export("SimpleQuery", SimpleQuery); diff --git a/src/queries/tool.js b/src/queries/tool.js new file mode 100644 index 0000000..aace7ca --- /dev/null +++ b/src/queries/tool.js @@ -0,0 +1,115 @@ +/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ +/*global _export: true */ + +#!/usr/bin/env node + +// keywords: js, javascript, new class creator, generator + +/** + * Create a class, manage inheritance, static methods, + * protected attributes and can hide methods or/and secure methods + * + * @method newClass + * @param {Class} Class Classes to inherit from (0..n) + * @param {Object} option Class option (0..n) + * @param {Boolean} [option.secure_methods=false] Make methods not configurable + * and not writable + * @param {Boolean} [option.hide_methods=false] Make methods not enumerable + * @param {Object} [option.static_methods={}] Add static methods + * @param {Function} constructor The new class constructor + * @return {Class} The new class + */ +function newClass() { + var j, k, constructors = [], option, new_class; + + for (j = 0; j < arguments.length; j += 1) { + if (typeof arguments[j] === "function") { + constructors.push(arguments[j]); + } else if (typeof arguments[j] === "object") { + option = option || {}; + for (k in arguments[j]) { + if (arguments[j].hasOwnProperty(k)) { + option[j] = arguments[j][k]; + } + } + } + } + + function postCreate(that) { + // modify the object according to 'option' + var key; + if (option) { + for (key in that) { + if (that.hasOwnProperty(key)) { + if (typeof that[key] === "function") { + Object.defineProperty(that, key, { + configurable: option.secure_methods ? false : true, + enumerable: option.hide_methods ? false : true, + writable: option.secure_methods ? false : true, + value: that[key] + }); + } + } + } + } + } + + new_class = function (spec, my) { + var i; + spec = spec || {}; + my = my || {}; + // don't use forEach ! + for (i = 0; i < constructors.length; i += 1) { + constructors[i].apply(this, [spec, my]); + } + postCreate(this); + return this; + }; + for (j in (option.static_methods || {})) { + if (option.static_methods.hasOwnProperty(j)) { + new_class[j] = option.static_methods[j]; + } + } + postCreate(new_class); + return new_class; +} + +/** + * Escapes regexp special chars from a string. + * @method regexpEscapeString + * @param {String} string The string to escape + * @return {String} The escaped string + */ +function regexpEscapeString(string) { + if (typeof string === "string") { + return string.replace(/([\\\.\$\[\]\(\)\{\}\^\?\*\+\-])/g, "\\$1"); + } +} +_export("regexpEscapeString", regexpEscapeString); + +/** + * Convert a search text to a regexp. + * @method convertSearchTextToRegExp + * @param {String} string The string to convert + * @param {String} [wildcard_character=undefined] The wildcard chararter + * @return {RegExp} The search text regexp + */ +function convertSearchTextToRegExp(string, wildcard_character) { + return new RegExp("^" + regexpEscapeString(string).replace( + regexpEscapeString(wildcard_character), + '.*' + ) + "$"); +} +_export("convertSearchTextToRegExp", convertSearchTextToRegExp); + +// XXX +function sortFunction(key, way) { + if (way === 'descending') { + return function (a,b) { + return a[key] < b[key] ? 1 : a[key] > b[key] ? -1 : 0; + }; + } + return function (a,b) { + return a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0; + }; +} -- 2.30.9