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