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