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