- Change the XCQL output to include the nasty and redundant
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
index bc19d48..85ee63b 100644 (file)
-// $Id: CQLParser.java,v 1.3 2002-10-24 16:06:34 mike Exp $
+// $Id: CQLParser.java,v 1.22 2002-11-20 01:15:15 mike Exp $
 
 package org.z3950.zing.cql;
+import java.io.IOException;
+import java.util.Vector;
 import java.util.Properties;
 import java.io.InputStream;
+import java.io.FileInputStream;
 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 CQL strings into parse trees of CQLNode subtypes.
  *
- * @version    $Id: CQLParser.java,v 1.3 2002-10-24 16:06:34 mike Exp $
+ * @version    $Id: CQLParser.java,v 1.22 2002-11-20 01:15:15 mike Exp $
  * @see                <A href="http://zing.z3950.org/cql/index.html"
  *                     >http://zing.z3950.org/cql/index.html</A>
  */
-class CQLCompiler {
-    private String cql;
-    private String qualset;
-    private Properties qualsetProperties;
-    private StreamTokenizer st;
-
-    private class CQLParseException extends Exception {
-       CQLParseException(String s) { super(s); }
-    }
+public class CQLParser {
+    private CQLLexer lexer;
+    static private boolean DEBUG = false;
+    static private boolean LEXDEBUG = false;
 
-    public CQLCompiler(String cql, String qualset) {
-       this.cql = cql;
-       this.qualset = qualset;
+    private static void debug(String str) {
+       if (DEBUG)
+           System.err.println("PARSEDEBUG: " + str);
     }
 
-    public String convertToPQN()
-       throws FileNotFoundException, IOException {
-
-       if (qualsetProperties == null) {
-           //      ### Could think about caching named qualifier sets
-           //          across compilations (i.e. shared, in a static
-           //          Hashtable, between multiple CQLCompiler
-           //          instances.)  Probably not worth it.
-           InputStream is = this.getClass().getResourceAsStream(qualset);
-           if (is == null)
-               throw new FileNotFoundException("getResourceAsStream(" +
-                                               qualset + ")");
-           qualsetProperties = new Properties();
-           qualsetProperties.load(is);
-       }
-
-       st = new StreamTokenizer(new StringReader(cql));
-       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();
-       String ret;
-       try {
-           ret = parse_expression();
-       } catch (CQLParseException ex) {
-           System.err.println("### Oops: " + ex);
-           return null;
-       }
+    /**
+     * Compiles a CQL query.
+     * <P>
+     * The resulting parse tree may be further processed by hand (see
+     * the individual node-types' documentation for details on the
+     * data structure) or, more often, simply rendered out in the
+     * desired form using one of the back-ends.  <TT>toCQL()</TT>
+     * returns a decompiled CQL query equivalent to the one that was
+     * compiled in the first place; <TT>toXCQL()</TT> returns an
+     * XML snippet representing the query; and <TT>toPQF()</TT>
+     * returns the query rendered in Index Data's Prefix Query
+     * Format.
+     *
+     * @param cql      The query
+     * @return         A CQLNode object which is the root of a parse
+     * tree representing the query.  */
+    public CQLNode parse(String cql)
+       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 parseQuery()");
+       CQLNode root = parseQuery("srw.serverChoice", new CQLRelation("scr"));
+       if (lexer.ttype != lexer.TT_EOF)
+           throw new CQLParseException("junk after end: " + lexer.render());
 
-       // Interpret attributes as BIB-1 unless otherwise specified
-       return "@attrset bib-1 " + ret;
+       return root;
     }
 
-    private String parse_expression()
+    private CQLNode parseQuery(String qualifier, CQLRelation relation)
        throws CQLParseException, IOException {
-       String term = parse_term();
+       debug("in parseQuery()");
 
-       while (st.ttype == st.TT_WORD) {
-           String op = st.sval.toLowerCase();
-           if (!st.sval.equals("and") &&
-               !st.sval.equals("or") &&
-               !st.sval.equals("not"))
-               break;
-           match(st.TT_WORD);
-           String term2 = parse_term();
-           term = "@" + op + " " + term + " " + term2;
+       CQLNode term = parseTerm(qualifier, relation);
+       while (lexer.ttype != lexer.TT_EOF &&
+              lexer.ttype != ')') {
+           if (lexer.ttype == lexer.TT_AND) {
+               match(lexer.TT_AND);
+               CQLNode term2 = parseTerm(qualifier, relation);
+               term = new CQLAndNode(term, term2);
+           } else if (lexer.ttype == lexer.TT_OR) {
+               match(lexer.TT_OR);
+               CQLNode term2 = parseTerm(qualifier, relation);
+               term = new CQLOrNode(term, term2);
+           } else if (lexer.ttype == lexer.TT_NOT) {
+               match(lexer.TT_NOT);
+               CQLNode term2 = parseTerm(qualifier, relation);
+               term = new CQLNotNode(term, term2);
+           } else if (lexer.ttype == lexer.TT_PROX) {
+               match(lexer.TT_PROX);
+               CQLProxNode proxnode = new CQLProxNode(term);
+               gatherProxParameters(proxnode);
+               CQLNode term2 = parseTerm(qualifier, relation);
+               proxnode.addSecondSubterm(term2);
+               term = (CQLNode) proxnode;
+           } else {
+               throw new CQLParseException("expected boolean, got " +
+                                           lexer.render());
+           }
        }
 
+       debug("no more ops");
        return term;
     }
 
-    private String parse_term()
+    private CQLNode parseTerm(String qualifier, CQLRelation relation)
        throws CQLParseException, IOException {
-       if (st.ttype == '(') {
-           match('(');
-           String expr = parse_expression();
-           match(')');
-           return expr;
-       }
+       debug("in parseTerm()");
 
-       String word = null;
-       String attrs = "";
+       String word;
+       while (true) {
+           if (lexer.ttype == '(') {
+               debug("parenthesised term");
+               match('(');
+               CQLNode expr = parseQuery(qualifier, relation);
+               match(')');
+               return expr;
+           } else if (lexer.ttype == '>') {
+               match('>');
+               return parsePrefix(qualifier, relation);
+           }
 
-       // ### We treat ',' and '=' equivalently here, which isn't quite right.
-       while (st.ttype == st.TT_WORD) {
-           word = st.sval;
-           match(st.TT_WORD);
-           if (st.ttype != '=' && st.ttype != ',') {
-               // end of qualifer list
+           debug("non-parenthesised term");
+           word = matchSymbol("qualifier or term");
+           if (!isBaseRelation())
                break;
-           }
 
-           String attr = qualsetProperties.getProperty(word);
-           if (attr == null) {
-               throw new CQLParseException("unrecognised qualifier: " + word);
+           qualifier = word;
+           relation = new CQLRelation(lexer.render(lexer.ttype, false));
+           match(lexer.ttype);
+
+           while (lexer.ttype == '/') {
+               match('/');
+               if (lexer.ttype != lexer.TT_RELEVANT &&
+                   lexer.ttype != lexer.TT_FUZZY &&
+                   lexer.ttype != lexer.TT_STEM &&
+                   lexer.ttype != lexer.TT_PHONETIC)
+                   throw new CQLParseException("expected relation modifier, "
+                                               + "got " + lexer.render());
+               relation.addModifier(lexer.sval.toLowerCase());
+               match(lexer.ttype);
            }
-           attrs = attrs + attr + " ";
-           match(st.ttype);
-           word = null;        // mark as not-yet-read
+
+           debug("qualifier='" + qualifier + ", " +
+                 "relation='" + relation.toCQL() + "'");
        }
 
-       if (word == null) {
-           // got to the end of a "foo,bar=" sequence
-           word = st.sval;
-           if (st.ttype != '\'' || st.ttype != '"') {
-               word = "\"" + word + "\"";
-               match(st.ttype);
-           } else {
-               match(st.TT_WORD);
+       CQLTermNode node = new CQLTermNode(qualifier, relation, word);
+       debug("made term node " + node.toCQL());
+       return node;
+    }
+
+    private CQLNode parsePrefix(String qualifier, CQLRelation relation)
+       throws CQLParseException, IOException {
+       debug("prefix mapping");
+
+       String name = null;
+       String identifier = matchSymbol("prefix-name");
+       if (lexer.ttype == '=') {
+           match('=');
+           name = identifier;
+           identifier = matchSymbol("prefix-identifer");
+       }
+       CQLNode term = parseQuery(qualifier, relation);
+       return new CQLPrefixNode(name, identifier, term);
+    }
+
+    private void gatherProxParameters(CQLProxNode node)
+       throws CQLParseException, IOException {
+       for (int i = 0; i < 4; i++) {
+           if (lexer.ttype != '/')
+               return;         // end of proximity parameters
+
+           match('/');
+           if (lexer.ttype != '/') {
+               // not an omitted default
+               switch (i) {
+                   // Order should be: relation/distance/unit/ordering
+                   // For now, use MA's: unit/relation/distance/ordering
+               case 0: gatherProxRelation(node); break;
+               case 1: gatherProxDistance(node); break;
+               case 2: gatherProxUnit(node); break;
+               case 3: gatherProxOrdering(node); break;
+               }
            }
        }
+    }
 
-       return attrs + word;
+    private void gatherProxRelation(CQLProxNode node)
+       throws CQLParseException, IOException {
+       if (!isProxRelation())
+           throw new CQLParseException("expected proximity relation, got " +
+                                       lexer.render());
+       node.addModifier("relation", lexer.render(lexer.ttype, false));
+       match(lexer.ttype);
+       debug("gPR matched " + lexer.render(lexer.ttype, false));
     }
 
-    private void match(int token)
+    private void gatherProxDistance(CQLProxNode node)
        throws CQLParseException, IOException {
-       if (st.ttype != token)
-           throw new CQLParseException("expected " + render(st, token, null) +
-                                       ", " + "got " + render(st));
-       st.nextToken();
+       if (lexer.ttype != lexer.TT_NUMBER)
+           throw new CQLParseException("expected proximity distance, got " +
+                                       lexer.render());
+       node.addModifier("distance", lexer.render(lexer.ttype, false));
+       match(lexer.ttype);
+       debug("gPD matched " + lexer.render(lexer.ttype, false));
     }
 
-    // ### This utility should surely be a method of the StreamTokenizer class
-    private static String render(StreamTokenizer st) {
-       return render(st, st.ttype, null);
+    private void gatherProxUnit(CQLProxNode node)
+       throws CQLParseException, IOException {
+       if (lexer.ttype != lexer.TT_pWORD &&
+           lexer.ttype != lexer.TT_SENTENCE &&
+           lexer.ttype != lexer.TT_PARAGRAPH &&
+           lexer.ttype != lexer.TT_ELEMENT)
+           throw new CQLParseException("expected proximity unit, got " +
+                                       lexer.render());
+       node.addModifier("unit", lexer.render());
+       match(lexer.ttype);
+    }
+
+    private void gatherProxOrdering(CQLProxNode node)
+       throws CQLParseException, IOException {
+       if (lexer.ttype != lexer.TT_ORDERED &&
+           lexer.ttype != lexer.TT_UNORDERED)
+           throw new CQLParseException("expected proximity ordering, got " +
+                                       lexer.render());
+       node.addModifier("ordering", lexer.render());
+       match(lexer.ttype);
     }
 
-    private static String render(StreamTokenizer st, int token, String str) {
-       String ret;
+    private boolean isBaseRelation() {
+       debug("isBaseRelation: checking ttype=" + lexer.ttype +
+             " (" + lexer.render() + ")");
+       return (isProxRelation() ||
+               lexer.ttype == lexer.TT_ANY ||
+               lexer.ttype == lexer.TT_ALL ||
+               lexer.ttype == lexer.TT_EXACT ||
+               lexer.ttype == lexer.TT_SCR);
+    }
+
+    // Checks for a relation that may be used inside a prox operator
+    private boolean isProxRelation() {
+       debug("isProxRelation: 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);
+    }
 
-       switch (token) {
-       case st.TT_EOF: return "EOF";
-       case st.TT_EOL: return "EOL";
-       case st.TT_NUMBER: return "number";
-       case st.TT_WORD: ret = "word"; break;
-       case '"': case '\'': ret = "string"; break;
-       default: 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 + ")");
+    }
+
+    private String matchSymbol(String expected)
+       throws CQLParseException, IOException {
+
+       debug("in matchSymbol()");
+       if (lexer.ttype == lexer.TT_WORD ||
+           lexer.ttype == lexer.TT_NUMBER ||
+           lexer.ttype == '"' ||
+           // The following is a complete list of keywords.  Because
+           // they're listed here, they can be used unquoted as
+           // qualifiers, terms, prefix names and prefix identifiers.
+           // ### Instead, we should ask the lexer whether what we
+           // have is a keyword, and let the knowledge reside there.
+           lexer.ttype == lexer.TT_AND ||
+           lexer.ttype == lexer.TT_OR ||
+           lexer.ttype == lexer.TT_NOT ||
+           lexer.ttype == lexer.TT_PROX ||
+           lexer.ttype == lexer.TT_ANY ||
+           lexer.ttype == lexer.TT_ALL ||
+           lexer.ttype == lexer.TT_EXACT ||
+           lexer.ttype == lexer.TT_pWORD ||
+           lexer.ttype == lexer.TT_SENTENCE ||
+           lexer.ttype == lexer.TT_PARAGRAPH ||
+           lexer.ttype == lexer.TT_ELEMENT ||
+           lexer.ttype == lexer.TT_ORDERED ||
+           lexer.ttype == lexer.TT_UNORDERED ||
+           lexer.ttype == lexer.TT_RELEVANT ||
+           lexer.ttype == lexer.TT_FUZZY ||
+           lexer.ttype == lexer.TT_STEM ||
+           lexer.ttype == lexer.TT_SCR ||
+           lexer.ttype == lexer.TT_PHONETIC) {
+           String symbol = (lexer.ttype == lexer.TT_NUMBER) ?
+               lexer.render() : lexer.sval;
+           match(lexer.ttype);
+           return symbol;
        }
 
-       if (str != null)
-           ret += "(\"" + str + "\")";
-       return ret;
+       throw new CQLParseException("expected " + expected + ", " +
+                                   "got " + lexer.render());
     }
 
-    // ### Not really the right place for this test harness.
-    //
-    // e.g. java uk.org.miketaylor.zoom.CQLCompiler
-    //         '(au=Kerninghan or au=Ritchie) and ti=Unix' qualset.properties
-    // yields:
-    // @and
-    //         @or
-    //                 @attr 1=1 @attr 4=1 Kerninghan
-    //                 @attr 1=1 @attr 4=1 Ritchie
-    //         @attr 1=4 @attr 4=1 Unix
-    //
+
+    /**
+     * Simple test-harness for the CQLParser class.
+     * <P>
+     * Reads a CQL query either from its command-line argument, if
+     * there is one, or standard input otherwise.  So these two
+     * invocations are equivalent:
+     * <PRE>
+     *  CQLParser 'au=(Kerninghan or Ritchie) and ti=Unix'
+     *  echo au=(Kerninghan or Ritchie) and ti=Unix | CQLParser
+     * </PRE>
+     * The test-harness parses the supplied query and renders is as
+     * XCQL, so that both of the invocations above produce the
+     * following output:
+     * <PRE>
+     * &lt;triple&gt;
+     *   &lt;boolean&gt;
+     *     &lt;value&gt;and&lt;/value&gt;
+     *   &lt;/boolean&gt;
+     *   &lt;triple&gt;
+     *     &lt;boolean&gt;
+     *       &lt;value&gt;or&lt;/value&gt;
+     *     &lt;/boolean&gt;
+     *     &lt;searchClause&gt;
+     *       &lt;index&gt;au&lt;/index&gt;
+     *       &lt;relation&gt;
+     *         &lt;value&gt;=&lt;/value&gt;
+     *       &lt;/relation&gt;
+     *       &lt;term&gt;Kerninghan&lt;/term&gt;
+     *     &lt;/searchClause&gt;
+     *     &lt;searchClause&gt;
+     *       &lt;index&gt;au&lt;/index&gt;
+     *       &lt;relation&gt;
+     *         &lt;value&gt;=&lt;/value&gt;
+     *       &lt;/relation&gt;
+     *       &lt;term&gt;Ritchie&lt;/term&gt;
+     *     &lt;/searchClause&gt;
+     *   &lt;/triple&gt;
+     *   &lt;searchClause&gt;
+     *     &lt;index&gt;ti&lt;/index&gt;
+     *     &lt;relation&gt;
+     *       &lt;value&gt;=&lt;/value&gt;
+     *     &lt;/relation&gt;
+     *     &lt;term&gt;Unix&lt;/term&gt;
+     *   &lt;/searchClause&gt;
+     * &lt;/triple&gt;
+     * </PRE>
+     * <P>
+     * @param -c
+     * Causes the output to be written in CQL rather than XCQL - that
+     * is, a query equivalent to that which was input, is output.  In
+     * effect, the test harness acts as a query canonicaliser.
+     * @return
+     * The input query, either as XCQL [default] or CQL [if the
+     * <TT>-c</TT> option is supplied].
+     */
     public static void main (String[] args) {
-       if (args.length != 2) {
-           System.err.println("Usage: CQLQuery <cql> <qualset>");
+       char mode = 'x';        // x=XCQL, c=CQL, p=PQF
+       String pfile = null;
+
+       Vector argv = new Vector();
+       for (int i = 0; i < args.length; i++) {
+           argv.add(args[i]);
+       }
+
+       if (argv.size() > 0 && argv.get(0).equals("-c")) {
+           mode = 'c';
+           argv.remove(0);
+       } else if (argv.size() > 1 && argv.get(0).equals("-p")) {
+           mode = 'p';
+           argv.remove(0);
+           pfile = (String) argv.get(0);
+           argv.remove(0);
+       }
+
+       if (argv.size() > 1) {
+           System.err.println(
+               "Usage: CQLParser [-c] [-p <pqf-properties> [<CQL-query>]");
+           System.err.println("If unspecified, query is read from stdin");
            System.exit(1);
        }
 
-       CQLCompiler cc = new CQLCompiler(args[0], args[1]);
+       String cql;
+       if (argv.size() == 1) {
+           cql = (String) argv.get(0);
+       } else {
+           byte[] bytes = new byte[10000];
+           try {
+               // Read in the whole of standard input in one go
+               int nbytes = System.in.read(bytes);
+           } catch (IOException ex) {
+               System.err.println("Can't read query: " + ex.getMessage());
+               System.exit(2);
+           }
+           cql = new String(bytes);
+       }
+
+       CQLParser parser = new CQLParser();
+       CQLNode root = null;
        try {
-           String pqn = cc.convertToPQN();
-           System.out.println(pqn);
-       } catch (FileNotFoundException ex) {
-           System.err.println("Can't find qualifier set: " + ex);
-           System.exit(2);
+           root = parser.parse(cql);
+       } catch (CQLParseException ex) {
+           System.err.println("Syntax error: " + ex.getMessage());
+           System.exit(3);
+       } catch (IOException ex) {
+           System.err.println("Can't compile query: " + ex.getMessage());
+           System.exit(4);
+       }
+
+       try {
+           if (mode == 'c') {
+               System.out.println(root.toCQL());
+           } else if (mode == 'p') {
+               InputStream f = new FileInputStream(pfile);
+               if (f == null)
+                   throw new FileNotFoundException(pfile);
+
+               Properties config = new Properties();
+               config.load(f);
+               f.close();
+               System.out.println(root.toPQF(config));
+           } else {
+               System.out.print(root.toXCQL(0));
+           }
        } catch (IOException ex) {
-           System.err.println("Can't read qualifier set: " + ex);
-           System.exit(2);
+           System.err.println("Can't render query: " + ex.getMessage());
+           System.exit(5);
+       } catch (UnknownQualifierException ex) {
+           System.err.println("Unknown qualifier: " + ex.getMessage());
+           System.exit(6);
+       } catch (UnknownRelationException ex) {
+           System.err.println("Unknown relation: " + ex.getMessage());
+           System.exit(7);
+       } catch (UnknownRelationModifierException ex) {
+           System.err.println("Unknown relation modifier: " +
+                              ex.getMessage());
+           System.exit(8);
+       } catch (UnknownPositionException ex) {
+           System.err.println("Unknown position: " + ex.getMessage());
+           System.exit(9);
+       } catch (PQFTranslationException ex) {
+           // We catch all of this class's subclasses, so --
+           throw new Error("can't get a PQFTranslationException");
        }
     }
 }