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">{&quot;type&quot;:&quot;complex&quot;,&quot;operator&quot;:&quot;AND&quot;,&quot;query_list&quot;:[{&quot;type&quot;:&quot;simple&quot;,&quot;operator&quot;:&quot;=&quot;,&quot;id&quot;:&quot;1&quot;,&quot;value&quot;:&quot;abc&quot;},{&quot;type&quot;:&quot;simple&quot;,&quot;operator&quot;:&quot;=&quot;,&quot;id&quot;:&quot;2&quot;,&quot;value&quot;:&quot;def&quot;}]}</textarea></td>
+    </tr>
+    <tr>
+      <td>Object List:<br /><textarea id="list">[{&quot;1&quot;:&quot;abc&quot;,&quot;2&quot;:&quot;def&quot;},{&quot;1&quot;:&quot;def&quot;,&quot;2&quot;:&quot;abc&quot;}]</textarea></td>
+      <td>Result (Query String):<br /><textarea id="result">[{&quot;1&quot;:&quot;abc&quot;,&quot;2&quot;:&quot;def&quot;}]</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="[[&quot;1&quot;,&quot;ascending&quot;],[&quot;2&quot;,&quot;descending&quot;]]" /></td>
+    </tr>
+    <tr>
+      <td><label for="select_list">Select_list: </label></td>
+      <td><input type="text" id="select_list" name="select_list" value="[&quot;1&quot;,&quot;2&quot;]" /></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