Add LICENSE
[cql-js-moved-to-github.git] / cql.js
diff --git a/cql.js b/cql.js
index ebd5e76..37035d3 100644 (file)
--- a/cql.js
+++ b/cql.js
@@ -1,10 +1,14 @@
-function indent(n) {
+var EXPORTED_SYMBOLS = ["CQLParser"];
+
+var DEFAULT_SERVER_CHOICE_FIELD = 'cql.serverChoice';
+var DEFAULT_SERVER_CHOICE_RELATION = 'scr';
+
+function indent(n, c) {
     var s = "";
     for (var i = 0; i < n; i++)
-        s = s + " ";
+        s = s + c;
     return s;
 }
-
 // CQLModifier
 var CQLModifier = function () {
     this.name = null;
@@ -13,60 +17,133 @@ var CQLModifier = function () {
 }
 
 CQLModifier.prototype = {
-    toXCQL: function (n) {
-        var s = indent(n+1) + "<modifier>\n";
-        s = s + indent(n+2) + "<name>" + this.name + "</name>\n";
+    toString: function () {
+      return this.name + this.relation + this.value;
+    },
+
+    toXCQL: function (n, c) {
+        var s = indent(n+1, c) + "<modifier>\n";
+        s = s + indent(n+2, c) + "<name>" + this.name + "</name>\n";
         if (this.relation != null)
-            s = s + indent(n+2) 
+            s = s + indent(n+2, c) 
                 + "<relation>" + this.relation + "</relation>\n";
         if (this.value != null)
-            s = s + indent(n+2) 
+            s = s + indent(n+2, c) 
                 + "<value>" + this.value +"</value>\n";
-        s = s + indent(n+1) + "</modifier>\n";
+        s = s + indent(n+1, c) + "</modifier>\n";
+        return s;
+    },
+
+    toFQ: function () {
+        //we ignore modifier relation symbol, for value-less modifiers
+        //we assume 'true'
+        var value = this.value.length > 0 ? this.value : "true";
+        var s = '"'+this.name+'": "'+value+'"';
         return s;
     }
 }
+
 // CQLSearchClause
 var CQLSearchClause = function (field, fielduri, relation, relationuri, 
-                                modifiers, term) {
+                                modifiers, term, scf, scr) {
     this.field = field;
     this.fielduri = fielduri;
     this.relation = relation;
     this.relationuri = relationuri;
     this.modifiers = modifiers;
     this.term = term;
+    this.scf = scf || DEFAULT_SERVER_CHOICE_FIELD;
+    this.scr = scr || DEFAULT_SERVER_CHOICE_RELATION;
 }
 
 CQLSearchClause.prototype = {
-    toXCQL: function (n) {
-        var s = indent(n) + "<searchClause>\n";
+    toString: function () {
+      var field = this.field;
+      var relation = this.relation;
+      if (field == this.scf && relation == this.scr) {
+        //avoid redundant field/relation
+        field = null;
+        relation = null;
+      }
+      return (field ? field + ' ' : '') + 
+        (relation ? relation : '') +
+        (this.modifiers.length > 0 ? '/' + this.modifiers.join('/') : '') +
+        (relation || this.modifiers.length ? ' ' : '') +
+        '"' + this.term + '"';
+    },
+
+    toXCQL: function (n, c) {
+        var s = indent(n, c) + "<searchClause>\n";
         if (this.fielduri.length > 0)
         {
-            s = s + indent(n+1) + "<prefixes>\n" +
-                indent(n+2) + "<prefix>\n" +
-                indent(n+3) + "<identifier>" + this.fielduri +
+            s = s + indent(n+1, c) + "<prefixes>\n" +
+                indent(n+2, c) + "<prefix>\n" +
+                indent(n+3, c) + "<identifier>" + this.fielduri +
                 "</identifier>\n" +
-                indent(n+2) + "</prefix>\n" +
-                indent(n+1) + "</prefixes>\n";
+                indent(n+2, c) + "</prefix>\n" +
+                indent(n+1, c) + "</prefixes>\n";
         }
-        s = s + indent(n+1) + "<index>" + this.field + "</index>\n";
-        s = s + indent(n+1) + "<relation>\n";
+        s = s + indent(n+1, c) + "<index>" + this.field + "</index>\n";
+        s = s + indent(n+1, c) + "<relation>\n";
         if (this.relationuri.length > 0) {
-            s = s + indent(n+2) +
+            s = s + indent(n+2, c) +
                 "<identifier>" + this.relationuri + "</identifier>\n";
         }
-        s = s + indent(n+2) + "<value>" + this.relation + "</value>\n";
+        s = s + indent(n+2, c) + "<value>" + this.relation + "</value>\n";
         if (this.modifiers.length > 0) {
-            s = s + indent(n+2) + "<modifiers>\n";
+            s = s + indent(n+2, c) + "<modifiers>\n";
             for (var i = 0; i < this.modifiers.length; i++)
-                s = s + this.modifiers[i].toXCQL(n+2);
-            s = s + indent(n+2) + "</modifiers>\n";
+                s = s + this.modifiers[i].toXCQL(n+2, c);
+            s = s + indent(n+2, c) + "</modifiers>\n";
+        }
+        s = s + indent(n+1, c) + "</relation>\n";
+        s = s + indent(n+1, c) + "<term>" + this.term + "</term>\n";
+        s = s + indent(n, c) + "</searchClause>\n";
+        return s;
+    },
+
+    toFQ: function () {
+        var s = '{"term": "'+this.term+'"';
+        if (this.field.length > 0 && this.field != this.scf)
+          s+= ', "field": "'+this.field+'"';
+        if (this.relation.length > 0 && this.relation != this.scr)
+          s+= ', "relation": "'+this._mapRelation(this.relation)+'"';
+        for (var i = 0; i < this.modifiers.length; i++) {
+          //since modifiers are mapped to keys, ignore the reserved ones
+          if (this.modifiers[i].name == "term"
+            ||this.modifiers[i].name == "field"
+            ||this.modifiers[i].name == "relation")
+            continue;
+          s += ', ' + this.modifiers[i].toFQ();
         }
-        s = s + indent(n+1) + "</relation>\n";
-        s = s + indent(n+1) + "<term>" + this.term + "</term>\n";
-        s = s + indent(n) + "</searchClause>\n";
+        s += '}';
         return s;
+    },
+
+    _mapRelation: function (rel) {
+      switch(rel) {
+        case "<" : return "lt";
+        case ">" : return "gt";
+        case "=" : return "eq";
+        case "<>" : return "ne";
+        case ">=" : return "ge";
+        case "<=" : return "le";
+        default: return rel;
+      }
+    },
+
+    _remapRelation: function (rel) {
+      switch(rel) {
+        case "lt" : return "<";
+        case "gt" : return ">";
+        case "eq" : return "=";
+        case "ne" : return "<>";
+        case "ge" : return ">=";
+        case "le" : return "<=";
+        default: return rel;
+      }
     }
+
 }
 // CQLBoolean
 var CQLBoolean = function() {
@@ -77,25 +154,44 @@ var CQLBoolean = function() {
 }
 
 CQLBoolean.prototype = {
-    toXCQL: function (n) {
-        var s = indent(n) + "<triple>\n";
-        s = s + indent(n+1) + "<boolean>\n" +
-            indent(n+2) + "<value>" + this.op + "</value>\n";
+    toString: function () {
+      return (this.left.op ? '(' + this.left + ')' : this.left) + ' ' + 
+        this.op.toUpperCase() +
+        (this.modifiers.length > 0 ? '/' + this.modifiers.join('/') : '') + 
+        ' ' + (this.right.op ? '(' + this.right + ')' : this.right);;
+    },
+    toXCQL: function (n, c) {
+        var s = indent(n, c) + "<triple>\n";
+        s = s + indent(n+1, c) + "<boolean>\n" +
+            indent(n+2, c) + "<value>" + this.op + "</value>\n";
         if (this.modifiers.length > 0) {
-            s = s + indent(n+2) + "<modifiers>\n";
+            s = s + indent(n+2, c) + "<modifiers>\n";
             for (var i = 0; i < this.modifiers.length; i++)
-                s = s + this.modifiers[i].toXCQL(n+2);
-            s = s + indent(n+2) + "</modifiers>\n";
+                s = s + this.modifiers[i].toXCQL(n+2, c);
+            s = s + indent(n+2, c) + "</modifiers>\n";
         }
-        s = s + indent(n+1) + "</boolean>\n";
-        s = s + indent(n+1) + "<leftOperand>\n" +
-            this.left.toXCQL(n+2) + indent(n+1) + "</leftOperand>\n";
+        s = s + indent(n+1, c) + "</boolean>\n";
+        s = s + indent(n+1, c) + "<leftOperand>\n" +
+            this.left.toXCQL(n+2, c) + indent(n+1, c) + "</leftOperand>\n";
 
-        s = s + indent(n+1) + "<rightOperand>\n" +
-            this.right.toXCQL(n+2) + indent(n+1) + "</rightOperand>\n";
-        s = s + indent(n) + "</triple>\n";
+        s = s + indent(n+1, c) + "<rightOperand>\n" +
+            this.right.toXCQL(n+2, c) + indent(n+1, c) + "</rightOperand>\n";
+        s = s + indent(n, c) + "</triple>\n";
         return s;
+    },
+
+    toFQ: function (n, c, nl) {
+      var s = '{"op": "'+this.op+'"';
+      //proximity modifiers
+      for (var i = 0; i < this.modifiers.length; i++)
+        s += ', ' + this.modifiers[i].toFQ();
+      s += ','+nl+indent(n, c)+' "s1": '+this.left.toFQ(n+1, c, nl);
+      s += ','+nl+indent(n, c)+' "s2": '+this.right.toFQ(n+1, c, nl);
+      var fill = n && c ? ' ' : '';
+      s += nl+indent(n-1, c)+fill+'}';
+      return s;
     }
+
 }
 // CQLParser
 var CQLParser = function () {
@@ -107,23 +203,97 @@ var CQLParser = function () {
     this.val = null;
     this.prefixes = new Object();
     this.tree = null;
+    this.scf = null;
+    this.scr = null;
 }
 
 CQLParser.prototype = {
-    parse: function (query) {
+    parse: function (query, scf, scr) {
         if (!query)
             throw new Error("The query to be parsed cannot be empty");
-        
+        this.scf = typeof scf != 'string' 
+          ? DEFAULT_SERVER_CHOICE_FIELD : scf;
+        this.scr = typeof scr != 'string' 
+          ? DEFAULT_SERVER_CHOICE_RELATION : scr; 
         this.qs = query;
         this.ql = this.qs.length;
         this.qi = 0;
         this._move(); 
-        this.tree = this._parseQuery("cql.serverChoice", "scr", new Array());
+        this.tree = this._parseQuery(this.scf, this.scr, new Array());
         if (this.look != "")
             throw new Error("EOF expected");
     },
-    toXCQL: function () {
-        return this.tree.toXCQL();
+    parseFromFQ: function (query, scf, scr) {
+       if (!query)
+          throw new Error("The query to be parsed cannot be empty");
+       if (typeof query == 'string')
+         query = JSON.parse(query);
+       this.scf = typeof scf != 'string' 
+         ? DEFAULT_SERVER_CHOICE_FIELD : scf;
+       this.scr = typeof scr != 'string' 
+         ? DEFAULT_SERVER_CHOICE_RELATION : scr;
+       this.tree = this._parseFromFQ(query);
+    },
+    _parseFromFQ: function (fq) {
+        //op-node
+        if (fq.hasOwnProperty('op') 
+            && fq.hasOwnProperty('s1')
+            && fq.hasOwnProperty('s2')) {
+          var node = new CQLBoolean();
+          node.op = fq.op;
+          node.left = this._parseFromFQ(fq.s1);
+          node.right = this._parseFromFQ(fq.s2);
+          //include all other members as modifiers
+          node.modifiers = [];
+          for (var key in fq) {
+            if (key == 'op' || key == 's1' || key == 's2')
+              continue;
+            var mod = new CQLModifier();
+            mod.name = key;
+            mod.relation = '=';
+            mod.value = fq[key];
+            node.modifiers.push(mod);
+          }
+          return node;
+        }
+        //search-clause node
+        if (fq.hasOwnProperty('term')) {
+          var node = new CQLSearchClause();
+          node.term = fq.term;
+          node.scf = this.scf;
+          node.scr = this.scr;
+          node.field = fq.hasOwnProperty('field') 
+            ? fq.field : this.scf;
+          node.relation = fq.hasOwnProperty('relation')
+            ? node._remapRelation(fq.relation) : this.scr;
+          //include all other members as modifiers
+          node.relationuri = '';
+          node.fielduri = '';
+          node.modifiers = [];
+          for (var key in fq) {
+            if (key == 'term' || key == 'field' || key == 'relation')
+              continue;
+            var mod = new CQLModifier();
+            mod.name = key;
+            mod.relation = '=';
+            mod.value = fq[key];
+            node.modifiers.push(mod);
+          }
+          return node;
+        }
+        throw new Error('Unknow node type; '+JSON.stringify(fq));
+    },
+    toXCQL: function (c) {
+        c = typeof c == "undefined" ? ' ' : c;
+        return this.tree.toXCQL(0, c);
+    },
+    toFQ: function (c, nl) {
+        c = typeof c == "undefined" ? '  ' : c;
+        nl = typeof nl == "undefined" ? '\n' : c;
+        return this.tree.toFQ(0, c, nl);
+    },
+    toString: function () {
+        return this.tree.toString();
     },
     _parseQuery: function(field, relation, modifiers) {
         var left = this._parseSearchClause(field, relation, modifiers);
@@ -229,7 +399,9 @@ CQLParser.prototype = {
                         relation,
                         reluri,
                         modifiers,
-                        first);
+                        first,
+                        this.scf,
+                        this.scr);
                 return sc;
             }
         // prefixes
@@ -259,41 +431,55 @@ CQLParser.prototype = {
 
     },
     _move: function () {
+        //eat whitespace
         while (this.qi < this.ql 
                && this._strchr(" \t\r\n", this.qs.charAt(this.qi)))
             this.qi++;
+        //eof
         if (this.qi == this.ql) {
             this.look = "";
             return;
         }
+        //current char
         var c = this.qs.charAt(this.qi);
+        //separators
         if (this._strchr("()/", c)) {
             this.look = c;
             this.qi++;
+        //comparitor
         } else if (this._strchr("<>=", c)) {
             this.look = c;
             this.qi++;
+            //comparitors can repeat, could be if
             while (this.qi < this.ql 
                    && this._strchr("<>=", this.qs.charAt(this.qi))) {
                 this.look = this.look + this.qs.charAt(this.qi);
                 this.qi++;
             }
+        //quoted string
         } else if (this._strchr("\"'", c)) {
             this.look = "q";
+            //remember quote char
             var mark = c;
             this.qi++;
             this.val = "";
-            while (this.qi < this.ql 
-                   && this.qs.charAt(this.qi) != mark) {
-                if (this.qs.charAt(this.qi) == '\\' 
-                    && this.qi < this.ql-1)
-                    this.qi++;
-                this.val = this.val + this.qs.charAt(this.qi);
-                this.qi++;
+            var escaped = false;
+            while (this.qi < this.ql) {
+              if (!escaped && this.qs.charAt(this.qi) == mark)
+                break;
+              if (!escaped && this.qs.charAt(this.qi) == '\\')
+                escaped = true;
+              else
+                escaped = false;
+              this.val += this.qs.charAt(this.qi);
+              this.qi++;
             }
             this.lval = this.val.toLowerCase();
             if (this.qi < this.ql)
                 this.qi++;
+            else //unterminated
+              this.look = ""; //notify error
+        //unquoted string
         } else {
             this.look = "s";
             this.val = "";