Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/main/java/org/z3950/zing/cql/CQLLexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ public class CQLLexer implements CQLTokenizer {
private String lval;
private StringBuilder buf = new StringBuilder();

static final String OPS_AND_WHITESPACE = "()/<>= \t\r\n";

public CQLLexer(String cql, boolean debug) {
qs = cql;
ql = cql.length();
Expand Down Expand Up @@ -89,8 +91,7 @@ public void move() {
} else {
what = TT_WORD;
buf.setLength(0); // reset buffer
while (qi < ql
&& !strchr("()/<>= \t\r\n", qs.charAt(qi))) {
while (qi < ql && !strchr(OPS_AND_WHITESPACE, qs.charAt(qi))) {
buf.append(qs.charAt(qi));
qi++;
}
Expand Down
52 changes: 31 additions & 21 deletions src/main/java/org/z3950/zing/cql/CQLTermNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ void toXCQLInternal(XCQLBuilder b, int level, List<CQLPrefix> prefixes,

@Override
public String toCQL() {
String quotedIndex = maybeQuote(index);
String quotedTerm = maybeQuote(term);
String quotedIndex = toCQLTerm(index);
String quotedTerm = toCQLTerm(term);
String res = quotedTerm;

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

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

return s + maybeQuote(text);
return s + toCQLTerm(text);
}

static String maybeQuote(String str) {
if (str == null)
// ensure that a term is properly quoted for CQL output if necessary.
// If the term has a bare double-quote (") it will be
// escaped with a backslash.
static String toCQLTerm(String str) {
if (str == null) {
Copy link
Contributor

@jakub-id jakub-id Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamdickmeiss this function name is confusing since it both quotes and escapes. Btw, we should do the same in cql-go.

Copy link
Contributor Author

@adamdickmeiss adamdickmeiss Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, the function name is bad.

There will never be a bare " as the result of a query in the current form. But it did happen in latest release until #5. This is just extra precaution to ensure that if it is bare, it will be escaped. That's all.

return null;

// There _must_ be a better way to make this test ...
if (str.length() == 0 ||
str.indexOf('"') != -1 ||
str.indexOf(' ') != -1 ||
str.indexOf('\t') != -1 ||
str.indexOf('=') != -1 ||
str.indexOf('<') != -1 ||
str.indexOf('>') != -1 ||
str.indexOf('/') != -1 ||
str.indexOf('(') != -1 ||
str.indexOf(')') != -1) {
str = '"' + str.replaceAll("(?<!\\\\)\"", "\\\\\"") + '"';
}

return str;
boolean quote = str.isEmpty();
boolean escaped = false;
StringBuilder sb = new StringBuilder();
for (char ch : str.toCharArray()) {
if (CQLLexer.OPS_AND_WHITESPACE.indexOf(ch) >= 0) {
quote = true;
}
if (ch == '"' && !escaped) {
sb.append('\\');
}
escaped = ch == '\\' && !escaped;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamdickmeiss what if \ is on the last position like in x\

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be left as is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the resulting query will be invalid no?

sb.append(ch);
}
if (escaped) {
// trailing backslash - escape it
sb.append('\\');
}
if (quote) {
return "\"" + sb.toString() + "\"";
} else {
return sb.toString();
}
}

@Override
Expand Down
62 changes: 62 additions & 0 deletions src/test/java/org/z3950/zing/cql/CQLTermNodeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.z3950.zing.cql;

import org.junit.Test;
import static org.junit.Assert.*;

public class CQLTermNodeTest {
@Test
public void TestTCQLTermQuoteNull() {
assertNull(CQLTermNode.toCQLTerm(null));
}

@Test
public void TestTCQLTermQuoteEmpty() {
assertEquals("\"\"", CQLTermNode.toCQLTerm(""));
}

@Test
public void TestTCQLTermQuoteRelation() {
assertEquals("\"<\"", CQLTermNode.toCQLTerm("<"));
}

@Test
public void TestTCQLTermQuoteSimple() {
assertEquals("simple", CQLTermNode.toCQLTerm("simple"));
}

@Test
public void TestTCQLTermQuoteBlank() {
assertEquals("\"a b\"", CQLTermNode.toCQLTerm("a b"));
}

@Test
public void TestTCQLTermQuoteQuote1() {
assertEquals("a\\\"", CQLTermNode.toCQLTerm("a\""));
}

@Test
public void TestTCQLTermQuoteQuote2() {
assertEquals("a\\\"", CQLTermNode.toCQLTerm("a\\\""));
}

@Test
public void TestTCQLTermQuoteQuote3() {
assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.toCQLTerm("a" + "\\\\" + "\""));
}

@Test
public void TestTCQLTermQuoteQuote4() {
assertEquals("a" + "\\\\" + "\\\"", CQLTermNode.toCQLTerm("a" + "\\\\" + "\\\""));
}

@Test
public void TestTCQLTermQuoteBackSlashTrail1() {
assertEquals("a\\\\", CQLTermNode.toCQLTerm("a\\"));
}

@Test
public void TestTCQLTermQuoteBackSlashTrail2() {
assertEquals("\"a \\\\\"", CQLTermNode.toCQLTerm("a \\"));
}

}