Update link to CQL spec
[cql-js-moved-to-github.git] / cql.js
1 var EXPORTED_SYMBOLS = ["CQLParser"];
2
3 var DEFAULT_SERVER_CHOICE_FIELD = 'cql.serverChoice';
4 var DEFAULT_SERVER_CHOICE_RELATION = 'scr';
5
6 function indent(n, c) {
7     var s = "";
8     for (var i = 0; i < n; i++)
9         s = s + c;
10     return s;
11 }
12 // CQLModifier
13 var CQLModifier = function () {
14     this.name = null;
15     this.relation = null;
16     this.value = null;
17 }
18
19 CQLModifier.prototype = {
20     toString: function () {
21       return this.name + this.relation + this.value;
22     },
23
24     toXCQL: function (n, c) {
25         var s = indent(n+1, c) + "<modifier>\n";
26         s = s + indent(n+2, c) + "<name>" + this.name + "</name>\n";
27         if (this.relation != null)
28             s = s + indent(n+2, c) 
29                 + "<relation>" + this.relation + "</relation>\n";
30         if (this.value != null)
31             s = s + indent(n+2, c) 
32                 + "<value>" + this.value +"</value>\n";
33         s = s + indent(n+1, c) + "</modifier>\n";
34         return s;
35     },
36
37     toFQ: function () {
38         //we ignore modifier relation symbol, for value-less modifiers
39         //we assume 'true'
40         var value = this.value.length > 0 ? this.value : "true";
41         var s = '"'+this.name+'": "'+value+'"';
42         return s;
43     }
44 }
45
46 // CQLSearchClause
47 var CQLSearchClause = function (field, fielduri, relation, relationuri, 
48                                 modifiers, term, scf, scr) {
49     this.field = field;
50     this.fielduri = fielduri;
51     this.relation = relation;
52     this.relationuri = relationuri;
53     this.modifiers = modifiers;
54     this.term = term;
55     this.scf = scf || DEFAULT_SERVER_CHOICE_FIELD;
56     this.scr = scr || DEFAULT_SERVER_CHOICE_RELATION;
57 }
58
59 CQLSearchClause.prototype = {
60     toString: function () {
61       var field = this.field;
62       var relation = this.relation;
63       if (field == this.scf && relation == this.scr) {
64         //avoid redundant field/relation
65         field = null;
66         relation = null;
67       }
68       return (field ? field + ' ' : '') + 
69         (relation ? relation : '') +
70         (this.modifiers.length > 0 ? '/' + this.modifiers.join('/') : '') +
71         (relation || this.modifiers.length ? ' ' : '') +
72         '"' + this.term + '"';
73     },
74
75     toXCQL: function (n, c) {
76         var s = indent(n, c) + "<searchClause>\n";
77         if (this.fielduri.length > 0)
78         {
79             s = s + indent(n+1, c) + "<prefixes>\n" +
80                 indent(n+2, c) + "<prefix>\n" +
81                 indent(n+3, c) + "<identifier>" + this.fielduri +
82                 "</identifier>\n" +
83                 indent(n+2, c) + "</prefix>\n" +
84                 indent(n+1, c) + "</prefixes>\n";
85         }
86         s = s + indent(n+1, c) + "<index>" + this.field + "</index>\n";
87         s = s + indent(n+1, c) + "<relation>\n";
88         if (this.relationuri.length > 0) {
89             s = s + indent(n+2, c) +
90                 "<identifier>" + this.relationuri + "</identifier>\n";
91         }
92         s = s + indent(n+2, c) + "<value>" + this.relation + "</value>\n";
93         if (this.modifiers.length > 0) {
94             s = s + indent(n+2, c) + "<modifiers>\n";
95             for (var i = 0; i < this.modifiers.length; i++)
96                 s = s + this.modifiers[i].toXCQL(n+2, c);
97             s = s + indent(n+2, c) + "</modifiers>\n";
98         }
99         s = s + indent(n+1, c) + "</relation>\n";
100         s = s + indent(n+1, c) + "<term>" + this.term + "</term>\n";
101         s = s + indent(n, c) + "</searchClause>\n";
102         return s;
103     },
104
105     toFQ: function () {
106         var s = '{"term": "'+this.term+'"';
107         if (this.field.length > 0 && this.field != this.scf)
108           s+= ', "field": "'+this.field+'"';
109         if (this.relation.length > 0 && this.relation != this.scr)
110           s+= ', "relation": "'+this._mapRelation(this.relation)+'"';
111         for (var i = 0; i < this.modifiers.length; i++) {
112           //since modifiers are mapped to keys, ignore the reserved ones
113           if (this.modifiers[i].name == "term"
114             ||this.modifiers[i].name == "field"
115             ||this.modifiers[i].name == "relation")
116             continue;
117           s += ', ' + this.modifiers[i].toFQ();
118         }
119         s += '}';
120         return s;
121     },
122
123     _mapRelation: function (rel) {
124       switch(rel) {
125         case "<" : return "lt";
126         case ">" : return "gt";
127         case "=" : return "eq";
128         case "<>" : return "ne";
129         case ">=" : return "ge";
130         case "<=" : return "le";
131         default: return rel;
132       }
133     },
134
135     _remapRelation: function (rel) {
136       switch(rel) {
137         case "lt" : return "<";
138         case "gt" : return ">";
139         case "eq" : return "=";
140         case "ne" : return "<>";
141         case "ge" : return ">=";
142         case "le" : return "<=";
143         default: return rel;
144       }
145     }
146
147 }
148 // CQLBoolean
149 var CQLBoolean = function() {
150     this.op = null;
151     this.modifiers = null;
152     this.left = null;
153     this.right = null;
154 }
155
156 CQLBoolean.prototype = {
157     toString: function () {
158       return (this.left.op ? '(' + this.left + ')' : this.left) + ' ' + 
159         this.op.toUpperCase() +
160         (this.modifiers.length > 0 ? '/' + this.modifiers.join('/') : '') + 
161         ' ' + (this.right.op ? '(' + this.right + ')' : this.right);;
162     },
163     toXCQL: function (n, c) {
164         var s = indent(n, c) + "<triple>\n";
165         s = s + indent(n+1, c) + "<boolean>\n" +
166             indent(n+2, c) + "<value>" + this.op + "</value>\n";
167         if (this.modifiers.length > 0) {
168             s = s + indent(n+2, c) + "<modifiers>\n";
169             for (var i = 0; i < this.modifiers.length; i++)
170                 s = s + this.modifiers[i].toXCQL(n+2, c);
171             s = s + indent(n+2, c) + "</modifiers>\n";
172         }
173         s = s + indent(n+1, c) + "</boolean>\n";
174         s = s + indent(n+1, c) + "<leftOperand>\n" +
175             this.left.toXCQL(n+2, c) + indent(n+1, c) + "</leftOperand>\n";
176
177         s = s + indent(n+1, c) + "<rightOperand>\n" +
178             this.right.toXCQL(n+2, c) + indent(n+1, c) + "</rightOperand>\n";
179         s = s + indent(n, c) + "</triple>\n";
180         return s;
181     },
182
183     toFQ: function (n, c, nl) {
184       var s = '{"op": "'+this.op+'"';
185       //proximity modifiers
186       for (var i = 0; i < this.modifiers.length; i++)
187         s += ', ' + this.modifiers[i].toFQ();
188       s += ','+nl+indent(n, c)+' "s1": '+this.left.toFQ(n+1, c, nl);
189       s += ','+nl+indent(n, c)+' "s2": '+this.right.toFQ(n+1, c, nl);
190       var fill = n && c ? ' ' : '';
191       s += nl+indent(n-1, c)+fill+'}';
192       return s;
193     }
194
195 }
196 // CQLParser
197 var CQLParser = function () {
198     this.qi = null;
199     this.ql = null;
200     this.qs = null;
201     this.look = null;
202     this.lval = null;
203     this.val = null;
204     this.prefixes = new Object();
205     this.tree = null;
206     this.scf = null;
207     this.scr = null;
208 }
209
210 CQLParser.prototype = {
211     parse: function (query, scf, scr) {
212         if (!query)
213             throw new Error("The query to be parsed cannot be empty");
214         this.scf = typeof scf != 'string' 
215           ? DEFAULT_SERVER_CHOICE_FIELD : scf;
216         this.scr = typeof scr != 'string' 
217           ? DEFAULT_SERVER_CHOICE_RELATION : scr; 
218         this.qs = query;
219         this.ql = this.qs.length;
220         this.qi = 0;
221         this._move(); 
222         this.tree = this._parseQuery(this.scf, this.scr, new Array());
223         if (this.look != "")
224             throw new Error("EOF expected");
225     },
226     parseFromFQ: function (query, scf, scr) {
227        if (!query)
228           throw new Error("The query to be parsed cannot be empty");
229        if (typeof query == 'string')
230          query = JSON.parse(query);
231        this.scf = typeof scf != 'string' 
232          ? DEFAULT_SERVER_CHOICE_FIELD : scf;
233        this.scr = typeof scr != 'string' 
234          ? DEFAULT_SERVER_CHOICE_RELATION : scr;
235        this.tree = this._parseFromFQ(query);
236     },
237     _parseFromFQ: function (fq) {
238         //op-node
239         if (fq.hasOwnProperty('op') 
240             && fq.hasOwnProperty('s1')
241             && fq.hasOwnProperty('s2')) {
242           var node = new CQLBoolean();
243           node.op = fq.op;
244           node.left = this._parseFromFQ(fq.s1);
245           node.right = this._parseFromFQ(fq.s2);
246           //include all other members as modifiers
247           node.modifiers = [];
248           for (var key in fq) {
249             if (key == 'op' || key == 's1' || key == 's2')
250               continue;
251             var mod = new CQLModifier();
252             mod.name = key;
253             mod.relation = '=';
254             mod.value = fq[key];
255             node.modifiers.push(mod);
256           }
257           return node;
258         }
259         //search-clause node
260         if (fq.hasOwnProperty('term')) {
261           var node = new CQLSearchClause();
262           node.term = fq.term;
263           node.scf = this.scf;
264           node.scr = this.scr;
265           node.field = fq.hasOwnProperty('field') 
266             ? fq.field : this.scf;
267           node.relation = fq.hasOwnProperty('relation')
268             ? node._remapRelation(fq.relation) : this.scr;
269           //include all other members as modifiers
270           node.relationuri = '';
271           node.fielduri = '';
272           node.modifiers = [];
273           for (var key in fq) {
274             if (key == 'term' || key == 'field' || key == 'relation')
275               continue;
276             var mod = new CQLModifier();
277             mod.name = key;
278             mod.relation = '=';
279             mod.value = fq[key];
280             node.modifiers.push(mod);
281           }
282           return node;
283         }
284         throw new Error('Unknow node type; '+JSON.stringify(fq));
285     },
286     toXCQL: function (c) {
287         c = typeof c == "undefined" ? ' ' : c;
288         return this.tree.toXCQL(0, c);
289     },
290     toFQ: function (c, nl) {
291         c = typeof c == "undefined" ? '  ' : c;
292         nl = typeof nl == "undefined" ? '\n' : c;
293         return this.tree.toFQ(0, c, nl);
294     },
295     toString: function () {
296         return this.tree.toString();
297     },
298     _parseQuery: function(field, relation, modifiers) {
299         var left = this._parseSearchClause(field, relation, modifiers);
300         while (this.look == "s" && (
301                     this.lval == "and" ||
302                     this.lval == "or" ||
303                     this.lval == "not" ||
304                     this.lval == "prox")) {
305             var b = new CQLBoolean();
306             b.op = this.lval;
307             this._move();
308             b.modifiers = this._parseModifiers();
309             b.left = left;
310             b.right = this._parseSearchClause(field, relation, modifiers);
311             left = b;
312         }
313         return left;
314     },
315     _parseModifiers: function() {
316         var ar = new Array();
317         while (this.look == "/") {
318             this._move();
319             if (this.look != "s" && this.look != "q")
320                 throw new Error("Invalid modifier.")
321             
322             var name = this.lval;
323             this._move();
324             if (this.look.length > 0 
325                 && this._strchr("<>=", this.look.charAt(0))) {
326                 var rel = this.look;
327                 this._move();
328                 if (this.look != "s" && this.look != "q")
329                     throw new Error("Invalid relation within the modifier.");
330                 
331                 var m = new CQLModifier();
332                 m.name = name;
333                 m.relation = rel;
334                 m.value = this.val;
335                 ar.push(m);
336                 this._move();
337             } else {
338                 var m = new CQLModifier();
339                 m.name = name;
340                 m.relation = "";
341                 m.value = "";
342                 ar.push(m);
343             }
344         }
345         return ar;
346     },
347     _parseSearchClause: function(field, relation, modifiers) {
348         if (this.look == "(") {
349             this._move();
350             var b = this._parseQuery(field, relation, modifiers);
351             if (this.look == ")")
352                 this._move();
353             else
354                 throw new Error("Missing closing parenthesis.");
355
356             return b;
357         } else if (this.look == "s" || this.look == "q") {
358             var first = this.val;   // dont know if field or term yet
359             this._move();
360             if (this.look == "q" ||
361                     (this.look == "s" &&
362                      this.lval != "and" &&
363                      this.lval != "or" &&
364                      this.lval != "not" &&
365                      this.lval != "prox")) {
366                 var rel = this.val;    // string relation
367                 this._move();
368                 return this._parseSearchClause(first, rel,
369                                                this._parseModifiers());
370             } else if (this.look.length > 0 
371                        && this._strchr("<>=", this.look.charAt(0))) {
372                 var rel = this.look;   // other relation <, = ,etc
373                 this._move();
374                 return this._parseSearchClause(first, rel, 
375                                                this._parseModifiers());
376             } else {
377                 // it's a search term
378                 var pos = field.indexOf('.');
379                 var pre = "";
380                 if (pos != -1)
381                     pre = field.substring(0, pos);
382                 
383                 var uri = this._lookupPrefix(pre);
384                 if (uri.length > 0)
385                     field = field.substring(pos+1);
386                 
387                 pos = relation.indexOf('.');
388                 if (pos == -1)
389                     pre = "cql";
390                 else
391                     pre = relation.substring(0, pos);
392
393                 var reluri = this._lookupPrefix(pre);
394                 if (reluri.Length > 0)
395                     relation = relation.Substring(pos+1);
396
397                 var sc = new CQLSearchClause(field,
398                         uri,
399                         relation,
400                         reluri,
401                         modifiers,
402                         first,
403                         this.scf,
404                         this.scr);
405                 return sc;
406             }
407         // prefixes
408         } else if (this.look == ">") {
409             this._move();
410             if (this.look != "s" && this.look != "q")
411                 throw new Error("Expecting string or a quoted expression.");
412             
413             var first = this.lval;
414             this._move();
415             if (this.look == "=")
416             {
417                 this._move();
418                 if (this.look != "s" && this.look != "q")
419                     throw new Error("Expecting string or a quoted expression.");
420                 
421                 this._addPrefix(first, this.lval);
422                 this._move();
423                 return this._parseQuery(field, relation, modifiers);
424             } else {
425                 this._addPrefix("default", first);
426                 return this._parseQuery(field, relation, modifiers);
427             }
428         } else {
429             throw new Error("Invalid search clause.");
430         }
431
432     },
433     _move: function () {
434         //eat whitespace
435         while (this.qi < this.ql 
436                && this._strchr(" \t\r\n", this.qs.charAt(this.qi)))
437             this.qi++;
438         //eof
439         if (this.qi == this.ql) {
440             this.look = "";
441             return;
442         }
443         //current char
444         var c = this.qs.charAt(this.qi);
445         //separators
446         if (this._strchr("()/", c)) {
447             this.look = c;
448             this.qi++;
449         //comparitor
450         } else if (this._strchr("<>=", c)) {
451             this.look = c;
452             this.qi++;
453             //comparitors can repeat, could be if
454             while (this.qi < this.ql 
455                    && this._strchr("<>=", this.qs.charAt(this.qi))) {
456                 this.look = this.look + this.qs.charAt(this.qi);
457                 this.qi++;
458             }
459         //quoted string
460         } else if (this._strchr("\"'", c)) {
461             this.look = "q";
462             //remember quote char
463             var mark = c;
464             this.qi++;
465             this.val = "";
466             var escaped = false;
467             while (this.qi < this.ql) {
468               if (!escaped && this.qs.charAt(this.qi) == mark)
469                 break;
470               if (!escaped && this.qs.charAt(this.qi) == '\\')
471                 escaped = true;
472               else
473                 escaped = false;
474               this.val += this.qs.charAt(this.qi);
475               this.qi++;
476             }
477             this.lval = this.val.toLowerCase();
478             if (this.qi < this.ql)
479                 this.qi++;
480             else //unterminated
481               this.look = ""; //notify error
482         //unquoted string
483         } else {
484             this.look = "s";
485             this.val = "";
486             while (this.qi < this.ql 
487                    && !this._strchr("()/<>= \t\r\n", this.qs.charAt(this.qi))) {
488                 this.val = this.val + this.qs.charAt(this.qi);
489                 this.qi++;
490             }
491             this.lval = this.val.toLowerCase();
492         }
493     },
494     _strchr: function (s, ch) {
495         return s.indexOf(ch) >= 0
496     },
497     _lookupPrefix: function(name) {
498         return this.prefixes[name] ? this.prefixes[name] : "";
499     },
500     _addPrefix: function(name, value) {
501         //overwrite existing items
502         this.prefixes[name] = value;
503     }
504 }