Improve test-harnesses and their associated scripts.
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
1 // $Id: CQLParser.java,v 1.12 2002-11-01 23:45:28 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.12 2002-11-01 23:45:28 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 && lexer.ttype != '"') {
88                 throw new CQLParseException("expected qualifier or term, " +
89                                             "got " + lexer.render());
90             }
91
92             debug("non-parenthesised term");
93             word = lexer.sval;
94             match(lexer.ttype);
95             if (!isBaseRelation())
96                 break;
97
98             qualifier = word;
99             relation = new CQLRelation(lexer.render(lexer.ttype, false));
100             match(lexer.ttype);
101
102             while (lexer.ttype == '/') {
103                 match('/');
104                 if (lexer.ttype != lexer.TT_RELEVANT &&
105                     lexer.ttype != lexer.TT_FUZZY &&
106                     lexer.ttype != lexer.TT_STEM)
107                     throw new CQLParseException("expected relation modifier, "
108                                                 + "got " + lexer.render());
109                 relation.addModifier(lexer.sval);
110                 match(lexer.ttype);
111             }
112
113             debug("qualifier='" + qualifier + ", " +
114                   "relation='" + relation.toCQL() + "'");
115         }
116
117         CQLTermNode node = new CQLTermNode(qualifier, relation, word);
118         debug("made term node " + node.toCQL());
119         return node;
120     }
121
122     private void gatherProxParameters(CQLProxNode node)
123         throws CQLParseException, IOException {
124         for (int i = 0; i < 4; i++) {
125             if (lexer.ttype != '/')
126                 return;         // end of proximity parameters
127
128             match('/');
129             if (lexer.ttype != '/') {
130                 // not an omitted default
131                 switch (i) {
132                     // Assumes order is: relation/distance/unit/ordering
133                 case 0: gatherProxRelation(node); break;
134                 case 1: gatherProxDistance(node); break;
135                 case 2: gatherProxUnit(node); break;
136                 case 3: gatherProxOrdering(node); break;
137                 }
138             }
139         }
140     }
141
142     private void gatherProxRelation(CQLProxNode node)
143         throws CQLParseException, IOException {
144         if (!isProxRelation())
145             throw new CQLParseException("expected proximity relation, got " +
146                                         lexer.render());
147         node.addModifier("relation", lexer.render(lexer.ttype, false));
148         match(lexer.ttype);
149         debug("gPR matched " + lexer.render(lexer.ttype, false));
150     }
151
152     private void gatherProxDistance(CQLProxNode node)
153         throws CQLParseException, IOException {
154         if (lexer.ttype != lexer.TT_NUMBER)
155             throw new CQLParseException("expected proximity distance, got " +
156                                         lexer.render());
157         node.addModifier("distance", lexer.render(lexer.ttype, false));
158         match(lexer.ttype);
159         debug("gPD matched " + lexer.render(lexer.ttype, false));
160     }
161
162     private void gatherProxUnit(CQLProxNode node)
163         throws CQLParseException, IOException {
164         if (lexer.ttype != lexer.TT_pWORD &&
165             lexer.ttype != lexer.TT_SENTENCE &&
166             lexer.ttype != lexer.TT_PARAGRAPH &&
167             lexer.ttype != lexer.TT_ELEMENT)
168             throw new CQLParseException("expected proximity unit, got " +
169                                         lexer.render());
170         node.addModifier("unit", lexer.render());
171         match(lexer.ttype);
172     }
173
174     private void gatherProxOrdering(CQLProxNode node)
175         throws CQLParseException, IOException {
176         if (lexer.ttype != lexer.TT_ORDERED &&
177             lexer.ttype != lexer.TT_UNORDERED)
178             throw new CQLParseException("expected proximity ordering, got " +
179                                         lexer.render());
180         node.addModifier("ordering", lexer.render());
181         match(lexer.ttype);
182     }
183
184     boolean isBaseRelation() {
185         debug("isBaseRelation: checking ttype=" + lexer.ttype +
186               " (" + lexer.render() + ")");
187         return (isProxRelation() ||
188                 lexer.ttype == lexer.TT_ANY ||
189                 lexer.ttype == lexer.TT_ALL ||
190                 lexer.ttype == lexer.TT_EXACT);
191     }
192
193     boolean isProxRelation() {
194         debug("isProxRelation: checking ttype=" + lexer.ttype +
195               " (" + lexer.render() + ")");
196         return (lexer.ttype == '<' ||
197                 lexer.ttype == '>' ||
198                 lexer.ttype == '=' ||
199                 lexer.ttype == lexer.TT_LE ||
200                 lexer.ttype == lexer.TT_GE ||
201                 lexer.ttype == lexer.TT_NE);
202     }
203
204     private void match(int token)
205         throws CQLParseException, IOException {
206         debug("in match(" + lexer.render(token, true) + ")");
207         if (lexer.ttype != token)
208             throw new CQLParseException("expected " +
209                                         lexer.render(token, true) +
210                                         ", " + "got " + lexer.render());
211         int tmp = lexer.nextToken();
212         debug("match() got token=" + lexer.ttype + ", " +
213               "nval=" + lexer.nval + ", sval='" + lexer.sval + "'" +
214               " (tmp=" + tmp + ")");
215     }
216
217
218     // Test harness.
219     //
220     // e.g. echo '(au=Kerninghan or au=Ritchie) and ti=Unix' |
221     //                          java org.z3950.zing.cql.CQLParser
222     // yields:
223     //  <triple>
224     //    <boolean>and</boolean>
225     //    <triple>
226     //      <boolean>or</boolean>
227     //      <searchClause>
228     //        <index>au<index>
229     //        <relation>=<relation>
230     //        <term>Kerninghan<term>
231     //      </searchClause>
232     //      <searchClause>
233     //        <index>au<index>
234     //        <relation>=<relation>
235     //        <term>Ritchie<term>
236     //      </searchClause>
237     //    </triple>
238     //    <searchClause>
239     //      <index>ti<index>
240     //      <relation>=<relation>
241     //      <term>Unix<term>
242     //    </searchClause>
243     //  </triple>
244     //
245     public static void main (String[] args) {
246         boolean canonicalise = false;
247         Vector argv = new Vector();
248         for (int i = 0; i < args.length; i++) {
249             argv.add(args[i]);
250         }
251
252         if (argv.size() > 0 && argv.get(0).equals("-c")) {
253             canonicalise = true;
254             argv.remove(0);
255         }
256
257         if (argv.size() > 1) {
258             System.err.println("Usage: CQLParser [-c] [<CQL-query>]");
259             System.err.println("If unspecified, query is read from stdin");
260             System.exit(1);
261         }
262
263         String cql;
264         if (argv.size() == 1) {
265             cql = (String) argv.get(0);
266         } else {
267             byte[] bytes = new byte[10000];
268             try {
269                 // Read in the whole of standard input in one go
270                 int nbytes = System.in.read(bytes);
271             } catch (java.io.IOException ex) {
272                 System.err.println("Can't read query: " + ex.getMessage());
273                 System.exit(2);
274             }
275             cql = new String(bytes);
276         }
277
278         CQLParser parser = new CQLParser();
279         CQLNode root;
280         try {
281             root = parser.parse(cql);
282             debug("root='" + root + "'");
283             if (canonicalise) {
284                 System.out.println(root.toCQL());
285             } else {
286                 System.out.println(root.toXCQL(0));
287             }
288         } catch (CQLParseException ex) {
289             System.err.println("Syntax error: " + ex.getMessage());
290             System.exit(3);
291         } catch (java.io.IOException ex) {
292             System.err.println("Can't compile query: " + ex.getMessage());
293             System.exit(4);
294         }
295     }
296 }