New gatherModifiers() method, used to implement boolean modifiers.
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
1 // $Id: CQLParser.java,v 1.31 2007-06-29 10:25:38 mike Exp $
2
3 package org.z3950.zing.cql;
4 import java.io.IOException;
5 import java.util.Vector;
6 import java.util.Properties;
7 import java.io.InputStream;
8 import java.io.FileInputStream;
9 import java.io.FileNotFoundException;
10
11
12 /**
13  * Compiles CQL strings into parse trees of CQLNode subtypes.
14  *
15  * @version     $Id: CQLParser.java,v 1.31 2007-06-29 10:25:38 mike Exp $
16  * @see         <A href="http://zing.z3950.org/cql/index.html"
17  *                      >http://zing.z3950.org/cql/index.html</A>
18  */
19 public class CQLParser {
20     private CQLLexer lexer;
21     static private boolean DEBUG = false;
22     static private boolean LEXDEBUG = false;
23
24     private static void debug(String str) {
25         if (DEBUG)
26             System.err.println("PARSEDEBUG: " + str);
27     }
28
29     /**
30      * Compiles a CQL query.
31      * <P>
32      * The resulting parse tree may be further processed by hand (see
33      * the individual node-types' documentation for details on the
34      * data structure) or, more often, simply rendered out in the
35      * desired form using one of the back-ends.  <TT>toCQL()</TT>
36      * returns a decompiled CQL query equivalent to the one that was
37      * compiled in the first place; <TT>toXCQL()</TT> returns an
38      * XML snippet representing the query; and <TT>toPQF()</TT>
39      * returns the query rendered in Index Data's Prefix Query
40      * Format.
41      *
42      * @param cql       The query
43      * @return          A CQLNode object which is the root of a parse
44      * tree representing the query.  */
45     public CQLNode parse(String cql)
46         throws CQLParseException, IOException {
47         lexer = new CQLLexer(cql, LEXDEBUG);
48
49         lexer.nextToken();
50         debug("about to parseQuery()");
51         CQLNode root = parseQuery("srw.serverChoice", new CQLRelation("scr"));
52         if (lexer.ttype != lexer.TT_EOF)
53             throw new CQLParseException("junk after end: " + lexer.render());
54
55         return root;
56     }
57
58     private CQLNode parseQuery(String index, CQLRelation relation)
59         throws CQLParseException, IOException {
60         debug("in parseQuery()");
61
62         CQLNode term = parseTerm(index, relation);
63         while (lexer.ttype != lexer.TT_EOF && lexer.ttype != ')') {
64             if (lexer.ttype == lexer.TT_AND ||
65                 lexer.ttype == lexer.TT_OR ||
66                 lexer.ttype == lexer.TT_NOT) {
67                 int type = lexer.ttype;
68                 String val = lexer.sval;
69                 match(type);
70                 ModifierSet ms = gatherModifiers(val);
71                 CQLNode term2 = parseTerm(index, relation);
72                 term = ((type == lexer.TT_AND) ? new CQLAndNode(term, term2, ms) :
73                         (type == lexer.TT_OR)  ? new CQLOrNode (term, term2, ms) :
74                                                  new CQLNotNode(term, term2, ms));
75             } else if (lexer.ttype == lexer.TT_PROX) {
76                 match(lexer.ttype);
77                 CQLProxNode proxnode = new CQLProxNode(term);
78                 gatherProxParameters(proxnode);
79                 CQLNode term2 = parseTerm(index, relation);
80                 proxnode.addSecondSubterm(term2);
81                 term = (CQLNode) proxnode;
82             } else {
83                 throw new CQLParseException("expected boolean, got " +
84                                             lexer.render());
85             }
86         }
87
88         debug("no more ops");
89         return term;
90     }
91
92     private ModifierSet gatherModifiers(String base)
93         throws CQLParseException, IOException {
94         debug("in gatherModifiers()");
95
96         ModifierSet ms = new ModifierSet(base);
97         while (lexer.ttype == '/') {
98             match('/');
99             if (lexer.ttype != lexer.TT_WORD)
100                 throw new CQLParseException("expected modifier, "
101                                             + "got " + lexer.render());
102             String type = lexer.sval.toLowerCase();
103             match(lexer.ttype);
104             if (!isRelation()) {
105                 // It's a simple modifier consisting of type only
106                 ms.addModifier(type);
107             } else {
108                 // It's a complex modifier of the form type=value
109                 String comparision = lexer.render(lexer.ttype, false);
110                 match(lexer.ttype);
111
112                 // Yuck
113                 String value = lexer.ttype == lexer.TT_WORD ? lexer.sval :
114                     (double) lexer.nval == (int) lexer.nval ?
115                     new Integer((int) lexer.nval).toString() :
116                     new Double((double) lexer.nval).toString();
117
118                 matchSymbol("modifier value");
119                 ms.addModifier(type, comparision, value);
120             }
121         }
122
123         return ms;
124     }
125
126     private CQLNode parseTerm(String index, CQLRelation relation)
127         throws CQLParseException, IOException {
128         debug("in parseTerm()");
129
130         String word;
131         while (true) {
132             if (lexer.ttype == '(') {
133                 debug("parenthesised term");
134                 match('(');
135                 CQLNode expr = parseQuery(index, relation);
136                 match(')');
137                 return expr;
138             } else if (lexer.ttype == '>') {
139                 match('>');
140                 return parsePrefix(index, relation);
141             }
142
143             debug("non-parenthesised term");
144             word = matchSymbol("index or term");
145             if (!isRelation() && lexer.ttype != lexer.TT_WORD)
146                 break;
147
148             index = word;
149             String relstr = (lexer.ttype == lexer.TT_WORD ?
150                              lexer.sval : lexer.render(lexer.ttype, false));
151             relation = new CQLRelation(relstr);
152             match(lexer.ttype);
153             ModifierSet ms = gatherModifiers(relstr);
154             relation.setModifiers(ms);
155             debug("index='" + index + ", " +
156                   "relation='" + relation.toCQL() + "'");
157         }
158
159         CQLTermNode node = new CQLTermNode(index, relation, word);
160         debug("made term node " + node.toCQL());
161         return node;
162     }
163
164     private CQLNode parsePrefix(String index, CQLRelation relation)
165         throws CQLParseException, IOException {
166         debug("prefix mapping");
167
168         String name = null;
169         String identifier = matchSymbol("prefix-name");
170         if (lexer.ttype == '=') {
171             match('=');
172             name = identifier;
173             identifier = matchSymbol("prefix-identifer");
174         }
175         CQLNode term = parseQuery(index, relation);
176         return new CQLPrefixNode(name, identifier, term);
177     }
178
179     private void gatherProxParameters(CQLProxNode node)
180         throws CQLParseException, IOException {
181         for (int i = 0; i < 4; i++) {
182             if (lexer.ttype != '/')
183                 return;         // end of proximity parameters
184
185             match('/');
186             if (lexer.ttype != '/') {
187                 // not an omitted default
188                 switch (i) {
189                     // Order should be: relation/distance/unit/ordering
190                     // For now, use MA's: unit/relation/distance/ordering
191                 case 0: gatherProxRelation(node); break;
192                 case 1: gatherProxDistance(node); break;
193                 case 2: gatherProxUnit(node); break;
194                 case 3: gatherProxOrdering(node); break;
195                 }
196             }
197         }
198     }
199
200     private void gatherProxRelation(CQLProxNode node)
201         throws CQLParseException, IOException {
202         if (!isRelation())
203             throw new CQLParseException("expected proximity relation, got " +
204                                         lexer.render());
205         node.addModifier("relation", null, lexer.render(lexer.ttype, false));
206         match(lexer.ttype);
207         debug("gPR matched " + lexer.render(lexer.ttype, false));
208     }
209
210     private void gatherProxDistance(CQLProxNode node)
211         throws CQLParseException, IOException {
212         if (lexer.ttype != lexer.TT_NUMBER)
213             throw new CQLParseException("expected proximity distance, got " +
214                                         lexer.render());
215         node.addModifier("distance", null, lexer.render(lexer.ttype, false));
216         match(lexer.ttype);
217         debug("gPD matched " + lexer.render(lexer.ttype, false));
218     }
219
220     private void gatherProxUnit(CQLProxNode node)
221         throws CQLParseException, IOException {
222         if (lexer.ttype != lexer.TT_pWORD &&
223             lexer.ttype != lexer.TT_SENTENCE &&
224             lexer.ttype != lexer.TT_PARAGRAPH &&
225             lexer.ttype != lexer.TT_ELEMENT)
226             throw new CQLParseException("expected proximity unit, got " +
227                                         lexer.render());
228         node.addModifier("unit", null, lexer.render());
229         match(lexer.ttype);
230     }
231
232     private void gatherProxOrdering(CQLProxNode node)
233         throws CQLParseException, IOException {
234         if (lexer.ttype != lexer.TT_ORDERED &&
235             lexer.ttype != lexer.TT_UNORDERED)
236             throw new CQLParseException("expected proximity ordering, got " +
237                                         lexer.render());
238         node.addModifier("ordering", null, lexer.render());
239         match(lexer.ttype);
240     }
241
242     // Checks for a relation that may be used inside a prox operator
243     private boolean isRelation() {
244         debug("isRelation: checking ttype=" + lexer.ttype +
245               " (" + lexer.render() + ")");
246         return (lexer.ttype == '<' ||
247                 lexer.ttype == '>' ||
248                 lexer.ttype == '=' ||
249                 lexer.ttype == lexer.TT_LE ||
250                 lexer.ttype == lexer.TT_GE ||
251                 lexer.ttype == lexer.TT_NE);
252     }
253
254     private void match(int token)
255         throws CQLParseException, IOException {
256         debug("in match(" + lexer.render(token, true) + ")");
257         if (lexer.ttype != token)
258             throw new CQLParseException("expected " +
259                                         lexer.render(token, true) +
260                                         ", " + "got " + lexer.render());
261         int tmp = lexer.nextToken();
262         debug("match() got token=" + lexer.ttype + ", " +
263               "nval=" + lexer.nval + ", sval='" + lexer.sval + "'" +
264               " (tmp=" + tmp + ")");
265     }
266
267     private String matchSymbol(String expected)
268         throws CQLParseException, IOException {
269
270         debug("in matchSymbol()");
271         if (lexer.ttype == lexer.TT_WORD ||
272             lexer.ttype == lexer.TT_NUMBER ||
273             lexer.ttype == '"' ||
274             // The following is a complete list of keywords.  Because
275             // they're listed here, they can be used unquoted as
276             // indexes, terms, prefix names and prefix identifiers.
277             // ### Instead, we should ask the lexer whether what we
278             // have is a keyword, and let the knowledge reside there.
279             lexer.ttype == lexer.TT_AND ||
280             lexer.ttype == lexer.TT_OR ||
281             lexer.ttype == lexer.TT_NOT ||
282             lexer.ttype == lexer.TT_PROX ||
283             lexer.ttype == lexer.TT_pWORD ||
284             lexer.ttype == lexer.TT_SENTENCE ||
285             lexer.ttype == lexer.TT_PARAGRAPH ||
286             lexer.ttype == lexer.TT_ELEMENT ||
287             lexer.ttype == lexer.TT_ORDERED ||
288             lexer.ttype == lexer.TT_UNORDERED) {
289             String symbol = (lexer.ttype == lexer.TT_NUMBER) ?
290                 lexer.render() : lexer.sval;
291             match(lexer.ttype);
292             return symbol;
293         }
294
295         throw new CQLParseException("expected " + expected + ", " +
296                                     "got " + lexer.render());
297     }
298
299
300     /**
301      * Simple test-harness for the CQLParser class.
302      * <P>
303      * Reads a CQL query either from its command-line argument, if
304      * there is one, or standard input otherwise.  So these two
305      * invocations are equivalent:
306      * <PRE>
307      *  CQLParser 'au=(Kerninghan or Ritchie) and ti=Unix'
308      *  echo au=(Kerninghan or Ritchie) and ti=Unix | CQLParser
309      * </PRE>
310      * The test-harness parses the supplied query and renders is as
311      * XCQL, so that both of the invocations above produce the
312      * following output:
313      * <PRE>
314      *  &lt;triple&gt;
315      *    &lt;boolean&gt;
316      *      &lt;value&gt;and&lt;/value&gt;
317      *    &lt;/boolean&gt;
318      *    &lt;triple&gt;
319      *      &lt;boolean&gt;
320      *        &lt;value&gt;or&lt;/value&gt;
321      *      &lt;/boolean&gt;
322      *      &lt;searchClause&gt;
323      *        &lt;index&gt;au&lt;/index&gt;
324      *        &lt;relation&gt;
325      *          &lt;value&gt;=&lt;/value&gt;
326      *        &lt;/relation&gt;
327      *        &lt;term&gt;Kerninghan&lt;/term&gt;
328      *      &lt;/searchClause&gt;
329      *      &lt;searchClause&gt;
330      *        &lt;index&gt;au&lt;/index&gt;
331      *        &lt;relation&gt;
332      *          &lt;value&gt;=&lt;/value&gt;
333      *        &lt;/relation&gt;
334      *        &lt;term&gt;Ritchie&lt;/term&gt;
335      *      &lt;/searchClause&gt;
336      *    &lt;/triple&gt;
337      *    &lt;searchClause&gt;
338      *      &lt;index&gt;ti&lt;/index&gt;
339      *      &lt;relation&gt;
340      *        &lt;value&gt;=&lt;/value&gt;
341      *      &lt;/relation&gt;
342      *      &lt;term&gt;Unix&lt;/term&gt;
343      *    &lt;/searchClause&gt;
344      *  &lt;/triple&gt;
345      * </PRE>
346      * <P>
347      * @param -c
348      *  Causes the output to be written in CQL rather than XCQL - that
349      *  is, a query equivalent to that which was input, is output.  In
350      *  effect, the test harness acts as a query canonicaliser.
351      * @return
352      *  The input query, either as XCQL [default] or CQL [if the
353      *  <TT>-c</TT> option is supplied].
354      */
355     public static void main (String[] args) {
356         char mode = 'x';        // x=XCQL, c=CQL, p=PQF
357         String pfile = null;
358
359         Vector<String> argv = new Vector<String>();
360         for (int i = 0; i < args.length; i++) {
361             argv.add(args[i]);
362         }
363
364         if (argv.size() > 0 && argv.get(0).equals("-d")) {
365             DEBUG = true;
366             argv.remove(0);
367         }
368
369         if (argv.size() > 0 && argv.get(0).equals("-c")) {
370             mode = 'c';
371             argv.remove(0);
372         } else if (argv.size() > 1 && argv.get(0).equals("-p")) {
373             mode = 'p';
374             argv.remove(0);
375             pfile = (String) argv.get(0);
376             argv.remove(0);
377         }
378
379         if (argv.size() > 1) {
380             System.err.println("Usage: CQLParser [-d] [-c] [-p <pqf-properties> [<CQL-query>]");
381             System.err.println("If unspecified, query is read from stdin");
382             System.exit(1);
383         }
384
385         String cql;
386         if (argv.size() == 1) {
387             cql = (String) argv.get(0);
388         } else {
389             byte[] bytes = new byte[10000];
390             try {
391                 // Read in the whole of standard input in one go
392                 int nbytes = System.in.read(bytes);
393             } catch (IOException ex) {
394                 System.err.println("Can't read query: " + ex.getMessage());
395                 System.exit(2);
396             }
397             cql = new String(bytes);
398         }
399
400         CQLParser parser = new CQLParser();
401         CQLNode root = null;
402         try {
403             root = parser.parse(cql);
404         } catch (CQLParseException ex) {
405             System.err.println("Syntax error: " + ex.getMessage());
406             System.exit(3);
407         } catch (IOException ex) {
408             System.err.println("Can't compile query: " + ex.getMessage());
409             System.exit(4);
410         }
411
412         try {
413             if (mode == 'c') {
414                 System.out.println(root.toCQL());
415             } else if (mode == 'p') {
416                 InputStream f = new FileInputStream(pfile);
417                 if (f == null)
418                     throw new FileNotFoundException(pfile);
419
420                 Properties config = new Properties();
421                 config.load(f);
422                 f.close();
423                 System.out.println(root.toPQF(config));
424             } else {
425                 System.out.print(root.toXCQL(0));
426             }
427         } catch (IOException ex) {
428             System.err.println("Can't render query: " + ex.getMessage());
429             System.exit(5);
430         } catch (UnknownIndexException ex) {
431             System.err.println("Unknown index: " + ex.getMessage());
432             System.exit(6);
433         } catch (UnknownRelationException ex) {
434             System.err.println("Unknown relation: " + ex.getMessage());
435             System.exit(7);
436         } catch (UnknownRelationModifierException ex) {
437             System.err.println("Unknown relation modifier: " +
438                                ex.getMessage());
439             System.exit(8);
440         } catch (UnknownPositionException ex) {
441             System.err.println("Unknown position: " + ex.getMessage());
442             System.exit(9);
443         } catch (PQFTranslationException ex) {
444             // We catch all of this class's subclasses, so --
445             throw new Error("can't get a PQFTranslationException");
446         }
447     }
448 }