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