01cc05dd1ae1d16c53216f9277b8cfa18b13724a
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLLexer.java
1 // $Id: CQLLexer.java,v 1.13 2007-06-29 15:38:56 mike Exp $
2
3 package org.z3950.zing.cql;
4 import java.io.StreamTokenizer;
5 import java.io.StringReader;
6 import java.util.Hashtable;
7
8
9 // This is a semi-trivial subclass for java.io.StreamTokenizer that:
10 //      * Has a halfDecentPushBack() method that actually works
11 //      * Includes a render() method
12 //      * Knows about the multi-character tokens "<=", ">=" and "<>"
13 //      * Recognises a set of keywords as tokens in their own right
14 //      * Includes some primitive debugging-output facilities
15 // It's used only by CQLParser.
16 //
17 class CQLLexer extends StreamTokenizer {
18     // New publicly visible token-types
19     static int TT_LE        = 1000;     // The "<=" relation
20     static int TT_GE        = 1001;     // The ">=" relation
21     static int TT_NE        = 1002;     // The "<>" relation
22     static int TT_EQEQ      = 1003;     // The "==" relation
23     static int TT_AND       = 1004;     // The "and" boolean
24     static int TT_OR        = 1005;     // The "or" boolean
25     static int TT_NOT       = 1006;     // The "not" boolean
26     static int TT_PROX      = 1007;     // The "prox" boolean
27
28     // Support for keywords.  It would be nice to compile this linear
29     // list into a Hashtable, but it's hard to store ints as hash
30     // values, and next to impossible to use them as hash keys.  So
31     // we'll just scan the (very short) list every time we need to do
32     // a lookup.
33     private class Keyword {
34         int token;
35         String keyword;
36         Keyword(int token, String keyword) {
37             this.token = token;
38             this.keyword = keyword;
39         }
40     }
41     // This should logically be static, but Java won't allow it  :-P
42     private Keyword[] keywords = {
43         new Keyword(TT_AND, "and"),
44         new Keyword(TT_OR,  "or"),
45         new Keyword(TT_NOT, "not"),
46         new Keyword(TT_PROX, "prox"),
47     };
48
49     // For halfDecentPushBack() and the code at the top of nextToken()
50     private static int TT_UNDEFINED = -1000;
51     private int saved_ttype = TT_UNDEFINED;
52     private double saved_nval;
53     private String saved_sval;
54
55     // Controls debugging output
56     private static boolean DEBUG;
57
58     CQLLexer(String cql, boolean lexdebug) {
59         super(new StringReader(cql));
60         wordChars('!', '?');    // ASCII-dependency!
61         wordChars('[', '`');    // ASCII-dependency!
62         quoteChar('"');
63         ordinaryChar('=');
64         ordinaryChar('<');
65         ordinaryChar('>');
66         ordinaryChar('/');
67         ordinaryChar('(');
68         ordinaryChar(')');
69         wordChars('\'', '\''); // prevent this from introducing strings
70         parseNumbers();
71         DEBUG = lexdebug;
72     }
73
74     private static void debug(String str) {
75         if (DEBUG)
76             System.err.println("LEXDEBUG: " + str);
77     }
78
79     // I don't honestly understand why we need this, but the
80     // documentation for java.io.StreamTokenizer.pushBack() is pretty
81     // vague about its semantics, and it seems to me that they could
82     // be summed up as "it doesn't work".  This version has the very
83     // clear semantics "pretend I didn't call nextToken() just then".
84     //
85     private void halfDecentPushBack() {
86         saved_ttype = ttype;
87         saved_nval = nval;
88         saved_sval = sval;
89     }
90
91     public int nextToken() throws java.io.IOException {
92         if (saved_ttype != TT_UNDEFINED) {
93             ttype = saved_ttype;
94             nval = saved_nval;
95             sval = saved_sval;
96             saved_ttype = TT_UNDEFINED;
97             debug("using saved ttype=" + ttype + ", " +
98                   "nval=" + nval + ", sval='" + sval + "'");
99             return ttype;
100         }
101
102         underlyingNextToken();
103         if (ttype == '<') {
104             debug("token starts with '<' ...");
105             underlyingNextToken();
106             if (ttype == '=') {
107                 debug("token continues with '=' - it's '<='");
108                 ttype = TT_LE;
109             } else if (ttype == '>') {
110                 debug("token continues with '>' - it's '<>'");
111                 ttype = TT_NE;
112             } else {
113                 debug("next token is " + render() + " (pushed back)");
114                 halfDecentPushBack();
115                 ttype = '<';
116                 debug("AFTER: ttype is now " + ttype + " - " + render());
117             }
118         } else if (ttype == '>') {
119             debug("token starts with '>' ...");
120             underlyingNextToken();
121             if (ttype == '=') {
122                 debug("token continues with '=' - it's '>='");
123                 ttype = TT_GE;
124             } else {
125                 debug("next token is " + render() + " (pushed back)");
126                 halfDecentPushBack();
127                 ttype = '>';
128                 debug("AFTER: ttype is now " + ttype + " - " + render());
129             }
130         } else if (ttype == '=') {
131             debug("token starts with '=' ...");
132             underlyingNextToken();
133             if (ttype == '=') {
134                 debug("token continues with '=' - it's '=='");
135                 ttype = TT_EQEQ;
136             } else {
137                 debug("next token is " + render() + " (pushed back)");
138                 halfDecentPushBack();
139                 ttype = '=';
140                 debug("AFTER: ttype is now " + ttype + " - " + render());
141             }
142         }
143
144         debug("done nextToken(): ttype=" + ttype + ", " +
145               "nval=" + nval + ", " + "sval='" + sval + "'" +
146               " (" + render() + ")");
147
148         return ttype;
149     }
150
151     // It's important to do keyword recognition here at the lowest
152     // level, otherwise when one of these words follows "<" or ">"
153     // (which can be the beginning of multi-character tokens) it gets
154     // pushed back as a string, and its keywordiness is not
155     // recognised.
156     //
157     public int underlyingNextToken() throws java.io.IOException {
158         super.nextToken();
159         if (ttype == TT_WORD)
160             for (int i = 0; i < keywords.length; i++)
161                 if (sval.equalsIgnoreCase(keywords[i].keyword))
162                     ttype = keywords[i].token;
163
164         return ttype;
165     }
166
167     // Simpler interface for the usual case: current token with quoting
168     String render() {
169         return render(ttype, true);
170     }
171
172     String render(int token, boolean quoteChars) {
173         if (token == TT_EOF) {
174             return "EOF";
175         } else if (token == TT_NUMBER) {
176             if ((double) nval == (int) nval) {
177                 return new Integer((int) nval).toString();
178             } else {
179                 return new Double((double) nval).toString();
180             }
181         } else if (token == TT_WORD) {
182             return "word: " + sval;
183         } else if (token == '"') {
184             return "string: \"" + sval + "\"";
185         } else if (token == TT_LE) {
186             return "<=";
187         } else if (token == TT_GE) {
188             return ">=";
189         } else if (token == TT_NE) {
190             return "<>";
191         } else if (token == TT_EQEQ) {
192             return "==";
193         }
194
195         // Check whether its associated with one of the keywords
196         for (int i = 0; i < keywords.length; i++)
197             if (token == keywords[i].token)
198                 return keywords[i].keyword;
199
200         // Otherwise it must be a single character, such as '(' or '/'.
201         String res = String.valueOf((char) token);
202         if (quoteChars) res = "'" + res + "'";
203         return res;
204     }
205
206     public static void main(String[] args) throws Exception {
207         if (args.length > 1) {
208             System.err.println("Usage: CQLLexer [<CQL-query>]");
209             System.err.println("If unspecified, query is read from stdin");
210             System.exit(1);
211         }
212
213         String cql;
214         if (args.length == 1) {
215             cql = args[0];
216         } else {
217             byte[] bytes = new byte[10000];
218             try {
219                 // Read in the whole of standard input in one go
220                 int nbytes = System.in.read(bytes);
221             } catch (java.io.IOException ex) {
222                 System.err.println("Can't read query: " + ex.getMessage());
223                 System.exit(2);
224             }
225             cql = new String(bytes);
226         }
227
228         CQLLexer lexer = new CQLLexer(cql, true);
229         int token;
230         while ((token = lexer.nextToken()) != TT_EOF) {
231             // Nothing to do: debug() statements render tokens for us
232         }
233     }
234 }