1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const EXPORTED_SYMBOLS = ["QueryStringToExpression"];
/**
* A module to parse a query string to a nsIAbBooleanExpression. A valid query
* string is in this form:
*
* (OP1(FIELD1,COND1,VALUE1)..(FIELDn,CONDn,VALUEn)(BOOL2(FIELD1,COND1,VALUE1)..)..)
*
* OPn A boolean operator joining subsequent terms delimited by ().
*
* @see {nsIAbBooleanOperationTypes}.
* FIELDn An addressbook card data field.
* CONDn A condition to compare FIELDn with VALUEn.
* @see {nsIAbBooleanConditionTypes}.
* VALUEn The value to be matched in the FIELDn via the CONDn.
* The value must be URL encoded by the caller, if it contains any
* special characters including '(' and ')'.
*/
var QueryStringToExpression = {
/**
* Convert a query string to a nsIAbBooleanExpression.
*
* @param {string} qs - The query string to convert.
* @returns {nsIAbBooleanExpression}
*/
convert(qs) {
let tokens = this.parse(qs);
// An array of nsIAbBooleanExpression, the first element is the root exp,
// the last element is the current operating exp.
let stack = [];
for (let { type, depth, value } of tokens) {
while (depth < stack.length) {
// We are done with the current exp, go one level up.
stack.pop();
}
if (type == "op") {
if (depth == stack.length) {
// We are done with the current exp, go one level up.
stack.pop();
}
// Found a new exp, go one level down.
let parent = stack.slice(-1)[0];
let exp = this.createBooleanExpression(value);
stack.push(exp);
if (parent) {
parent.expressions = [...parent.expressions, exp];
}
} else if (type == "field") {
// Add a new nsIAbBooleanConditionString to the current exp.
let condition = this.createBooleanConditionString(...value);
let exp = stack.slice(-1)[0];
exp.expressions = [...exp.expressions, condition];
}
}
return stack[0];
},
/**
* Parse a query string to an array of tokens.
*
* @param {string} qs - The query string to parse.
* @param {number} depth - The depth of a token.
* @param {object[]} tokens - The tokens to return.
* @param {"op"|"field"} tokens[].type - The token type.
* @param {number} tokens[].depth - The token depth.
* @param {string|string[]} tokens[].value - The token value.
*/
parse(qs, depth = 0, tokens = []) {
if (qs[0] == "?") {
qs = qs.slice(1);
}
while (qs[0] == ")" && depth > 0) {
depth--;
qs = qs.slice(1);
}
if (qs.length == 0) {
// End of input.
return tokens;
}
if (qs[0] != "(") {
throw Components.Exception(
`Invalid query string: ${qs}`,
Cr.NS_ERROR_ILLEGAL_VALUE
);
}
qs = qs.slice(1);
let nextOpen = qs.indexOf("(");
let nextClose = qs.indexOf(")");
if (nextOpen != -1 && nextOpen < nextClose) {
// Case: "OP("
depth++;
tokens.push({
type: "op",
depth,
value: qs.slice(0, nextOpen),
});
this.parse(qs.slice(nextOpen), depth, tokens);
} else if (nextClose != -1) {
// Case: "FIELD, COND, VALUE)"
tokens.push({
type: "field",
depth,
value: qs.slice(0, nextClose).split(","),
});
this.parse(qs.slice(nextClose + 1), depth, tokens);
}
return tokens;
},
/**
* Create a nsIAbBooleanExpression from a string.
*
* @param {string} operation - The operation string.
* @returns {nsIAbBooleanExpression}
*/
createBooleanExpression(operation) {
let op = {
and: Ci.nsIAbBooleanOperationTypes.AND,
or: Ci.nsIAbBooleanOperationTypes.OR,
not: Ci.nsIAbBooleanOperationTypes.NOT,
}[operation];
if (op == undefined) {
throw Components.Exception(
`Invalid operation: ${operation}`,
Cr.NS_ERROR_ILLEGAL_VALUE
);
}
let exp = Cc["@mozilla.org/boolean-expression/n-peer;1"].createInstance(
Ci.nsIAbBooleanExpression
);
exp.operation = op;
return exp;
},
/**
* Create a nsIAbBooleanConditionString.
*
* @param {string} name - The field name.
* @param {nsIAbBooleanConditionTypes} condition - The condition.
* @param {string} value - The value string.
* @returns {nsIAbBooleanConditionString}
*/
createBooleanConditionString(name, condition, value) {
value = decodeURIComponent(value);
let cond = {
"=": Ci.nsIAbBooleanConditionTypes.Is,
"!=": Ci.nsIAbBooleanConditionTypes.IsNot,
lt: Ci.nsIAbBooleanConditionTypes.LessThan,
gt: Ci.nsIAbBooleanConditionTypes.GreaterThan,
bw: Ci.nsIAbBooleanConditionTypes.BeginsWith,
ew: Ci.nsIAbBooleanConditionTypes.EndsWith,
c: Ci.nsIAbBooleanConditionTypes.Contains,
"!c": Ci.nsIAbBooleanConditionTypes.DoesNotContain,
"~=": Ci.nsIAbBooleanConditionTypes.SoundsLike,
regex: Ci.nsIAbBooleanConditionTypes.RegExp,
ex: Ci.nsIAbBooleanConditionTypes.Exists,
"!ex": Ci.nsIAbBooleanConditionTypes.DoesNotExist,
}[condition];
if (name == "" || condition == "" || value == "" || cond == undefined) {
throw Components.Exception(
`Failed to create condition string from name=${name}, condition=${condition}, value=${value}, cond=${cond}`,
Cr.NS_ERROR_ILLEGAL_VALUE
);
}
let cs = Cc[
"@mozilla.org/boolean-expression/condition-string;1"
].createInstance(Ci.nsIAbBooleanConditionString);
cs.condition = cond;
try {
cs.name = Services.textToSubURI.unEscapeAndConvert("UTF-8", name);
cs.value = Services.textToSubURI.unEscapeAndConvert("UTF-8", value);
} catch (e) {
cs.name = name;
cs.value = value;
}
return cs;
},
};
|