New method parseTopLevelPrefixes() handles both initial prefixes and a
[cql-java-moved-to-github.git] / src / org / z3950 / zing / cql / CQLParser.java
index 892c8da..f622bbb 100644 (file)
@@ -1,4 +1,4 @@
-// $Id: CQLParser.java,v 1.25 2007-06-26 16:43:41 mike Exp $
+// $Id: CQLParser.java,v 1.37 2007-07-03 13:34:30 mike Exp $
 
 package org.z3950.zing.cql;
 import java.io.IOException;
@@ -12,15 +12,44 @@ import java.io.FileNotFoundException;
 /**
  * Compiles CQL strings into parse trees of CQLNode subtypes.
  *
- * @version    $Id: CQLParser.java,v 1.25 2007-06-26 16:43:41 mike Exp $
+ * @version    $Id: CQLParser.java,v 1.37 2007-07-03 13:34:30 mike Exp $
  * @see                <A href="http://zing.z3950.org/cql/index.html"
  *                     >http://zing.z3950.org/cql/index.html</A>
  */
 public class CQLParser {
     private CQLLexer lexer;
+    private int compat;        // When false, implement CQL 1.2
+    public static int V1POINT1 = 12368;
+    public static int V1POINT2 = 12369;
+    public static int V1POINT1SORT = 12370;
+
     static private boolean DEBUG = false;
     static private boolean LEXDEBUG = false;
 
+    /**
+     * The new parser implements a dialect of CQL specified by the
+     * <tt>compat</tt> argument:
+     * <ul>
+     *  <li>V1POINT1 - CQL version 1.1
+     *  </li>
+     *  <li>V1POINT2 - CQL version 1.2
+     *  </li>
+     *  <li>V1POINT1SORT - CQL version 1.1 but including
+     *         <tt>sortby</tt> as specified for CQL 1.2.
+     *  </li>
+     * </ul>
+     */
+    CQLParser(int compat) {
+       this.compat = compat;
+    }
+
+    /**
+     * The new parser implements CQL 1.2
+     */
+    CQLParser() {
+       this.compat = V1POINT2;
+    }
+
     private static void debug(String str) {
        if (DEBUG)
            System.err.println("PARSEDEBUG: " + str);
@@ -48,39 +77,61 @@ public class CQLParser {
 
        lexer.nextToken();
        debug("about to parseQuery()");
-       CQLNode root = parseQuery("srw.serverChoice", new CQLRelation("scr"));
+       CQLNode root = parseTopLevelPrefixes("cql.serverChoice",
+               new CQLRelation(compat == V1POINT2 ? "=" : "scr"));
        if (lexer.ttype != lexer.TT_EOF)
            throw new CQLParseException("junk after end: " + lexer.render());
 
        return root;
     }
 
-    private CQLNode parseQuery(String qualifier, CQLRelation relation)
+    private CQLNode parseTopLevelPrefixes(String index, CQLRelation relation)
+       throws CQLParseException, IOException {
+       debug("top-level prefix mapping");
+
+       if (lexer.ttype == '>') {
+           return parsePrefix(index, relation, true);
+       }
+
+       CQLNode node = parseQuery(index, relation);
+       if ((compat == V1POINT2 || compat == V1POINT1SORT) &&
+           lexer.ttype == lexer.TT_SORTBY) {
+           match(lexer.ttype);
+           debug("sortspec");
+
+           CQLSortNode sortnode = new CQLSortNode(node);
+           while (lexer.ttype != lexer.TT_EOF) {
+               String sortindex = matchSymbol("sort index");
+               ModifierSet ms = gatherModifiers(sortindex);
+               sortnode.addSortIndex(ms);
+           }
+           node = sortnode;
+       }
+
+       return node;
+    }
+
+    private CQLNode parseQuery(String index, CQLRelation relation)
        throws CQLParseException, IOException {
        debug("in parseQuery()");
 
-       CQLNode term = parseTerm(qualifier, relation);
+       CQLNode term = parseTerm(index, 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;
+              lexer.ttype != ')' &&
+              lexer.ttype != lexer.TT_SORTBY) {
+           if (lexer.ttype == lexer.TT_AND ||
+               lexer.ttype == lexer.TT_OR ||
+               lexer.ttype == lexer.TT_NOT ||
+               lexer.ttype == lexer.TT_PROX) {
+               int type = lexer.ttype;
+               String val = lexer.sval;
+               match(type);
+               ModifierSet ms = gatherModifiers(val);
+               CQLNode term2 = parseTerm(index, relation);
+               term = ((type == lexer.TT_AND) ? new CQLAndNode(term, term2, ms) :
+                       (type == lexer.TT_OR)  ? new CQLOrNode (term, term2, ms) :
+                       (type == lexer.TT_NOT) ? new CQLNotNode(term, term2, ms) :
+                                                new CQLProxNode(term, term2, ms));
            } else {
                throw new CQLParseException("expected boolean, got " +
                                            lexer.render());
@@ -91,7 +142,34 @@ public class CQLParser {
        return term;
     }
 
-    private CQLNode parseTerm(String qualifier, CQLRelation relation)
+    private ModifierSet gatherModifiers(String base)
+       throws CQLParseException, IOException {
+       debug("in gatherModifiers()");
+
+       ModifierSet ms = new ModifierSet(base);
+       while (lexer.ttype == '/') {
+           match('/');
+           if (lexer.ttype != lexer.TT_WORD)
+               throw new CQLParseException("expected modifier, "
+                                           + "got " + lexer.render());
+           String type = lexer.sval.toLowerCase();
+           match(lexer.ttype);
+           if (!isRelation()) {
+               // It's a simple modifier consisting of type only
+               ms.addModifier(type);
+           } else {
+               // It's a complex modifier of the form type=value
+               String comparision = lexer.render(lexer.ttype, false);
+               match(lexer.ttype);
+               String value = matchSymbol("modifier value");
+               ms.addModifier(type, comparision, value);
+           }
+       }
+
+       return ms;
+    }
+
+    private CQLNode parseTerm(String index, CQLRelation relation)
        throws CQLParseException, IOException {
        debug("in parseTerm()");
 
@@ -100,57 +178,40 @@ public class CQLParser {
            if (lexer.ttype == '(') {
                debug("parenthesised term");
                match('(');
-               CQLNode expr = parseQuery(qualifier, relation);
+               CQLNode expr = parseQuery(index, relation);
                match(')');
                return expr;
            } else if (lexer.ttype == '>') {
-               match('>');
-               return parsePrefix(qualifier, relation);
+               return parsePrefix(index, relation, false);
            }
 
            debug("non-parenthesised term");
-           word = matchSymbol("qualifier or term");
-           if (!isBaseRelation())
+           word = matchSymbol("index or term");
+           if (!isRelation() && lexer.ttype != lexer.TT_WORD)
                break;
 
-           qualifier = word;
-           relation = new CQLRelation(lexer.ttype == lexer.TT_WORD ?
-                                      lexer.sval :
-                                      lexer.render(lexer.ttype, false));
+           index = word;
+           String relstr = (lexer.ttype == lexer.TT_WORD ?
+                            lexer.sval : lexer.render(lexer.ttype, false));
+           relation = new CQLRelation(relstr);
            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 &&
-                   lexer.ttype != lexer.TT_WORD)
-                   throw new CQLParseException("expected relation modifier, "
-                                               + "got " + lexer.render());
-               if (lexer.ttype == lexer.TT_WORD &&
-                   lexer.sval.indexOf('.') == -1)
-                   throw new CQLParseException("unknown first-class " +
-                                               "relation modifier: " +
-                                               lexer.sval);
-
-               relation.addModifier(lexer.sval.toLowerCase());
-               match(lexer.ttype);
-           }
-
-           debug("qualifier='" + qualifier + ", " +
+           ModifierSet ms = gatherModifiers(relstr);
+           relation.setModifiers(ms);
+           debug("index='" + index + ", " +
                  "relation='" + relation.toCQL() + "'");
        }
 
-       CQLTermNode node = new CQLTermNode(qualifier, relation, word);
+       CQLTermNode node = new CQLTermNode(index, relation, word);
        debug("made term node " + node.toCQL());
        return node;
     }
 
-    private CQLNode parsePrefix(String qualifier, CQLRelation relation)
+    private CQLNode parsePrefix(String index, CQLRelation relation,
+                               boolean topLevel)
        throws CQLParseException, IOException {
        debug("prefix mapping");
 
+       match('>');
        String name = null;
        String identifier = matchSymbol("prefix-name");
        if (lexer.ttype == '=') {
@@ -158,101 +219,24 @@ public class CQLParser {
            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;
-               }
-           }
-       }
-    }
-
-    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 gatherProxDistance(CQLProxNode node)
-       throws CQLParseException, IOException {
-       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));
-    }
+       CQLNode node = topLevel ?
+           parseTopLevelPrefixes(index, relation) :
+           parseQuery(index, relation);
 
-    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);
+       return new CQLPrefixNode(name, identifier, node);
     }
 
-    private boolean isBaseRelation()
-       throws CQLParseException {
-       debug("isBaseRelation: checking ttype=" + lexer.ttype +
-             " (" + lexer.render() + ")");
-
-       if (lexer.ttype == lexer.TT_WORD &&
-           lexer.sval.indexOf('.') == -1)
-           throw new CQLParseException("unknown first-class relation: " +
-                                       lexer.sval);
-
-       return (isProxRelation() ||
-               lexer.ttype == lexer.TT_ANY ||
-               lexer.ttype == lexer.TT_ALL ||
-               lexer.ttype == lexer.TT_EXACT ||
-               lexer.ttype == lexer.TT_SCR ||
-               lexer.ttype == lexer.TT_WORD);
-    }
-
-    // Checks for a relation that may be used inside a prox operator
-    private boolean isProxRelation() {
-       debug("isProxRelation: checking ttype=" + lexer.ttype +
+    // Checks for a relation
+    private boolean isRelation() {
+       debug("isRelation: 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_NE ||
+               lexer.ttype == lexer.TT_EQEQ);
     }
 
     private void match(int token)
@@ -277,27 +261,14 @@ public class CQLParser {
            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.
+           // indexes, 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) {
+           lexer.ttype == lexer.TT_SORTBY) {
            String symbol = (lexer.ttype == lexer.TT_NUMBER) ?
                lexer.render() : lexer.sval;
            match(lexer.ttype);
@@ -356,6 +327,10 @@ public class CQLParser {
      * &lt;/triple&gt;
      * </PRE>
      * <P>
+     * @param -1
+     * CQL version 1.1 (default version 1.2)
+     * @param -d
+     * Debug mode: extra output written to stderr.
      * @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
@@ -373,6 +348,12 @@ public class CQLParser {
            argv.add(args[i]);
        }
 
+       int compat = V1POINT2;
+       if (argv.size() > 0 && argv.get(0).equals("-1")) {
+           compat = V1POINT1;
+           argv.remove(0);
+       }
+
        if (argv.size() > 0 && argv.get(0).equals("-d")) {
            DEBUG = true;
            argv.remove(0);
@@ -389,7 +370,8 @@ public class CQLParser {
        }
 
        if (argv.size() > 1) {
-           System.err.println("Usage: CQLParser [-d] [-c] [-p <pqf-properties> [<CQL-query>]");
+           System.err.println("Usage: CQLParser [-1] [-d] [-c] " +
+                              "[-p <pqf-properties> [<CQL-query>]");
            System.err.println("If unspecified, query is read from stdin");
            System.exit(1);
        }
@@ -409,7 +391,7 @@ public class CQLParser {
            cql = new String(bytes);
        }
 
-       CQLParser parser = new CQLParser();
+       CQLParser parser = new CQLParser(compat);
        CQLNode root = null;
        try {
            root = parser.parse(cql);
@@ -439,8 +421,8 @@ public class CQLParser {
        } catch (IOException ex) {
            System.err.println("Can't render query: " + ex.getMessage());
            System.exit(5);
-       } catch (UnknownQualifierException ex) {
-           System.err.println("Unknown qualifier: " + ex.getMessage());
+       } catch (UnknownIndexException ex) {
+           System.err.println("Unknown index: " + ex.getMessage());
            System.exit(6);
        } catch (UnknownRelationException ex) {
            System.err.println("Unknown relation: " + ex.getMessage());