First more-or-less working version: does terms, booleans, qualifiers
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
1 // $Id: CQLParser.java,v 1.8 2002-10-27 00:46:25 mike Exp $
2
3 package org.z3950.zing.cql;
4 import java.util.Properties;
5 import java.io.InputStream;
6 import java.io.IOException;
7 import java.io.StringReader;
8 import java.io.StreamTokenizer;
9
10
11 /**
12  * Compiles a CQL string into a parse tree ...
13  * ###
14  *
15  * @version     $Id: CQLParser.java,v 1.8 2002-10-27 00:46:25 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 PARSEDEBUG = false;
22     static private boolean LEXDEBUG = false;
23
24     private class CQLParseException extends Exception {
25         CQLParseException(String s) { super(s); }
26     }
27
28     static void debug(String str) {
29         if (PARSEDEBUG)
30             System.err.println("PARSEDEBUG: " + str);
31     }
32
33     public CQLNode parse(String cql)
34         throws CQLParseException, IOException {
35         lexer = new CQLLexer(cql, LEXDEBUG);
36
37         lexer.nextToken();
38         debug("about to parse_query()");
39         CQLNode root = parse_query("srw.serverChoice", "=");
40         if (lexer.ttype != lexer.TT_EOF)
41             throw new CQLParseException("junk after end: " + lexer.render());
42
43         return root;
44     }
45
46     private CQLNode parse_query(String qualifier, String relation)
47         throws CQLParseException, IOException {
48         debug("in parse_query()");
49
50         CQLNode term = parse_term(qualifier, relation);
51         while (lexer.ttype == lexer.TT_WORD) {
52             String op = lexer.sval.toLowerCase();
53             debug("checking op '" + op + "'");
54             if (lexer.sval.equals("and")) {
55                 match(lexer.TT_WORD);
56                 CQLNode term2 = parse_term(qualifier, relation);
57                 term = new CQLAndNode(term, term2);
58             } else if (lexer.sval.equals("or")) {
59                 match(lexer.TT_WORD);
60                 CQLNode term2 = parse_term(qualifier, relation);
61                 term = new CQLOrNode(term, term2);
62             } else if (lexer.sval.equals("not")) {
63                 match(lexer.TT_WORD);
64                 CQLNode term2 = parse_term(qualifier, relation);
65                 term = new CQLNotNode(term, term2);
66             } else if (lexer.sval.equals("prox")) {
67                 // ### Handle "prox"
68             } else {
69                 throw new CQLParseException("unrecognised boolean: '" +
70                                             lexer.sval + "'");
71             }
72         }
73
74         debug("no more ops");
75         return term;
76     }
77
78     private CQLNode parse_term(String qualifier, String relation)
79         throws CQLParseException, IOException {
80         debug("in parse_term()");
81
82         String word;
83         while (true) {
84             if (lexer.ttype == '(') {
85                 debug("parenthesised term");
86                 match('(');
87                 CQLNode expr = parse_query(qualifier, relation);
88                 match(')');
89                 return expr;
90             } else if (lexer.ttype != lexer.TT_WORD && lexer.ttype != '"') {
91                 throw new CQLParseException("expected qualifier or term, " +
92                                             "got " + lexer.render());
93             }
94
95             debug("non-parenthesised term");
96             word = lexer.sval;
97             match(lexer.ttype);
98             if (!isRelation())
99                 break;
100
101             qualifier = word;
102             relation = lexer.render(false);
103             match(lexer.ttype);
104             debug("qualifier='" + qualifier + ", relation='" + relation + "'");
105         }
106
107         CQLTermNode node = new CQLTermNode(qualifier, relation, word);
108         debug("made term node " + node);
109         return node;
110     }
111
112     boolean isRelation() {
113         // ### Also need to handle <=, >=, <>
114         return (lexer.ttype == '<' ||
115                 lexer.ttype == '>' ||
116                 lexer.ttype == '=');
117     }
118
119     private void match(int token)
120         throws CQLParseException, IOException {
121         debug("in match(" + lexer.render(token, null, true) + ")");
122         if (lexer.ttype != token)
123             throw new CQLParseException("expected " +
124                                         lexer.render(token, null, true) +
125                                         ", " + "got " + lexer.render());
126         lexer.nextToken();
127     }
128
129
130     // Test harness.
131     //
132     // e.g. echo '(au=Kerninghan or au=Ritchie) and ti=Unix' |
133     //                          java org.z3950.zing.cql.CQLParser
134     // yields:
135     //  <triple>
136     //    <boolean>and</boolean>
137     //    <triple>
138     //      <boolean>or</boolean>
139     //      <searchClause>
140     //        <index>au<index>
141     //        <relation>=<relation>
142     //        <term>Kerninghan<term>
143     //      </searchClause>
144     //      <searchClause>
145     //        <index>au<index>
146     //        <relation>=<relation>
147     //        <term>Ritchie<term>
148     //      </searchClause>
149     //    </triple>
150     //    <searchClause>
151     //      <index>ti<index>
152     //      <relation>=<relation>
153     //      <term>Unix<term>
154     //    </searchClause>
155     //  </triple>
156     //
157     public static void main (String[] args) {
158         if (args.length != 0) {
159             System.err.println("Usage: " + args[0]);
160             System.exit(1);
161         }
162
163         byte[] bytes = new byte[10000];
164         try {
165             // Read in the whole of standard input in one go
166             int nbytes = System.in.read(bytes);
167         } catch (java.io.IOException ex) {
168             System.err.println("Can't read query: " + ex.getMessage());
169             System.exit(2);
170         }
171         String cql = new String(bytes);
172         CQLParser parser = new CQLParser();
173         CQLNode root;
174         try {
175             root = parser.parse(cql);
176             debug("root='" + root + "'");
177             System.out.println(root.toXCQL(0));
178         } catch (CQLParseException ex) {
179             System.err.println("Syntax error: " + ex.getMessage());
180             System.exit(3);
181         } catch (java.io.IOException ex) {
182             System.err.println("Can't compile query: " + ex.getMessage());
183             System.exit(4);
184         }
185     }
186 }
187
188
189 // This is a trivial subclass for java.io.StreamTokenizer which knows
190 // about the multi-character tokens "<=", ">=" and "<>", and included
191 // a render() method.  Used only by CQLParser.
192 //
193 class CQLLexer extends StreamTokenizer {
194     private static boolean lexdebug;
195
196     CQLLexer(String cql, boolean lexdebug) {
197         super(new StringReader(cql));
198         this.ordinaryChar('=');
199         this.ordinaryChar('<');
200         this.ordinaryChar('>');
201         this.ordinaryChar('/');
202         this.ordinaryChar('(');
203         this.ordinaryChar(')');
204         this.wordChars('\'', '\''); // prevent this from introducing strings
205         this.lexdebug = lexdebug;
206     }
207
208     public int nextToken() throws java.io.IOException {
209         int token = super.nextToken();
210         if (lexdebug)
211             System.out.println("LEXDEBUG: " +
212                                "token=" + token + ", " +
213                                "nval=" + this.nval + ", " +
214                                "sval=" + this.sval);
215
216         return token;
217     }
218
219     String render() {
220         return this.render(this.ttype, null, true);
221     }
222
223     String render(boolean quoteChars) {
224         return this.render(this.ttype, null, quoteChars);
225     }
226
227     String render(int token, String str, boolean quoteChars) {
228         String ret;
229
230         if (token == this.TT_EOF) {
231             return "EOF";
232         } else if (token == this.TT_EOL) {
233             return "EOL";
234         } else if (token == this.TT_NUMBER) {
235             return "number: " + this.nval;
236         } else if (token == this.TT_WORD) {
237             return "word: \"" + this.sval + "\"";
238         } else if (token == '"') {
239             return "string: \"" + this.sval + "\"";
240         }
241
242         String res = String.valueOf((char) token);
243         if (quoteChars) res = "'" + res + "'";
244         return res;
245     }
246 }