Skip to content

Commit efaf5ae

Browse files
Proper fixup for " when generating CQL query (#26)
The replaceAll for looking back whether the " was escaped did not work as the preceding backslash could have been the result of escaping a backslash itself. Also deal with a trailing backslash (unterminated sequence). A follow up for #5
1 parent adefe70 commit efaf5ae

File tree

3 files changed

+96
-23
lines changed

3 files changed

+96
-23
lines changed

src/main/java/org/z3950/zing/cql/CQLLexer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class CQLLexer implements CQLTokenizer {
1818
private String lval;
1919
private StringBuilder buf = new StringBuilder();
2020

21+
static final String OPS_AND_WHITESPACE = "()/<>= \t\r\n";
22+
2123
public CQLLexer(String cql, boolean debug) {
2224
qs = cql;
2325
ql = cql.length();
@@ -89,8 +91,7 @@ public void move() {
8991
} else {
9092
what = TT_WORD;
9193
buf.setLength(0); // reset buffer
92-
while (qi < ql
93-
&& !strchr("()/<>= \t\r\n", qs.charAt(qi))) {
94+
while (qi < ql && !strchr(OPS_AND_WHITESPACE, qs.charAt(qi))) {
9495
buf.append(qs.charAt(qi));
9596
qi++;
9697
}

src/main/java/org/z3950/zing/cql/CQLTermNode.java

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ void toXCQLInternal(XCQLBuilder b, int level, List<CQLPrefix> prefixes,
7878

7979
@Override
8080
public String toCQL() {
81-
String quotedIndex = maybeQuote(index);
82-
String quotedTerm = maybeQuote(term);
81+
String quotedIndex = toCQLTerm(index);
82+
String quotedTerm = toCQLTerm(term);
8383
String res = quotedTerm;
8484

8585
if (index != null &&
@@ -192,7 +192,7 @@ public String toPQF(Properties config) throws PQFTranslationException {
192192
if (isResultSetIndex(index)) {
193193
// Special case: ignore relation, modifiers, wildcards, etc.
194194
// There's parallel code in toType1BER()
195-
return "@set " + maybeQuote(term);
195+
return "@set " + toCQLTerm(term);
196196
}
197197

198198
List<String> attrs = getAttrs(config);
@@ -219,28 +219,38 @@ public String toPQF(Properties config) throws PQFTranslationException {
219219
text = text.substring(0, len - 1);
220220
}
221221

222-
return s + maybeQuote(text);
222+
return s + toCQLTerm(text);
223223
}
224224

225-
static String maybeQuote(String str) {
226-
if (str == null)
225+
// ensure that a term is properly quoted for CQL output if necessary.
226+
// If the term has a bare double-quote (") it will be
227+
// escaped with a backslash.
228+
static String toCQLTerm(String str) {
229+
if (str == null) {
227230
return null;
228-
229-
// There _must_ be a better way to make this test ...
230-
if (str.length() == 0 ||
231-
str.indexOf('"') != -1 ||
232-
str.indexOf(' ') != -1 ||
233-
str.indexOf('\t') != -1 ||
234-
str.indexOf('=') != -1 ||
235-
str.indexOf('<') != -1 ||
236-
str.indexOf('>') != -1 ||
237-
str.indexOf('/') != -1 ||
238-
str.indexOf('(') != -1 ||
239-
str.indexOf(')') != -1) {
240-
str = '"' + str.replaceAll("(?<!\\\\)\"", "\\\\\"") + '"';
241231
}
242-
243-
return str;
232+
boolean quote = str.isEmpty();
233+
boolean escaped = false;
234+
StringBuilder sb = new StringBuilder();
235+
for (char ch : str.toCharArray()) {
236+
if (CQLLexer.OPS_AND_WHITESPACE.indexOf(ch) >= 0) {
237+
quote = true;
238+
}
239+
if (ch == '"' && !escaped) {
240+
sb.append('\\');
241+
}
242+
escaped = ch == '\\' && !escaped;
243+
sb.append(ch);
244+
}
245+
if (escaped) {
246+
// trailing backslash - escape it
247+
sb.append('\\');
248+
}
249+
if (quote) {
250+
return "\"" + sb.toString() + "\"";
251+
} else {
252+
return sb.toString();
253+
}
244254
}
245255

246256
@Override
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.z3950.zing.cql;
2+
3+
import org.junit.Test;
4+
import static org.junit.Assert.*;
5+
6+
public class CQLTermNodeTest {
7+
@Test
8+
public void TestTCQLTermQuoteNull() {
9+
assertNull(CQLTermNode.toCQLTerm(null));
10+
}
11+
12+
@Test
13+
public void TestTCQLTermQuoteEmpty() {
14+
assertEquals("\"\"", CQLTermNode.toCQLTerm(""));
15+
}
16+
17+
@Test
18+
public void TestTCQLTermQuoteRelation() {
19+
assertEquals("\"<\"", CQLTermNode.toCQLTerm("<"));
20+
}
21+
22+
@Test
23+
public void TestTCQLTermQuoteSimple() {
24+
assertEquals("simple", CQLTermNode.toCQLTerm("simple"));
25+
}
26+
27+
@Test
28+
public void TestTCQLTermQuoteBlank() {
29+
assertEquals("\"a b\"", CQLTermNode.toCQLTerm("a b"));
30+
}
31+
32+
@Test
33+
public void TestTCQLTermQuoteQuote1() {
34+
assertEquals("a\\\"", CQLTermNode.toCQLTerm("a\""));
35+
}
36+
37+
@Test
38+
public void TestTCQLTermQuoteQuote2() {
39+
assertEquals("a\\\"", CQLTermNode.toCQLTerm("a\\\""));
40+
}
41+
42+
@Test
43+
public void TestTCQLTermQuoteQuote3() {
44+
assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.toCQLTerm("a" + "\\\\" + "\""));
45+
}
46+
47+
@Test
48+
public void TestTCQLTermQuoteQuote4() {
49+
assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.toCQLTerm("a" + "\\\\" + "\\\""));
50+
}
51+
52+
@Test
53+
public void TestTCQLTermQuoteBackSlashTrail1() {
54+
assertEquals("a\\\\", CQLTermNode.toCQLTerm("a\\"));
55+
}
56+
57+
@Test
58+
public void TestTCQLTermQuoteBackSlashTrail2() {
59+
assertEquals("\"a \\\\\"", CQLTermNode.toCQLTerm("a \\"));
60+
}
61+
62+
}

0 commit comments

Comments
 (0)