From 02e546dd8811a0462e25d69f00b34ff4330d59ee Mon Sep 17 00:00:00 2001 From: Tristan Cavelier <tristan.cavelier@tiolive.com> Date: Fri, 16 Nov 2012 11:34:26 +0100 Subject: [PATCH] Add complex queries to jIO --- examples/example-queries.html | 74 ++++++++++++ grunt/15_gruntQueries/Makefile | 21 ++++ grunt/15_gruntQueries/grunt.js | 85 ++++++++++++++ grunt/15_gruntQueries/package.json | 23 ++++ src/queries/Makefile | 7 ++ src/queries/begin.js | 5 + src/queries/end.js | 2 + src/queries/parser-begin.js | 2 + src/queries/parser-end.js | 4 + src/queries/parser.par | 111 ++++++++++++++++++ src/queries/query.js | 182 +++++++++++++++++++++++++++++ src/queries/serializer.js | 18 +++ 12 files changed, 534 insertions(+) create mode 100644 examples/example-queries.html create mode 100644 grunt/15_gruntQueries/Makefile create mode 100644 grunt/15_gruntQueries/grunt.js create mode 100644 grunt/15_gruntQueries/package.json create mode 100644 src/queries/Makefile create mode 100644 src/queries/begin.js create mode 100644 src/queries/end.js create mode 100644 src/queries/parser-begin.js create mode 100644 src/queries/parser-end.js create mode 100644 src/queries/parser.par create mode 100644 src/queries/query.js create mode 100644 src/queries/serializer.js diff --git a/examples/example-queries.html b/examples/example-queries.html new file mode 100644 index 0000000..18b1bd3 --- /dev/null +++ b/examples/example-queries.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <title>Complex Queries Example</title> + <style type="text/css" media="screen"> + table, textarea, input { + width: 100%; + } + textarea { + height: 10em; + } + </style> +</head> +<body> + <table> + <tr> + <td>Query (String):<br /><textarea id="str">1:abc AND 2:def</textarea></td> + <td>Query (Object):<br /><textarea id="obj">{"type":"complex","operator":"AND","query_list":[{"type":"simple","operator":"=","id":"1","value":"abc"},{"type":"simple","operator":"=","id":"2","value":"def"}]}</textarea></td> + </tr> + <tr> + <td>Object List:<br /><textarea id="list">[{"1":"abc","2":"def"},{"1":"def","2":"abc"}]</textarea></td> + <td>Result (Query String):<br /><textarea id="result">[{"1":"abc","2":"def"}]</textarea></td> + </tr> + <tr> + <td><label for="wildcard">Wildcard char: </label></td> + <td><input type="text" id="wildcard" name="wildcard" value="%" /></td> + </tr> + <tr> + <td><label for="sort_on">Sort on: </label></td> + <td><input type="text" id="sort_on" name="sort_on" value="[["1","ascending"],["2","descending"]]" /></td> + </tr> + <tr> + <td><label for="select_list">Select_list: </label></td> + <td><input type="text" id="select_list" name="select_list" value="["1","2"]" /></td> + </tr> + <tr> + <td><label for="limit">Limit: </label></td> + <td><input type="text" id="limit" name="limit" value="[0,100]" /></td> + </tr> + </table> + <button onclick="parse()">Parse</button> + <button onclick="serialize()">Serialize</button> + <button onclick="query()">Query</button> + <script type="text/javascript" src="../jio.min.js"></script> + <script type="text/javascript" src="../complex-queries.min.js"></script> + <script type="text/javascript" src="http://code.jquery.com/jquery-1.8.2.min.js"></script> + <script type="text/javascript"> + <!-- +var parse = function () { + $('#obj').attr('value',JSON.stringify(jIO.ComplexQueries.parse($('#str').attr('value')))); +}; +var serialize = function () { + $('#str').attr('value',jIO.ComplexQueries.serialize(JSON.parse($('#obj').attr('value')))); +}; +var query = function () { + $('#result').attr('value',JSON.stringify( + jIO.ComplexQueries.query( + { + query:$('#str').attr('value'), + filter:{ + sort_on:JSON.parse($('#sort_on').attr('value')), + limit:JSON.parse($('#limit').attr('value')), + select_list:JSON.parse($('#select_list').attr('value')) + }, + wildcard_character:$('#wildcard').attr('value') + }, JSON.parse($('#list').attr('value')) + ) + )); +}; + // --> + </script> +</body> +</html> diff --git a/grunt/15_gruntQueries/Makefile b/grunt/15_gruntQueries/Makefile new file mode 100644 index 0000000..05c5dae --- /dev/null +++ b/grunt/15_gruntQueries/Makefile @@ -0,0 +1,21 @@ +JSCCDIR = /opt/jscc +JSCCCMD = rhino $(JSCCDIR)/jscc.js -t $(JSCCDIR)/driver_web.js_ + +SRC_DIR = ../../src/queries +BUILD_DIR = ../../built/queries + +OUT = $(BUILD_DIR)/complex-queries.js + +PARSER_PAR = $(SRC_DIR)/parser.par +PARSER_OUT = $(BUILD_DIR)/parser.js + +auto: prepare parser grunt + +prepare: + mkdir -p $(BUILD_DIR) + +parser: + $(JSCCCMD) $(PARSER_PAR) >> $(PARSER_OUT) + +grunt: + grunt diff --git a/grunt/15_gruntQueries/grunt.js b/grunt/15_gruntQueries/grunt.js new file mode 100644 index 0000000..75f3b39 --- /dev/null +++ b/grunt/15_gruntQueries/grunt.js @@ -0,0 +1,85 @@ +/*global module:false*/ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: '<json:package.json>', + meta: { + banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - '+ + '<%= grunt.template.today("yyyy-mm-dd") %>\n' + + '<%= pkg.homepage ? "* " + pkg.homepage + "\n" : "" %>' + + '* Copyright (c) <%= grunt.template.today("yyyy") %> Nexedi;' + + ' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */' + }, + concat: { + dist: { + src: ['<banner:meta.banner>', + // Wrapper top + '<file_strip_banner:../../src/queries/begin.js>', + // code + '<file_strip_banner:../../src/queries/parser-begin.js>', + '<file_strip_banner:../../built/queries/parser.js>', + '<file_strip_banner:../../src/queries/parser-end.js>', + '<file_strip_banner:../../src/queries/serializer.js>', + '<file_strip_banner:../../src/queries/query.js>', + // Wrapper bottom + '<file_strip_banner:../../src/queries/end.js>'], + dest: '../../<%= pkg.name %>.js' + } + }, + min: { + dist: { + src: ['<banner:meta.banner>', '<config:concat.dist.dest>'], + dest: '../../<%= pkg.name %>.min.js' + } + }, + qunit: { + files: ['../../test/cq-tests.html'] + }, + lint: { + files: ['grunt.js', + '../../src/queries/serializer.js', + '../../src/queries/query.js'] + }, + watch: { + files: '<config:lint.files>', + tasks: 'lint qunit' + }, + jshint: { + options: { + curly: true, + eqeqeq: true, + immed: true, + latedef: true, + newcap: true, + noarg: true, + sub: true, + undef: true, + boss: true, + eqnull: true, + browser: true + }, + globals: { + scope: true, + console: true, + unescape: true, + // Needed to avoid "not defined error" with requireJs + define: true, + require: true, + // Needed to avoid "not defined error" with sinonJs + sinon: true, + module: true, + test: true, + ok: true, + deepEqual: true, + expect: true, + stop: true, + start: true, + equal: true + } + }, + uglify: {} + }); + // Default task. + grunt.registerTask('default', 'lint concat min'); +}; diff --git a/grunt/15_gruntQueries/package.json b/grunt/15_gruntQueries/package.json new file mode 100644 index 0000000..beff80d --- /dev/null +++ b/grunt/15_gruntQueries/package.json @@ -0,0 +1,23 @@ +{ + "name": "complex-queries", + "title": "Complex Queries", + "description": "Complex Queries", + "version": "0.1.0", + "homepage": "", + "author": { + "name": "Tristan Cavelier", + "email": "tristan.cavelier@tiolive.com" + }, + "repository": { + "type": "git", + "url": "http://git.erp5.org/repos/jio.git" + }, + "bugs": { + "url": "" + }, + "licenses": [ + ], + "dependencies": { + }, + "keywords": [] +} diff --git a/src/queries/Makefile b/src/queries/Makefile new file mode 100644 index 0000000..6e1a88c --- /dev/null +++ b/src/queries/Makefile @@ -0,0 +1,7 @@ +auto: parser grunt + +grunt: + make -C ../../grunt/*_gruntQueries + +clean: + rm -f ./*~ diff --git a/src/queries/begin.js b/src/queries/begin.js new file mode 100644 index 0000000..591756a --- /dev/null +++ b/src/queries/begin.js @@ -0,0 +1,5 @@ +(function(scope){ + "use strict"; + Object.defineProperty(scope,"ComplexQueries",{ + configurable:false,enumerable:false,writable:false,value:{} + }); diff --git a/src/queries/end.js b/src/queries/end.js new file mode 100644 index 0000000..1a0684c --- /dev/null +++ b/src/queries/end.js @@ -0,0 +1,2 @@ + +}(jIO)); diff --git a/src/queries/parser-begin.js b/src/queries/parser-begin.js new file mode 100644 index 0000000..7f0d845 --- /dev/null +++ b/src/queries/parser-begin.js @@ -0,0 +1,2 @@ +Object.defineProperty(scope.ComplexQueries,"parse",{ + configurable:false,enumerable:false,writable:false,value:function(string){ diff --git a/src/queries/parser-end.js b/src/queries/parser-end.js new file mode 100644 index 0000000..b54ac61 --- /dev/null +++ b/src/queries/parser-end.js @@ -0,0 +1,4 @@ + return result; +} + +}); diff --git a/src/queries/parser.par b/src/queries/parser.par new file mode 100644 index 0000000..1929986 --- /dev/null +++ b/src/queries/parser.par @@ -0,0 +1,111 @@ +/~ Token definitions ~/ + +! ' |\t' ; + +' |\t' WHITESPACE ; +'\(' LEFT_PARENTHESE ; +'\)' RIGHT_PARENTHESE ; +'AND' AND ; +'OR' OR ; +'NOT' NOT ; +'[^><= :\(\)"][^ :\(\)"]*:' COLUMN ; +'"(\\.|[^\\"])*"' STRING ; +'[^><= :\(\)"][^ :\(\)"]*' WORD ; +'(>=?|<=?|!?=)' OPERATOR ; + +## + +/~ Grammar specification ~/ + +begin: search_text [* result = %1; *]; + +search_text + : and_expression [* %% = %1; *] + | and_expression search_text [* %% = mkComplexQuery('OR',[%1,%2]); *] + | and_expression OR search_text [* %% = mkComplexQuery('OR',[%1,%3]); *] + ; + +and_expression + : boolean_expression [* %% = %1 ; *] + | boolean_expression AND and_expression [* %% = mkComplexQuery('AND',[%1,%3]); *] + ; + +boolean_expression + : NOT expression [* %% = mkNotQuery(%2); *] + | expression [* %% = %1; *] + ; + +expression + : LEFT_PARENTHESE search_text RIGHT_PARENTHESE [* %% = %2; *] + | COLUMN expression [* simpleQuerySetId(%2,%1.split(':').slice(0,-1).join(':')); %% = %2; *] + | value [* %% = %1; *] + ; + +value + : OPERATOR string [* %2.operator = %1 ; %% = %2; *] + | string [* %% = %1; *] + ; + +string + : WORD [* %% = mkSimpleQuery('',%1); *] + | STRING [* %% = mkSimpleQuery('',%1.split('"').slice(1,-1).join('"')); *] + ; + +[* +var arrayExtend = function () { + var j,i,newlist=[],listoflists = arguments; + for (j=0; j<listoflists.length; ++j) { + for (i=0; i<listoflists[j].length; ++i) { + newlist.push(listoflists[j][i]); + } + } + return newlist; +}; +var mkSimpleQuery = function (id,value,operator) { + return {type:'simple',operator:'=',id:id,value:value}; +}; +var mkNotQuery = function (query) { + if (query.operator === 'NOT') { + return query.query_list[0]; + } + return {type:'complex',operator:'NOT',query_list:[query]}; +}; +var mkComplexQuery = function (operator,query_list) { + var i,query_list2 = []; + for (i=0; i<query_list.length; ++i) { + if (query_list[i].operator === operator) { + query_list2 = arrayExtend(query_list2,query_list[i].query_list); + } else { + query_list2.push(query_list[i]); + } + } + return {type:'complex',operator:operator,query_list:query_list2}; +}; +var simpleQuerySetId = function (query, id) { + var i; + if (query.type === 'complex') { + for (i = 0; i < query.query_list.length; ++i) { + simpleQuerySetId (query.query_list[i],id); + } + return true; + } + if (query.type === 'simple' && !query.id) { + query.id = id; + return true; + } + return false; +}; +var error_offsets = []; +var error_lookaheads = []; +var error_count = 0; +var result; +if ( ( error_count = __##PREFIX##parse( string, error_offsets, error_lookaheads ) ) > 0 ) { + var i; + for (i = 0; i < error_count; ++i) { + throw new Error ( "Parse error near \"" + + string.substr ( error_offsets[i] ) + + "\", expecting \"" + + error_lookaheads[i].join() + "\"" ); + } +} +*] diff --git a/src/queries/query.js b/src/queries/query.js new file mode 100644 index 0000000..5f54bb9 --- /dev/null +++ b/src/queries/query.js @@ -0,0 +1,182 @@ +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 || []; + 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/serializer.js b/src/queries/serializer.js new file mode 100644 index 0000000..2b190d7 --- /dev/null +++ b/src/queries/serializer.js @@ -0,0 +1,18 @@ +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; + } +}); -- 2.30.9