Remove obsolete keywords! Whoop!
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
1 // $Id: CQLParser.java,v 1.34 2007-06-29 12:54:05 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.34 2007-06-29 12:54:05 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                 lexer.ttype == lexer.TT_PROX) {
68                 int type = lexer.ttype;
69                 String val = lexer.sval;
70                 match(type);
71                 ModifierSet ms = gatherModifiers(val);
72                 CQLNode term2 = parseTerm(index, relation);
73                 term = ((type == lexer.TT_AND) ? new CQLAndNode(term, term2, ms) :
74                         (type == lexer.TT_OR)  ? new CQLOrNode (term, term2, ms) :
75                         (type == lexer.TT_NOT) ? new CQLNotNode(term, term2, ms) :
76                                                  new CQLProxNode(term, term2, ms));
77             } else {
78                 throw new CQLParseException("expected boolean, got " +
79                                             lexer.render());
80             }
81         }
82
83         debug("no more ops");
84         return term;
85     }
86
87     private ModifierSet gatherModifiers(String base)
88         throws CQLParseException, IOException {
89         debug("in gatherModifiers()");
90
91         ModifierSet ms = new ModifierSet(base);
92         while (lexer.ttype == '/') {
93             match('/');
94             if (lexer.ttype != lexer.TT_WORD)
95                 throw new CQLParseException("expected modifier, "
96                                             + "got " + lexer.render());
97             String type = lexer.sval.toLowerCase();
98             match(lexer.ttype);
99             if (!isRelation()) {
100                 // It's a simple modifier consisting of type only
101                 ms.addModifier(type);
102             } else {
103                 // It's a complex modifier of the form type=value
104                 String comparision = lexer.render(lexer.ttype, false);
105                 match(lexer.ttype);
106                 String value = matchSymbol("modifier value");
107                 ms.addModifier(type, comparision, value);
108             }
109         }
110
111         return ms;
112     }
113
114     private CQLNode parseTerm(String index, CQLRelation relation)
115         throws CQLParseException, IOException {
116         debug("in parseTerm()");
117
118         String word;
119         while (true) {
120             if (lexer.ttype == '(') {
121                 debug("parenthesised term");
122                 match('(');
123                 CQLNode expr = parseQuery(index, relation);
124                 match(')');
125                 return expr;
126             } else if (lexer.ttype == '>') {
127                 match('>');
128                 return parsePrefix(index, relation);
129             }
130
131             debug("non-parenthesised term");
132             word = matchSymbol("index or term");
133             if (!isRelation() && lexer.ttype != lexer.TT_WORD)
134                 break;
135
136             index = word;
137             String relstr = (lexer.ttype == lexer.TT_WORD ?
138                              lexer.sval : lexer.render(lexer.ttype, false));
139             relation = new CQLRelation(relstr);
140             match(lexer.ttype);
141             ModifierSet ms = gatherModifiers(relstr);
142             relation.setModifiers(ms);
143             debug("index='" + index + ", " +
144                   "relation='" + relation.toCQL() + "'");
145         }
146
147         CQLTermNode node = new CQLTermNode(index, relation, word);
148         debug("made term node " + node.toCQL());
149         return node;
150     }
151
152     private CQLNode parsePrefix(String index, CQLRelation relation)
153         throws CQLParseException, IOException {
154         debug("prefix mapping");
155
156         String name = null;
157         String identifier = matchSymbol("prefix-name");
158         if (lexer.ttype == '=') {
159             match('=');
160             name = identifier;
161             identifier = matchSymbol("prefix-identifer");
162         }
163         CQLNode term = parseQuery(index, relation);
164         return new CQLPrefixNode(name, identifier, term);
165     }
166
167     // Checks for a relation
168     private boolean isRelation() {
169         debug("isRelation: checking ttype=" + lexer.ttype +
170               " (" + lexer.render() + ")");
171         return (lexer.ttype == '<' ||
172                 lexer.ttype == '>' ||
173                 lexer.ttype == '=' ||
174                 lexer.ttype == lexer.TT_LE ||
175                 lexer.ttype == lexer.TT_GE ||
176                 lexer.ttype == lexer.TT_NE);
177     }
178
179     private void match(int token)
180         throws CQLParseException, IOException {
181         debug("in match(" + lexer.render(token, true) + ")");
182         if (lexer.ttype != token)
183             throw new CQLParseException("expected " +
184                                         lexer.render(token, true) +
185                                         ", " + "got " + lexer.render());
186         int tmp = lexer.nextToken();
187         debug("match() got token=" + lexer.ttype + ", " +
188               "nval=" + lexer.nval + ", sval='" + lexer.sval + "'" +
189               " (tmp=" + tmp + ")");
190     }
191
192     private String matchSymbol(String expected)
193         throws CQLParseException, IOException {
194
195         debug("in matchSymbol()");
196         if (lexer.ttype == lexer.TT_WORD ||
197             lexer.ttype == lexer.TT_NUMBER ||
198             lexer.ttype == '"' ||
199             // The following is a complete list of keywords.  Because
200             // they're listed here, they can be used unquoted as
201             // indexes, terms, prefix names and prefix identifiers.
202             // ### Instead, we should ask the lexer whether what we
203             // have is a keyword, and let the knowledge reside there.
204             lexer.ttype == lexer.TT_AND ||
205             lexer.ttype == lexer.TT_OR ||
206             lexer.ttype == lexer.TT_NOT ||
207             lexer.ttype == lexer.TT_PROX) {
208             String symbol = (lexer.ttype == lexer.TT_NUMBER) ?
209                 lexer.render() : lexer.sval;
210             match(lexer.ttype);
211             return symbol;
212         }
213
214         throw new CQLParseException("expected " + expected + ", " +
215                                     "got " + lexer.render());
216     }
217
218
219     /**
220      * Simple test-harness for the CQLParser class.
221      * <P>
222      * Reads a CQL query either from its command-line argument, if
223      * there is one, or standard input otherwise.  So these two
224      * invocations are equivalent:
225      * <PRE>
226      *  CQLParser 'au=(Kerninghan or Ritchie) and ti=Unix'
227      *  echo au=(Kerninghan or Ritchie) and ti=Unix | CQLParser
228      * </PRE>
229      * The test-harness parses the supplied query and renders is as
230      * XCQL, so that both of the invocations above produce the
231      * following output:
232      * <PRE>
233      *  &lt;triple&gt;
234      *    &lt;boolean&gt;
235      *      &lt;value&gt;and&lt;/value&gt;
236      *    &lt;/boolean&gt;
237      *    &lt;triple&gt;
238      *      &lt;boolean&gt;
239      *        &lt;value&gt;or&lt;/value&gt;
240      *      &lt;/boolean&gt;
241      *      &lt;searchClause&gt;
242      *        &lt;index&gt;au&lt;/index&gt;
243      *        &lt;relation&gt;
244      *          &lt;value&gt;=&lt;/value&gt;
245      *        &lt;/relation&gt;
246      *        &lt;term&gt;Kerninghan&lt;/term&gt;
247      *      &lt;/searchClause&gt;
248      *      &lt;searchClause&gt;
249      *        &lt;index&gt;au&lt;/index&gt;
250      *        &lt;relation&gt;
251      *          &lt;value&gt;=&lt;/value&gt;
252      *        &lt;/relation&gt;
253      *        &lt;term&gt;Ritchie&lt;/term&gt;
254      *      &lt;/searchClause&gt;
255      *    &lt;/triple&gt;
256      *    &lt;searchClause&gt;
257      *      &lt;index&gt;ti&lt;/index&gt;
258      *      &lt;relation&gt;
259      *        &lt;value&gt;=&lt;/value&gt;
260      *      &lt;/relation&gt;
261      *      &lt;term&gt;Unix&lt;/term&gt;
262      *    &lt;/searchClause&gt;
263      *  &lt;/triple&gt;
264      * </PRE>
265      * <P>
266      * @param -c
267      *  Causes the output to be written in CQL rather than XCQL - that
268      *  is, a query equivalent to that which was input, is output.  In
269      *  effect, the test harness acts as a query canonicaliser.
270      * @return
271      *  The input query, either as XCQL [default] or CQL [if the
272      *  <TT>-c</TT> option is supplied].
273      */
274     public static void main (String[] args) {
275         char mode = 'x';        // x=XCQL, c=CQL, p=PQF
276         String pfile = null;
277
278         Vector<String> argv = new Vector<String>();
279         for (int i = 0; i < args.length; i++) {
280             argv.add(args[i]);
281         }
282
283         if (argv.size() > 0 && argv.get(0).equals("-d")) {
284             DEBUG = true;
285             argv.remove(0);
286         }
287
288         if (argv.size() > 0 && argv.get(0).equals("-c")) {
289             mode = 'c';
290             argv.remove(0);
291         } else if (argv.size() > 1 && argv.get(0).equals("-p")) {
292             mode = 'p';
293             argv.remove(0);
294             pfile = (String) argv.get(0);
295             argv.remove(0);
296         }
297
298         if (argv.size() > 1) {
299             System.err.println("Usage: CQLParser [-d] [-c] [-p <pqf-properties> [<CQL-query>]");
300             System.err.println("If unspecified, query is read from stdin");
301             System.exit(1);
302         }
303
304         String cql;
305         if (argv.size() == 1) {
306             cql = (String) argv.get(0);
307         } else {
308             byte[] bytes = new byte[10000];
309             try {
310                 // Read in the whole of standard input in one go
311                 int nbytes = System.in.read(bytes);
312             } catch (IOException ex) {
313                 System.err.println("Can't read query: " + ex.getMessage());
314                 System.exit(2);
315             }
316             cql = new String(bytes);
317         }
318
319         CQLParser parser = new CQLParser();
320         CQLNode root = null;
321         try {
322             root = parser.parse(cql);
323         } catch (CQLParseException ex) {
324             System.err.println("Syntax error: " + ex.getMessage());
325             System.exit(3);
326         } catch (IOException ex) {
327             System.err.println("Can't compile query: " + ex.getMessage());
328             System.exit(4);
329         }
330
331         try {
332             if (mode == 'c') {
333                 System.out.println(root.toCQL());
334             } else if (mode == 'p') {
335                 InputStream f = new FileInputStream(pfile);
336                 if (f == null)
337                     throw new FileNotFoundException(pfile);
338
339                 Properties config = new Properties();
340                 config.load(f);
341                 f.close();
342                 System.out.println(root.toPQF(config));
343             } else {
344                 System.out.print(root.toXCQL(0));
345             }
346         } catch (IOException ex) {
347             System.err.println("Can't render query: " + ex.getMessage());
348             System.exit(5);
349         } catch (UnknownIndexException ex) {
350             System.err.println("Unknown index: " + ex.getMessage());
351             System.exit(6);
352         } catch (UnknownRelationException ex) {
353             System.err.println("Unknown relation: " + ex.getMessage());
354             System.exit(7);
355         } catch (UnknownRelationModifierException ex) {
356             System.err.println("Unknown relation modifier: " +
357                                ex.getMessage());
358             System.exit(8);
359         } catch (UnknownPositionException ex) {
360             System.err.println("Unknown position: " + ex.getMessage());
361             System.exit(9);
362         } catch (PQFTranslationException ex) {
363             // We catch all of this class's subclasses, so --
364             throw new Error("can't get a PQFTranslationException");
365         }
366     }
367 }