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