LOTS of changes. Biggies include proper support for relations, including
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
index 0684993..e463576 100644 (file)
-// $Id: CQLParser.java,v 1.5 2002-10-25 16:01:26 mike Exp $
+// $Id: CQLParser.java,v 1.10 2002-10-30 09:19:26 mike Exp $
 
 package org.z3950.zing.cql;
-import java.util.Properties;
-import java.io.InputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.StringReader;
-import java.io.StreamTokenizer;
 
 
 /**
- * Compiles a CQL string into a parse tree ...
+ * Compiles a CQL string into a parse tree.
  * ###
  *
- * @version    $Id: CQLParser.java,v 1.5 2002-10-25 16:01:26 mike Exp $
+ * @version    $Id: CQLParser.java,v 1.10 2002-10-30 09:19:26 mike Exp $
  * @see                <A href="http://zing.z3950.org/cql/index.html"
  *                     >http://zing.z3950.org/cql/index.html</A>
  */
 public class CQLParser {
-    private String cql;
-    private StreamTokenizer st;
+    private CQLLexer lexer;
+    static private boolean DEBUG = false;
+    static private boolean LEXDEBUG = false;
 
-    private class CQLParseException extends Exception {
-       CQLParseException(String s) { super(s); }
-    }
-
-    public CQLParser() {
-       // Nothing to do: do we need this constructor, then?
+    private static void debug(String str) {
+       if (DEBUG)
+           System.err.println("PARSEDEBUG: " + str);
     }
 
     public CQLNode parse(String cql)
-       throws FileNotFoundException, IOException {
-       this.cql = cql;
-       st = new StreamTokenizer(new StringReader(cql));
-       // ### these settings are wrong
-       st.wordChars('/', '/');
-       st.wordChars('0', '9'); // ### but 1 is still recognised as TT_NUM
-       st.wordChars('.', '.');
-       st.wordChars('-', '-');
-       st.ordinaryChar('=');
-       st.ordinaryChar(',');
-       st.ordinaryChar('(');
-       st.ordinaryChar(')');
-
-//     int token;
-//     while ((token = st.nextToken()) != st.TT_EOF) {
-//         System.out.println("token=" + token + ", " +
-//                            "nval=" + st.nval + ", " +
-//                            "sval=" + st.sval);
-//     }
-
-       st.nextToken();
-       CQLNode root;
-       try {
-           root = parse_expression();
-       } catch (CQLParseException ex) {
-           System.err.println("### Oops: " + ex);
-           return null;
-       }
+       throws CQLParseException, IOException {
+       lexer = new CQLLexer(cql, LEXDEBUG);
 
-       if (st.ttype != st.TT_EOF) {
-           System.err.println("### Extra bits: " + render(st));
-           return null;
-       }
+       lexer.nextToken();
+       debug("about to parse_query()");
+       CQLNode root = parse_query("srw.serverChoice", new CQLRelation("="));
+       if (lexer.ttype != lexer.TT_EOF)
+           throw new CQLParseException("junk after end: " + lexer.render());
 
        return root;
     }
 
-    private CQLNode parse_expression()
+    private CQLNode parse_query(String qualifier, CQLRelation relation)
        throws CQLParseException, IOException {
-       CQLNode term = parse_term();
-
-       while (st.ttype == st.TT_WORD) {
-           String op = st.sval.toLowerCase();
-           if (st.sval.equals("and")) {
-               match(st.TT_WORD);
-               CQLNode term2 = parse_term();
+       debug("in parse_query()");
+
+       CQLNode term = parse_term(qualifier, relation);
+       while (lexer.ttype != lexer.TT_EOF &&
+              lexer.ttype != ')') {
+           if (lexer.ttype == lexer.TT_AND) {
+               match(lexer.TT_AND);
+               CQLNode term2 = parse_term(qualifier, relation);
                term = new CQLAndNode(term, term2);
-           } else if (st.sval.equals("or")) {
-               match(st.TT_WORD);
-               CQLNode term2 = parse_term();
+           } else if (lexer.ttype == lexer.TT_OR) {
+               match(lexer.TT_OR);
+               CQLNode term2 = parse_term(qualifier, relation);
                term = new CQLOrNode(term, term2);
-           } else if (st.sval.equals("not")) {
-               match(st.TT_WORD);
-               CQLNode term2 = parse_term();
+           } else if (lexer.ttype == lexer.TT_NOT) {
+               match(lexer.TT_NOT);
+               CQLNode term2 = parse_term(qualifier, relation);
                term = new CQLNotNode(term, term2);
+           } else if (lexer.ttype == lexer.TT_PROX) {
+               // ### Handle "prox"
+           } else {
+               throw new CQLParseException("expected boolean, got " +
+                                           lexer.render());
            }
        }
 
+       debug("no more ops");
        return term;
     }
 
-    private CQLNode parse_term()
+    private CQLNode parse_term(String qualifier, CQLRelation relation)
        throws CQLParseException, IOException {
-       if (st.ttype == '(') {
-           match('(');
-           CQLNode expr = parse_expression();
-           match(')');
-           return expr;
-       }
+       debug("in parse_term()");
+
+       String word;
+       while (true) {
+           if (lexer.ttype == '(') {
+               debug("parenthesised term");
+               match('(');
+               CQLNode expr = parse_query(qualifier, relation);
+               match(')');
+               return expr;
+           } else if (lexer.ttype != lexer.TT_WORD && lexer.ttype != '"') {
+               throw new CQLParseException("expected qualifier or term, " +
+                                           "got " + lexer.render());
+           }
 
-       String word = st.sval;
-       return new CQLTermNode("x", "=", word);
-    }
+           debug("non-parenthesised term");
+           word = lexer.sval;
+           match(lexer.ttype);
+           if (!isBaseRelation())
+               break;
+
+           qualifier = word;
+           relation = new CQLRelation(lexer.render(lexer.ttype, false));
+           match(lexer.ttype);
+
+           while (lexer.ttype == '/') {
+               match('/');
+               // ### could insist on known modifiers only
+               if (lexer.ttype != lexer.TT_WORD)
+                   throw new CQLParseException("expected relation modifier, "
+                                               + "got " + lexer.render());
+               relation.addModifier(lexer.sval);
+               match(lexer.TT_WORD);
+           }
 
-    private void match(int token)
-       throws CQLParseException, IOException {
-       if (st.ttype != token)
-           throw new CQLParseException("expected " + render(st, token, null) +
-                                       ", " + "got " + render(st));
-       st.nextToken();
-    }
+           debug("qualifier='" + qualifier + ", " +
+                 "relation='" + relation.toCQL() + "'");
+       }
 
-    // ### This utility should surely be a method of the StreamTokenizer class
-    private static String render(StreamTokenizer st) {
-       return render(st, st.ttype, null);
+       CQLTermNode node = new CQLTermNode(qualifier, relation, word);
+       debug("made term node " + node.toCQL());
+       return node;
     }
 
-    private static String render(StreamTokenizer st, int token, String str) {
-       String ret;
-
-       if (token == st.TT_EOF) {
-           return "EOF";
-       } else if (token == st.TT_EOL) {
-           return "EOL";
-       } else if (token == st.TT_NUMBER) {
-           return "number";
-       } else if (token == st.TT_WORD) {
-           return "word";
-       } else if (token == '"' && token == '\'') {
-           return "string";
-       }
+    boolean isBaseRelation() {
+       debug("isBaseRelation: checking ttype=" + lexer.ttype +
+             " (" + lexer.render() + ")");
+       return (lexer.ttype == '<' ||
+               lexer.ttype == '>' ||
+               lexer.ttype == '=' ||
+               lexer.ttype == lexer.TT_LE ||
+               lexer.ttype == lexer.TT_GE ||
+               lexer.ttype == lexer.TT_NE ||
+               lexer.ttype == lexer.TT_ANY ||
+               lexer.ttype == lexer.TT_ALL ||
+               lexer.ttype == lexer.TT_EXACT);
+    }
 
-        return "'" + String.valueOf((char) token) + "'";
+    private void match(int token)
+       throws CQLParseException, IOException {
+       debug("in match(" + lexer.render(token, true) + ")");
+       if (lexer.ttype != token)
+           throw new CQLParseException("expected " +
+                                       lexer.render(token, true) +
+                                       ", " + "got " + lexer.render());
+       int tmp = lexer.nextToken();
+       debug("match() got token=" + lexer.ttype + ", " +
+             "nval=" + lexer.nval + ", sval='" + lexer.sval + "'" +
+             " (tmp=" + tmp + ")");
     }
 
 
@@ -141,7 +145,27 @@ public class CQLParser {
     // e.g. echo '(au=Kerninghan or au=Ritchie) and ti=Unix' |
     //                         java org.z3950.zing.cql.CQLParser
     // yields:
-    // ###
+    // <triple>
+    //   <boolean>and</boolean>
+    //   <triple>
+    //     <boolean>or</boolean>
+    //     <searchClause>
+    //       <index>au<index>
+    //       <relation>=<relation>
+    //       <term>Kerninghan<term>
+    //     </searchClause>
+    //     <searchClause>
+    //       <index>au<index>
+    //       <relation>=<relation>
+    //       <term>Ritchie<term>
+    //     </searchClause>
+    //   </triple>
+    //   <searchClause>
+    //     <index>ti<index>
+    //     <relation>=<relation>
+    //     <term>Unix<term>
+    //   </searchClause>
+    // </triple>
     //
     public static void main (String[] args) {
        if (args.length != 0) {
@@ -149,16 +173,27 @@ public class CQLParser {
            System.exit(1);
        }
 
-       byte[] bytes = new byte[1000];
+       byte[] bytes = new byte[10000];
        try {
+           // Read in the whole of standard input in one go
            int nbytes = System.in.read(bytes);
        } catch (java.io.IOException ex) {
-           System.err.println("Can't read query: " + ex);
+           System.err.println("Can't read query: " + ex.getMessage());
            System.exit(2);
        }
-       String cql = String(bytes);
+       String cql = new String(bytes);
        CQLParser parser = new CQLParser();
-       CQLNode root = parser.parse(cql);
-       System.out.println(root.toXCQL());
+       CQLNode root;
+       try {
+           root = parser.parse(cql);
+           debug("root='" + root + "'");
+           System.out.println(root.toXCQL(0));
+       } catch (CQLParseException ex) {
+           System.err.println("Syntax error: " + ex.getMessage());
+           System.exit(3);
+       } catch (java.io.IOException ex) {
+           System.err.println("Can't compile query: " + ex.getMessage());
+           System.exit(4);
+       }
     }
 }