Add classes for visitor traversal
[cql-java-moved-to-github.git] / src / main / java / org / z3950 / zing / cql / CQLParser.java
1
2 package org.z3950.zing.cql;
3
4 import java.io.BufferedReader;
5 import java.io.IOException;
6 import java.util.Properties;
7 import java.io.InputStream;
8 import java.io.FileInputStream;
9 import java.io.InputStreamReader;
10 import java.util.ArrayList;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Set;
14
15
16 /**
17  * Compiles CQL strings into parse trees of CQLNode subtypes.
18  *
19  * @see         <A href="http://zing.z3950.org/cql/index.html"
20  *                      >http://zing.z3950.org/cql/index.html</A>
21  */
22 public class CQLParser {
23     private CQLTokenizer lexer;
24     private final int compat;   // When false, implement CQL 1.2
25     private final Set<String> customRelations = new HashSet<String>();
26     
27     public static final int V1POINT1 = 12368;
28     public static final int V1POINT2 = 12369;
29     public static final int V1POINT1SORT = 12370;
30     public final boolean allowKeywordTerms;
31
32     static private boolean DEBUG = false;
33     static private boolean LEXDEBUG = false;
34
35     /**
36      * The new parser implements a dialect of CQL specified by the
37      * <tt>compat</tt> argument:
38      * <ul>
39      *  <li>V1POINT1 - CQL version 1.1
40      *  </li>
41      *  <li>V1POINT2 - CQL version 1.2
42      *  </li>
43      *  <li>V1POINT1SORT - CQL version 1.1 but including
44      *          <tt>sortby</tt> as specified for CQL 1.2.
45      *  </li>
46      * </ul>
47      */
48     public CQLParser(int compat) {
49         this.compat = compat;
50         this.allowKeywordTerms = true;
51     }
52     
53     /**
54      * Official CQL grammar allows registered keywords like 'and/or/not/sortby/prox' 
55      * to be used unquoted in terms. This constructor allows to create an instance 
56      * of a parser that prohibits this behavior while sacrificing compatibility.
57      * @param compat CQL version compatibility
58      * @param allowKeywordTerms when false registered keywords are disallowed in unquoted terms
59      */
60     public CQLParser(int compat, boolean allowKeywordTerms) {
61         this.compat = compat;
62         this.allowKeywordTerms = allowKeywordTerms;
63     }
64     
65     /**
66      * The new parser implements CQL 1.2
67      */
68     public CQLParser() {
69         this.compat = V1POINT2;
70         this.allowKeywordTerms = true;
71     }
72
73     private static void debug(String str) {
74         if (DEBUG)
75             System.err.println("PARSEDEBUG: " + str);
76     }
77     
78     /**
79      * Registers custom relation in this parser. Note that when a custom relation
80      * is registered the parser is no longer strictly compliant with the chosen spec.
81      * @param relation
82      * @return true if custom relation has not been registered already
83      */
84     public boolean registerCustomRelation(String relation) {
85       return customRelations.add(relation);
86     }
87     
88     /**
89      * Unregisters previously registered custom relation in this instance of the parser.
90      * @param relation
91      * @return true is relation has been previously registered
92      */
93     public boolean unregisterCustomRelation(String relation) {
94       return customRelations.remove(relation);
95     }
96
97     /**
98      * Compiles a CQL query.
99      * <P>
100      * The resulting parse tree may be further processed by hand (see
101      * the individual node-types' documentation for details on the
102      * data structure) or, more often, simply rendered out in the
103      * desired form using one of the back-ends.  <TT>toCQL()</TT>
104      * returns a decompiled CQL query equivalent to the one that was
105      * compiled in the first place; <TT>toXCQL()</TT> returns an
106      * XML snippet representing the query; and <TT>toPQF()</TT>
107      * returns the query rendered in Index Data's Prefix Query
108      * Format.
109      *
110      * @param cql       The query
111      * @return          A CQLNode object which is the root of a parse
112      * tree representing the query.  */
113     public CQLNode parse(String cql)
114         throws CQLParseException, IOException {
115         lexer = new CQLLexer(cql, LEXDEBUG);
116
117         lexer.move();
118         debug("about to parseQuery()");
119         CQLNode root = parseTopLevelPrefixes("cql.serverChoice",
120                 new CQLRelation(compat == V1POINT2 ? "=" : "scr"));
121         if (lexer.what() != CQLTokenizer.TT_EOF)
122             throw new CQLParseException("junk after end: " + lexer.render(), 
123               lexer.pos());
124
125         return root;
126     }
127
128     private CQLNode parseTopLevelPrefixes(String index, CQLRelation relation)
129         throws CQLParseException, IOException {
130         debug("top-level prefix mapping");
131
132         if (lexer.what() == '>') {
133             return parsePrefix(index, relation, true);
134         }
135
136         CQLNode node = parseQuery(index, relation);
137         if ((compat == V1POINT2 || compat == V1POINT1SORT) &&
138             lexer.what() == CQLTokenizer.TT_SORTBY) {
139             match(lexer.what());
140             debug("sortspec");
141
142             CQLSortNode sortnode = new CQLSortNode(node);
143             while (lexer.what() != CQLTokenizer.TT_EOF) {
144                 String sortindex = matchSymbol("sort index");
145                 ModifierSet ms = gatherModifiers(sortindex);
146                 sortnode.addSortIndex(ms);
147             }
148
149             if (sortnode.keys.size() == 0) {
150                 throw new CQLParseException("no sort keys", lexer.pos());
151             }
152
153             node = sortnode;
154         }
155
156         return node;
157     }
158
159     private CQLNode parseQuery(String index, CQLRelation relation)
160         throws CQLParseException, IOException {
161         debug("in parseQuery()");
162
163         CQLNode term = parseTerm(index, relation);
164         while (lexer.what() != CQLTokenizer.TT_EOF &&
165                lexer.what() != ')' &&
166                lexer.what() != CQLTokenizer.TT_SORTBY) {
167             if (lexer.what() == CQLTokenizer.TT_AND ||
168                 lexer.what() == CQLTokenizer.TT_OR ||
169                 lexer.what() == CQLTokenizer.TT_NOT ||
170                 lexer.what() == CQLTokenizer.TT_PROX) {
171                 int type = lexer.what();
172                 String val = lexer.value();
173                 match(type);
174                 ModifierSet ms = gatherModifiers(val);
175                 CQLNode term2 = parseTerm(index, relation);
176                 term = ((type == CQLTokenizer.TT_AND) ? new CQLAndNode(term, term2, ms) :
177                         (type == CQLTokenizer.TT_OR)  ? new CQLOrNode (term, term2, ms) :
178                         (type == CQLTokenizer.TT_NOT) ? new CQLNotNode(term, term2, ms) :
179                                                  new CQLProxNode(term, term2, ms));
180             } else {
181                 throw new CQLParseException("expected boolean, got " +
182                                             lexer.render(), lexer.pos());
183             }
184         }
185
186         debug("no more ops");
187         return term;
188     }
189
190     private ModifierSet gatherModifiers(String base)
191         throws CQLParseException, IOException {
192         debug("in gatherModifiers()");
193
194         ModifierSet ms = new ModifierSet(base);
195         while (lexer.what() == '/') {
196             match('/');
197             if (lexer.what() != CQLTokenizer.TT_WORD)
198                 throw new CQLParseException("expected modifier, "
199                                             + "got " + lexer.render(), 
200                   lexer.pos());
201             String type = lexer.value().toLowerCase();
202             match(lexer.what());
203             if (!isSymbolicRelation()) {
204                 // It's a simple modifier consisting of type only
205                 ms.addModifier(type);
206             } else {
207                 // It's a complex modifier of the form type=value
208                 String comparision = lexer.render(lexer.what(), false);
209                 match(lexer.what());
210                 String value = matchSymbol("modifier value");
211                 ms.addModifier(type, comparision, value);
212             }
213         }
214
215         return ms;
216     }
217
218     private CQLNode parseTerm(String index, CQLRelation relation)
219         throws CQLParseException, IOException {
220         debug("in parseTerm()");
221
222         String first;
223         StringBuilder all;
224         while (true) {
225             if (lexer.what() == '(') {
226                 debug("parenthesised term");
227                 match('(');
228                 CQLNode expr = parseQuery(index, relation);
229                 match(')');
230                 return expr;
231             } else if (lexer.what() == '>') {
232                 return parsePrefix(index, relation, false);
233             }
234
235             debug("non-parenthesised term");
236             first = matchSymbol("index or term");
237             all = new StringBuilder(first);
238             //match relation only on second postion
239             while (isWordOrString() && (all.length() > first.length() || !isRelation())) {
240               all.append(" ").append(lexer.value());
241               match(lexer.what());
242             }
243
244             if (!isRelation())
245               break; //we're done if no relation
246             
247             //render relation
248             String relstr = (lexer.what() == CQLTokenizer.TT_WORD ?
249                              lexer.value() : lexer.render(lexer.what(), false));
250             //we have relation, but it only makes sense if preceded by a single term
251             if (all.length() > first.length()) {
252               throw new CQLParseException("unexpected relation '"+relstr+"'"
253                 , lexer.pos());
254             }
255             index = first;
256             relation = new CQLRelation(relstr);
257             match(lexer.what());
258             ModifierSet ms = gatherModifiers(relstr);
259             relation.ms = ms;
260             debug("index='" + index + ", " +
261                   "relation='" + relation.toCQL() + "'");
262         }
263         CQLTermNode node = new CQLTermNode(index, relation, all.toString());
264         debug("made term node " + node.toCQL());
265         return node;
266     }
267
268     private CQLNode parsePrefix(String index, CQLRelation relation,
269                                 boolean topLevel)
270         throws CQLParseException, IOException {
271         debug("prefix mapping");
272
273         match('>');
274         String name = null;
275         String identifier = matchSymbol("prefix-name");
276         if (lexer.what() == '=') {
277             match('=');
278             name = identifier;
279             identifier = matchSymbol("prefix-identifer");
280         }
281         CQLNode node = topLevel ?
282             parseTopLevelPrefixes(index, relation) :
283             parseQuery(index, relation);
284
285         return new CQLPrefixNode(name, identifier, node);
286     }
287     
288     private boolean isWordOrString() {
289       return CQLTokenizer.TT_WORD == lexer.what() 
290         || CQLTokenizer.TT_STRING == lexer.what();
291     }
292
293     private boolean isRelation() {
294         debug("isRelation: checking what()=" + lexer.what() +
295               " (" + lexer.render() + ")");
296         if (lexer.what() == CQLTokenizer.TT_WORD &&
297             (lexer.value().indexOf('.') >= 0 ||
298              lexer.value().equals("any") ||
299              lexer.value().equals("all") ||
300              lexer.value().equals("within") ||
301              lexer.value().equals("encloses") ||
302              (lexer.value().equals("exact") && compat != V1POINT2) ||
303              (lexer.value().equals("scr") && compat != V1POINT2) ||
304              (lexer.value().equals("adj") && compat == V1POINT2) ||
305              customRelations.contains(lexer.value())))
306           return true;
307
308         return isSymbolicRelation();
309     }
310
311     private boolean isSymbolicRelation() {
312         debug("isSymbolicRelation: checking what()=" + lexer.what() +
313               " (" + lexer.render() + ")");
314         return (lexer.what() == '<' ||
315                 lexer.what() == '>' ||
316                 lexer.what() == '=' ||
317                 lexer.what() == CQLTokenizer.TT_LE ||
318                 lexer.what() == CQLTokenizer.TT_GE ||
319                 lexer.what() == CQLTokenizer.TT_NE ||
320                 lexer.what() == CQLTokenizer.TT_EQEQ);
321     }
322
323     private void match(int token)
324         throws CQLParseException, IOException {
325         debug("in match(" + lexer.render(token, true) + ")");
326         if (lexer.what() != token)
327             throw new CQLParseException("expected " +
328                                         lexer.render(token, true) +
329                                         ", " + "got " + lexer.render(), 
330               lexer.pos());
331         lexer.move();
332         debug("match() got token=" + lexer.what() + ", value()='" + lexer.value() + "'");
333     }
334
335     private String matchSymbol(String expected)
336         throws CQLParseException, IOException {
337
338         debug("in matchSymbol()");
339         if (lexer.what() == CQLTokenizer.TT_WORD ||
340             lexer.what() == CQLTokenizer.TT_STRING ||
341             // The following is a complete list of keywords.  Because
342             // they're listed here, they can be used unquoted as
343             // indexes, terms, prefix names and prefix identifiers.
344             (allowKeywordTerms &&
345             lexer.what() == CQLTokenizer.TT_AND ||
346             lexer.what() == CQLTokenizer.TT_OR ||
347             lexer.what() == CQLTokenizer.TT_NOT ||
348             lexer.what() == CQLTokenizer.TT_PROX ||
349             lexer.what() == CQLTokenizer.TT_SORTBY)) {
350             String symbol = lexer.value();
351             match(lexer.what());
352             return symbol;
353         }
354
355         throw new CQLParseException("expected " + expected + ", " +
356                                     "got " + lexer.render(), lexer.pos());
357     }
358
359
360     /**
361      * Simple test-harness for the CQLParser class.
362      * <P>
363      * Reads a CQL query either from its command-line argument, if
364      * there is one, or standard input otherwise.  So these two
365      * invocations are equivalent:
366      * <PRE>
367      *  CQLParser 'au=(Kerninghan or Ritchie) and ti=Unix'
368      *  echo au=(Kerninghan or Ritchie) and ti=Unix | CQLParser
369      * </PRE>
370      * The test-harness parses the supplied query and renders is as
371      * XCQL, so that both of the invocations above produce the
372      * following output:
373      * <PRE>
374      *  &lt;triple&gt;
375      *    &lt;boolean&gt;
376      *      &lt;value&gt;and&lt;/value&gt;
377      *    &lt;/boolean&gt;
378      *    &lt;triple&gt;
379      *      &lt;boolean&gt;
380      *        &lt;value&gt;or&lt;/value&gt;
381      *      &lt;/boolean&gt;
382      *      &lt;searchClause&gt;
383      *        &lt;index&gt;au&lt;/index&gt;
384      *        &lt;relation&gt;
385      *          &lt;value&gt;=&lt;/value&gt;
386      *        &lt;/relation&gt;
387      *        &lt;term&gt;Kerninghan&lt;/term&gt;
388      *      &lt;/searchClause&gt;
389      *      &lt;searchClause&gt;
390      *        &lt;index&gt;au&lt;/index&gt;
391      *        &lt;relation&gt;
392      *          &lt;value&gt;=&lt;/value&gt;
393      *        &lt;/relation&gt;
394      *        &lt;term&gt;Ritchie&lt;/term&gt;
395      *      &lt;/searchClause&gt;
396      *    &lt;/triple&gt;
397      *    &lt;searchClause&gt;
398      *      &lt;index&gt;ti&lt;/index&gt;
399      *      &lt;relation&gt;
400      *        &lt;value&gt;=&lt;/value&gt;
401      *      &lt;/relation&gt;
402      *      &lt;term&gt;Unix&lt;/term&gt;
403      *    &lt;/searchClause&gt;
404      *  &lt;/triple&gt;
405      * </PRE>
406      * <P>
407      * @param -1
408      *  CQL version 1.1 (default version 1.2)
409      * @param -d
410      *  Debug mode: extra output written to stderr.
411      * @param -c
412      *  Causes the output to be written in CQL rather than XCQL - that
413      *  is, a query equivalent to that which was input, is output.  In
414      *  effect, the test harness acts as a query canonicaliser.
415      * @return
416      *  The input query, either as XCQL [default] or CQL [if the
417      *  <TT>-c</TT> option is supplied].
418      */
419     public static void main (String[] args) {
420         char mode = 'x';        // x=XCQL, c=CQL, p=PQF
421         String pfile = null;
422
423         List<String> argv = new ArrayList<String>();
424         for (int i = 0; i < args.length; i++) {
425             argv.add(args[i]);
426         }
427
428         int compat = V1POINT2;
429         if (argv.size() > 0 && argv.get(0).equals("-1")) {
430             compat = V1POINT1;
431             argv.remove(0);
432         }
433
434         if (argv.size() > 0 && argv.get(0).equals("-d")) {
435             DEBUG = true;
436             argv.remove(0);
437         }
438
439         if (argv.size() > 0 && argv.get(0).equals("-c")) {
440             mode = 'c';
441             argv.remove(0);
442         } else if (argv.size() > 1 && argv.get(0).equals("-p")) {
443             mode = 'p';
444             argv.remove(0);
445             pfile = (String) argv.get(0);
446             argv.remove(0);
447         }
448
449         if (argv.size() > 1) {
450             System.err.println("Usage: CQLParser [-1] [-d] [-c] " +
451                                "[-p <pqf-properties> [<CQL-query>]");
452             System.err.println("If unspecified, query is read from stdin");
453             System.exit(1);
454         }
455
456         String cql;
457         if (argv.size() == 1) {
458             cql = (String) argv.get(0);
459         } else {
460             BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
461             try {
462                 // read a single line of input
463               cql = buff.readLine();
464               if (cql == null) {
465                 System.err.println("Can't read query from stdin");
466                 System.exit(2);
467                 return;
468               }
469             } catch (IOException ex) {
470                 System.err.println("Can't read query: " + ex.getMessage());
471                 System.exit(2);
472                 return;
473             }
474         }
475
476         CQLParser parser = new CQLParser(compat);
477         CQLNode root;
478         try {
479             root = parser.parse(cql);
480         } catch (CQLParseException ex) {
481             System.err.println("Syntax error: " + ex.getMessage());
482             StringBuilder space = new StringBuilder(cql.length());
483             System.out.println(cql);
484             for (int i=0; i<ex.getPosition(); i++) space.append(" ");
485             space.append("^");
486             System.err.println(space.toString());
487             System.exit(3);
488             return; //compiler
489         } catch (IOException ex) {
490             System.err.println("Can't compile query: " + ex.getMessage());
491             System.exit(4);
492             return; //compiler
493         }
494
495         try {
496             if (mode == 'c') {
497                 System.out.println(root.toCQL());
498             } else if (mode == 'p') {
499               try {
500                 InputStream f = new FileInputStream(pfile);
501                 Properties config = new Properties();
502                 config.load(f);
503                 f.close();
504                 System.out.println(root.toPQF(config));
505               } catch (IOException ex) {
506                 System.err.println("Can't load PQF properties:" + 
507                   ex.getMessage());
508                 System.exit(5);
509               }
510             } else {
511                 System.out.print(root.toXCQL());
512             }
513         } catch (UnknownIndexException ex) {
514             System.err.println("Unknown index: " + ex.getMessage());
515             System.exit(6);
516         } catch (UnknownRelationException ex) {
517             System.err.println("Unknown relation: " + ex.getMessage());
518             System.exit(7);
519         } catch (UnknownRelationModifierException ex) {
520             System.err.println("Unknown relation modifier: " +
521                                ex.getMessage());
522             System.exit(8);
523         } catch (UnknownPositionException ex) {
524             System.err.println("Unknown position: " + ex.getMessage());
525             System.exit(9);
526         } catch (PQFTranslationException ex) {
527             System.err.println("Cannot translate to PQF: " + ex.getMessage());
528             System.exit(10);
529         }
530     }
531 }