Recognise number tokens as terms, as well as words and strings.
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
1 // $Id: CQLParser.java,v 1.13 2002-11-02 01:24:14 mike Exp $
2
3 package org.z3950.zing.cql;
4 import java.io.IOException;
5 import java.util.Vector;
6
7
8 /**
9  * Compiles a CQL string into a parse tree.
10  * ##
11  *
12  * @version     $Id: CQLParser.java,v 1.13 2002-11-02 01:24:14 mike Exp $
13  * @see         <A href="http://zing.z3950.org/cql/index.html"
14  *                      >http://zing.z3950.org/cql/index.html</A>
15  */
16 public class CQLParser {
17     private CQLLexer lexer;
18     static private boolean DEBUG = false;
19     static private boolean LEXDEBUG = false;
20
21     private static void debug(String str) {
22         if (DEBUG)
23             System.err.println("PARSEDEBUG: " + str);
24     }
25
26     public CQLNode parse(String cql)
27         throws CQLParseException, IOException {
28         lexer = new CQLLexer(cql, LEXDEBUG);
29
30         lexer.nextToken();
31         debug("about to parse_query()");
32         CQLNode root = parse_query("srw.serverChoice", new CQLRelation("="));
33         if (lexer.ttype != lexer.TT_EOF)
34             throw new CQLParseException("junk after end: " + lexer.render());
35
36         return root;
37     }
38
39     private CQLNode parse_query(String qualifier, CQLRelation relation)
40         throws CQLParseException, IOException {
41         debug("in parse_query()");
42
43         CQLNode term = parse_term(qualifier, relation);
44         while (lexer.ttype != lexer.TT_EOF &&
45                lexer.ttype != ')') {
46             if (lexer.ttype == lexer.TT_AND) {
47                 match(lexer.TT_AND);
48                 CQLNode term2 = parse_term(qualifier, relation);
49                 term = new CQLAndNode(term, term2);
50             } else if (lexer.ttype == lexer.TT_OR) {
51                 match(lexer.TT_OR);
52                 CQLNode term2 = parse_term(qualifier, relation);
53                 term = new CQLOrNode(term, term2);
54             } else if (lexer.ttype == lexer.TT_NOT) {
55                 match(lexer.TT_NOT);
56                 CQLNode term2 = parse_term(qualifier, relation);
57                 term = new CQLNotNode(term, term2);
58             } else if (lexer.ttype == lexer.TT_PROX) {
59                 match(lexer.TT_PROX);
60                 CQLProxNode proxnode = new CQLProxNode(term);
61                 gatherProxParameters(proxnode);
62                 CQLNode term2 = parse_term(qualifier, relation);
63                 proxnode.addSecondSubterm(term2);
64                 term = (CQLNode) proxnode;
65             } else {
66                 throw new CQLParseException("expected boolean, got " +
67                                             lexer.render());
68             }
69         }
70
71         debug("no more ops");
72         return term;
73     }
74
75     private CQLNode parse_term(String qualifier, CQLRelation relation)
76         throws CQLParseException, IOException {
77         debug("in parse_term()");
78
79         String word;
80         while (true) {
81             if (lexer.ttype == '(') {
82                 debug("parenthesised term");
83                 match('(');
84                 CQLNode expr = parse_query(qualifier, relation);
85                 match(')');
86                 return expr;
87             } else if (lexer.ttype != lexer.TT_WORD &&
88                        lexer.ttype != lexer.TT_NUMBER &&
89                        lexer.ttype != '"') {
90                 throw new CQLParseException("expected qualifier or term, " +
91                                             "got " + lexer.render());
92             }
93
94             debug("non-parenthesised term");
95             if (lexer.ttype == lexer.TT_NUMBER) {
96                 word = lexer.render();
97             } else {
98                 word = lexer.sval;
99             }
100             match(lexer.ttype);
101             if (!isBaseRelation())
102                 break;
103
104             qualifier = word;
105             relation = new CQLRelation(lexer.render(lexer.ttype, false));
106             match(lexer.ttype);
107
108             while (lexer.ttype == '/') {
109                 match('/');
110                 if (lexer.ttype != lexer.TT_RELEVANT &&
111                     lexer.ttype != lexer.TT_FUZZY &&
112                     lexer.ttype != lexer.TT_STEM)
113                     throw new CQLParseException("expected relation modifier, "
114                                                 + "got " + lexer.render());
115                 relation.addModifier(lexer.sval);
116                 match(lexer.ttype);
117             }
118
119             debug("qualifier='" + qualifier + ", " +
120                   "relation='" + relation.toCQL() + "'");
121         }
122
123         CQLTermNode node = new CQLTermNode(qualifier, relation, word);
124         debug("made term node " + node.toCQL());
125         return node;
126     }
127
128     private void gatherProxParameters(CQLProxNode node)
129         throws CQLParseException, IOException {
130         for (int i = 0; i < 4; i++) {
131             if (lexer.ttype != '/')
132                 return;         // end of proximity parameters
133
134             match('/');
135             if (lexer.ttype != '/') {
136                 // not an omitted default
137                 switch (i) {
138                     // Order should be: relation/distance/unit/ordering
139                     // For now, use MA's: unit/relation/distance/ordering
140                 case 1: gatherProxRelation(node); break;
141                 case 2: gatherProxDistance(node); break;
142                 case 0: gatherProxUnit(node); break;
143                 case 3: gatherProxOrdering(node); break;
144                 }
145             }
146         }
147     }
148
149     private void gatherProxRelation(CQLProxNode node)
150         throws CQLParseException, IOException {
151         if (!isProxRelation())
152             throw new CQLParseException("expected proximity relation, got " +
153                                         lexer.render());
154         node.addModifier("relation", lexer.render(lexer.ttype, false));
155         match(lexer.ttype);
156         debug("gPR matched " + lexer.render(lexer.ttype, false));
157     }
158
159     private void gatherProxDistance(CQLProxNode node)
160         throws CQLParseException, IOException {
161         if (lexer.ttype != lexer.TT_NUMBER)
162             throw new CQLParseException("expected proximity distance, got " +
163                                         lexer.render());
164         node.addModifier("distance", lexer.render(lexer.ttype, false));
165         match(lexer.ttype);
166         debug("gPD matched " + lexer.render(lexer.ttype, false));
167     }
168
169     private void gatherProxUnit(CQLProxNode node)
170         throws CQLParseException, IOException {
171         if (lexer.ttype != lexer.TT_pWORD &&
172             lexer.ttype != lexer.TT_SENTENCE &&
173             lexer.ttype != lexer.TT_PARAGRAPH &&
174             lexer.ttype != lexer.TT_ELEMENT)
175             throw new CQLParseException("expected proximity unit, got " +
176                                         lexer.render());
177         node.addModifier("unit", lexer.render());
178         match(lexer.ttype);
179     }
180
181     private void gatherProxOrdering(CQLProxNode node)
182         throws CQLParseException, IOException {
183         if (lexer.ttype != lexer.TT_ORDERED &&
184             lexer.ttype != lexer.TT_UNORDERED)
185             throw new CQLParseException("expected proximity ordering, got " +
186                                         lexer.render());
187         node.addModifier("ordering", lexer.render());
188         match(lexer.ttype);
189     }
190
191     boolean isBaseRelation() {
192         debug("isBaseRelation: checking ttype=" + lexer.ttype +
193               " (" + lexer.render() + ")");
194         return (isProxRelation() ||
195                 lexer.ttype == lexer.TT_ANY ||
196                 lexer.ttype == lexer.TT_ALL ||
197                 lexer.ttype == lexer.TT_EXACT);
198     }
199
200     boolean isProxRelation() {
201         debug("isProxRelation: checking ttype=" + lexer.ttype +
202               " (" + lexer.render() + ")");
203         return (lexer.ttype == '<' ||
204                 lexer.ttype == '>' ||
205                 lexer.ttype == '=' ||
206                 lexer.ttype == lexer.TT_LE ||
207                 lexer.ttype == lexer.TT_GE ||
208                 lexer.ttype == lexer.TT_NE);
209     }
210
211     private void match(int token)
212         throws CQLParseException, IOException {
213         debug("in match(" + lexer.render(token, true) + ")");
214         if (lexer.ttype != token)
215             throw new CQLParseException("expected " +
216                                         lexer.render(token, true) +
217                                         ", " + "got " + lexer.render());
218         int tmp = lexer.nextToken();
219         debug("match() got token=" + lexer.ttype + ", " +
220               "nval=" + lexer.nval + ", sval='" + lexer.sval + "'" +
221               " (tmp=" + tmp + ")");
222     }
223
224
225     // Test harness.
226     //
227     // e.g. echo '(au=Kerninghan or au=Ritchie) and ti=Unix' |
228     //                          java org.z3950.zing.cql.CQLParser
229     // yields:
230     //  <triple>
231     //    <boolean>and</boolean>
232     //    <triple>
233     //      <boolean>or</boolean>
234     //      <searchClause>
235     //        <index>au<index>
236     //        <relation>=<relation>
237     //        <term>Kerninghan<term>
238     //      </searchClause>
239     //      <searchClause>
240     //        <index>au<index>
241     //        <relation>=<relation>
242     //        <term>Ritchie<term>
243     //      </searchClause>
244     //    </triple>
245     //    <searchClause>
246     //      <index>ti<index>
247     //      <relation>=<relation>
248     //      <term>Unix<term>
249     //    </searchClause>
250     //  </triple>
251     //
252     public static void main (String[] args) {
253         boolean canonicalise = false;
254         Vector argv = new Vector();
255         for (int i = 0; i < args.length; i++) {
256             argv.add(args[i]);
257         }
258
259         if (argv.size() > 0 && argv.get(0).equals("-c")) {
260             canonicalise = true;
261             argv.remove(0);
262         }
263
264         if (argv.size() > 1) {
265             System.err.println("Usage: CQLParser [-c] [<CQL-query>]");
266             System.err.println("If unspecified, query is read from stdin");
267             System.exit(1);
268         }
269
270         String cql;
271         if (argv.size() == 1) {
272             cql = (String) argv.get(0);
273         } else {
274             byte[] bytes = new byte[10000];
275             try {
276                 // Read in the whole of standard input in one go
277                 int nbytes = System.in.read(bytes);
278             } catch (java.io.IOException ex) {
279                 System.err.println("Can't read query: " + ex.getMessage());
280                 System.exit(2);
281             }
282             cql = new String(bytes);
283         }
284
285         CQLParser parser = new CQLParser();
286         CQLNode root;
287         try {
288             root = parser.parse(cql);
289             debug("root='" + root + "'");
290             if (canonicalise) {
291                 System.out.println(root.toCQL());
292             } else {
293                 System.out.print(root.toXCQL(0));
294             }
295         } catch (CQLParseException ex) {
296             System.err.println("Syntax error: " + ex.getMessage());
297             System.exit(3);
298         } catch (java.io.IOException ex) {
299             System.err.println("Can't compile query: " + ex.getMessage());
300             System.exit(4);
301         }
302     }
303 }