Use $Id$ instead of $Header$.
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
1 // $Id: CQLParser.java,v 1.3 2002-10-24 16:06:34 mike Exp $
2
3 package org.z3950.zing.cql;
4 import java.util.Properties;
5 import java.io.InputStream;
6 import java.io.FileNotFoundException;
7 import java.io.IOException;
8 import java.io.StringReader;
9 import java.io.StreamTokenizer;
10
11
12 /**
13  * Compiles a CQL string into a parse tree ...
14  * ###
15  *
16  * @version     $Id: CQLParser.java,v 1.3 2002-10-24 16:06:34 mike Exp $
17  * @see         <A href="http://zing.z3950.org/cql/index.html"
18  *                      >http://zing.z3950.org/cql/index.html</A>
19  */
20 class CQLCompiler {
21     private String cql;
22     private String qualset;
23     private Properties qualsetProperties;
24     private StreamTokenizer st;
25
26     private class CQLParseException extends Exception {
27         CQLParseException(String s) { super(s); }
28     }
29
30     public CQLCompiler(String cql, String qualset) {
31         this.cql = cql;
32         this.qualset = qualset;
33     }
34
35     public String convertToPQN()
36         throws FileNotFoundException, IOException {
37
38         if (qualsetProperties == null) {
39             //      ### Could think about caching named qualifier sets
40             //          across compilations (i.e. shared, in a static
41             //          Hashtable, between multiple CQLCompiler
42             //          instances.)  Probably not worth it.
43             InputStream is = this.getClass().getResourceAsStream(qualset);
44             if (is == null)
45                 throw new FileNotFoundException("getResourceAsStream(" +
46                                                 qualset + ")");
47             qualsetProperties = new Properties();
48             qualsetProperties.load(is);
49         }
50
51         st = new StreamTokenizer(new StringReader(cql));
52         st.wordChars('/', '/');
53         st.wordChars('0', '9'); // ### but 1 is still recognised as TT_NUM
54         st.wordChars('.', '.');
55         st.wordChars('-', '-');
56         st.ordinaryChar('=');
57         st.ordinaryChar(',');
58         st.ordinaryChar('(');
59         st.ordinaryChar(')');
60
61 //      int token;
62 //      while ((token = st.nextToken()) != st.TT_EOF) {
63 //          System.out.println("token=" + token + ", " +
64 //                             "nval=" + st.nval + ", " +
65 //                             "sval=" + st.sval);
66 //      }
67
68         st.nextToken();
69         String ret;
70         try {
71             ret = parse_expression();
72         } catch (CQLParseException ex) {
73             System.err.println("### Oops: " + ex);
74             return null;
75         }
76
77         if (st.ttype != st.TT_EOF) {
78             System.err.println("### Extra bits: " + render(st));
79             return null;
80         }
81
82         // Interpret attributes as BIB-1 unless otherwise specified
83         return "@attrset bib-1 " + ret;
84     }
85
86     private String parse_expression()
87         throws CQLParseException, IOException {
88         String term = parse_term();
89
90         while (st.ttype == st.TT_WORD) {
91             String op = st.sval.toLowerCase();
92             if (!st.sval.equals("and") &&
93                 !st.sval.equals("or") &&
94                 !st.sval.equals("not"))
95                 break;
96             match(st.TT_WORD);
97             String term2 = parse_term();
98             term = "@" + op + " " + term + " " + term2;
99         }
100
101         return term;
102     }
103
104     private String parse_term()
105         throws CQLParseException, IOException {
106         if (st.ttype == '(') {
107             match('(');
108             String expr = parse_expression();
109             match(')');
110             return expr;
111         }
112
113         String word = null;
114         String attrs = "";
115
116         // ### We treat ',' and '=' equivalently here, which isn't quite right.
117         while (st.ttype == st.TT_WORD) {
118             word = st.sval;
119             match(st.TT_WORD);
120             if (st.ttype != '=' && st.ttype != ',') {
121                 // end of qualifer list
122                 break;
123             }
124
125             String attr = qualsetProperties.getProperty(word);
126             if (attr == null) {
127                 throw new CQLParseException("unrecognised qualifier: " + word);
128             }
129             attrs = attrs + attr + " ";
130             match(st.ttype);
131             word = null;        // mark as not-yet-read
132         }
133
134         if (word == null) {
135             // got to the end of a "foo,bar=" sequence
136             word = st.sval;
137             if (st.ttype != '\'' || st.ttype != '"') {
138                 word = "\"" + word + "\"";
139                 match(st.ttype);
140             } else {
141                 match(st.TT_WORD);
142             }
143         }
144
145         return attrs + word;
146     }
147
148     private void match(int token)
149         throws CQLParseException, IOException {
150         if (st.ttype != token)
151             throw new CQLParseException("expected " + render(st, token, null) +
152                                         ", " + "got " + render(st));
153         st.nextToken();
154     }
155
156     // ### This utility should surely be a method of the StreamTokenizer class
157     private static String render(StreamTokenizer st) {
158         return render(st, st.ttype, null);
159     }
160
161     private static String render(StreamTokenizer st, int token, String str) {
162         String ret;
163
164         switch (token) {
165         case st.TT_EOF: return "EOF";
166         case st.TT_EOL: return "EOL";
167         case st.TT_NUMBER: return "number";
168         case st.TT_WORD: ret = "word"; break;
169         case '"': case '\'': ret = "string"; break;
170         default: return "'" + String.valueOf((char) token) + "'";
171         }
172
173         if (str != null)
174             ret += "(\"" + str + "\")";
175         return ret;
176     }
177
178     // ### Not really the right place for this test harness.
179     //
180     // e.g. java uk.org.miketaylor.zoom.CQLCompiler
181     //          '(au=Kerninghan or au=Ritchie) and ti=Unix' qualset.properties
182     // yields:
183     //  @and
184     //          @or
185     //                  @attr 1=1 @attr 4=1 Kerninghan
186     //                  @attr 1=1 @attr 4=1 Ritchie
187     //          @attr 1=4 @attr 4=1 Unix
188     //
189     public static void main (String[] args) {
190         if (args.length != 2) {
191             System.err.println("Usage: CQLQuery <cql> <qualset>");
192             System.exit(1);
193         }
194
195         CQLCompiler cc = new CQLCompiler(args[0], args[1]);
196         try {
197             String pqn = cc.convertToPQN();
198             System.out.println(pqn);
199         } catch (FileNotFoundException ex) {
200             System.err.println("Can't find qualifier set: " + ex);
201             System.exit(2);
202         } catch (IOException ex) {
203             System.err.println("Can't read qualifier set: " + ex);
204             System.exit(2);
205         }
206     }
207 }