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 + c;
return s;
}
// CQLModifier
var CQLModifier = function () {
this.name = null;
this.relation = null;
this.value = null;
}
CQLModifier.prototype = {
toString: function () {
return this.name + this.relation + this.value;
},
toXCQL: function (n, c) {
var s = indent(n+1, c) + "\n";
s = s + indent(n+2, c) + "" + this.name + "\n";
if (this.relation != null)
s = s + indent(n+2, c)
+ "" + this.relation + "\n";
if (this.value != null)
s = s + indent(n+2, c)
+ "" + this.value +"\n";
s = s + indent(n+1, c) + "\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, 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 = {
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) + "\n";
if (this.fielduri.length > 0)
{
s = s + indent(n+1, c) + "\n" +
indent(n+2, c) + "\n" +
indent(n+3, c) + "" + this.fielduri +
"\n" +
indent(n+2, c) + "\n" +
indent(n+1, c) + "\n";
}
s = s + indent(n+1, c) + "" + this.field + "\n";
s = s + indent(n+1, c) + "\n";
if (this.relationuri.length > 0) {
s = s + indent(n+2, c) +
"" + this.relationuri + "\n";
}
s = s + indent(n+2, c) + "" + this.relation + "\n";
if (this.modifiers.length > 0) {
s = s + indent(n+2, c) + "\n";
for (var i = 0; i < this.modifiers.length; i++)
s = s + this.modifiers[i].toXCQL(n+2, c);
s = s + indent(n+2, c) + "\n";
}
s = s + indent(n+1, c) + "\n";
s = s + indent(n+1, c) + "" + this.term + "\n";
s = s + indent(n, c) + "\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 += '}';
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() {
this.op = null;
this.modifiers = null;
this.left = null;
this.right = null;
}
CQLBoolean.prototype = {
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) + "\n";
s = s + indent(n+1, c) + "\n" +
indent(n+2, c) + "" + this.op + "\n";
if (this.modifiers.length > 0) {
s = s + indent(n+2, c) + "\n";
for (var i = 0; i < this.modifiers.length; i++)
s = s + this.modifiers[i].toXCQL(n+2, c);
s = s + indent(n+2, c) + "\n";
}
s = s + indent(n+1, c) + "\n";
s = s + indent(n+1, c) + "\n" +
this.left.toXCQL(n+2, c) + indent(n+1, c) + "\n";
s = s + indent(n+1, c) + "\n" +
this.right.toXCQL(n+2, c) + indent(n+1, c) + "\n";
s = s + indent(n, c) + "\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 () {
this.qi = null;
this.ql = null;
this.qs = null;
this.look = null;
this.lval = null;
this.val = null;
this.prefixes = new Object();
this.tree = null;
this.scf = null;
this.scr = null;
}
CQLParser.prototype = {
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(this.scf, this.scr, new Array());
if (this.look != "")
throw new Error("EOF expected");
},
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);
while (this.look == "s" && (
this.lval == "and" ||
this.lval == "or" ||
this.lval == "not" ||
this.lval == "prox")) {
var b = new CQLBoolean();
b.op = this.lval;
this._move();
b.modifiers = this._parseModifiers();
b.left = left;
b.right = this._parseSearchClause(field, relation, modifiers);
left = b;
}
return left;
},
_parseModifiers: function() {
var ar = new Array();
while (this.look == "/") {
this._move();
if (this.look != "s" && this.look != "q")
throw new Error("Invalid modifier.")
var name = this.lval;
this._move();
if (this.look.length > 0
&& this._strchr("<>=", this.look.charAt(0))) {
var rel = this.look;
this._move();
if (this.look != "s" && this.look != "q")
throw new Error("Invalid relation within the modifier.");
var m = new CQLModifier();
m.name = name;
m.relation = rel;
m.value = this.val;
ar.push(m);
this._move();
} else {
var m = new CQLModifier();
m.name = name;
m.relation = "";
m.value = "";
ar.push(m);
}
}
return ar;
},
_parseSearchClause: function(field, relation, modifiers) {
if (this.look == "(") {
this._move();
var b = this._parseQuery(field, relation, modifiers);
if (this.look == ")")
this._move();
else
throw new Error("Missing closing parenthesis.");
return b;
} else if (this.look == "s" || this.look == "q") {
var first = this.val; // dont know if field or term yet
this._move();
if (this.look == "q" ||
(this.look == "s" &&
this.lval != "and" &&
this.lval != "or" &&
this.lval != "not" &&
this.lval != "prox")) {
var rel = this.val; // string relation
this._move();
return this._parseSearchClause(first, rel,
this._parseModifiers());
} else if (this.look.length > 0
&& this._strchr("<>=", this.look.charAt(0))) {
var rel = this.look; // other relation <, = ,etc
this._move();
return this._parseSearchClause(first, rel,
this._parseModifiers());
} else {
// it's a search term
var pos = field.indexOf('.');
var pre = "";
if (pos != -1)
pre = field.substring(0, pos);
var uri = this._lookupPrefix(pre);
if (uri.length > 0)
field = field.substring(pos+1);
pos = relation.indexOf('.');
if (pos == -1)
pre = "cql";
else
pre = relation.substring(0, pos);
var reluri = this._lookupPrefix(pre);
if (reluri.Length > 0)
relation = relation.Substring(pos+1);
var sc = new CQLSearchClause(field,
uri,
relation,
reluri,
modifiers,
first,
this.scf,
this.scr);
return sc;
}
// prefixes
} else if (this.look == ">") {
this._move();
if (this.look != "s" && this.look != "q")
throw new Error("Expecting string or a quoted expression.");
var first = this.lval;
this._move();
if (this.look == "=")
{
this._move();
if (this.look != "s" && this.look != "q")
throw new Error("Expecting string or a quoted expression.");
this._addPrefix(first, this.lval);
this._move();
return this._parseQuery(field, relation, modifiers);
} else {
this._addPrefix("default", first);
return this._parseQuery(field, relation, modifiers);
}
} else {
throw new Error("Invalid search clause.");
}
},
_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 = "";
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 = "";
while (this.qi < this.ql
&& !this._strchr("()/<>= \t\r\n", this.qs.charAt(this.qi))) {
this.val = this.val + this.qs.charAt(this.qi);
this.qi++;
}
this.lval = this.val.toLowerCase();
}
},
_strchr: function (s, ch) {
return s.indexOf(ch) >= 0
},
_lookupPrefix: function(name) {
return this.prefixes[name] ? this.prefixes[name] : "";
},
_addPrefix: function(name, value) {
//overwrite existing items
this.prefixes[name] = value;
}
}