Port regression/randomized testing to JUnit
[cql-java-moved-to-github.git] / src / main / java / org / z3950 / zing / cql / CQLParser.java
index cf1272c..72a39a3 100644 (file)
@@ -1,27 +1,34 @@
-// $Id: CQLParser.java,v 1.39 2007-08-06 15:54:48 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.Reader;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 
 /**
  * Compiles CQL strings into parse trees of CQLNode subtypes.
  *
- * @version    $Id: CQLParser.java,v 1.39 2007-08-06 15:54:48 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;
+    private PositionAwareReader par; //active reader with position
+    private final int compat;  // When false, implement CQL 1.2
+    private final Set<String> customRelations = new HashSet<String>();
+    
+    public static final int V1POINT1 = 12368;
+    public static final int V1POINT2 = 12369;
+    public static final int V1POINT1SORT = 12370;
+    public final boolean allowKeywordTerms;
 
     static private boolean DEBUG = false;
     static private boolean LEXDEBUG = false;
@@ -41,19 +48,72 @@ public class CQLParser {
      */
     public CQLParser(int compat) {
        this.compat = compat;
+        this.allowKeywordTerms = true;
     }
-
+    
+    /**
+     * Official CQL grammar allows registered keywords like 'and/or/not/sortby/prox' 
+     * to be used unquoted in terms. This constructor allows to create an instance 
+     * of a parser that prohibits this behavior while sacrificing compatibility.
+     * @param compat CQL version compatibility
+     * @param allowKeywordTerms when false registered keywords are disallowed in unquoted terms
+     */
+    public CQLParser(int compat, boolean allowKeywordTerms) {
+       this.compat = compat;
+        this.allowKeywordTerms = allowKeywordTerms;
+    }
+    
     /**
      * The new parser implements CQL 1.2
      */
     public CQLParser() {
        this.compat = V1POINT2;
+        this.allowKeywordTerms = true;
     }
 
     private static void debug(String str) {
        if (DEBUG)
            System.err.println("PARSEDEBUG: " + str);
     }
+    
+    /**
+     * Registers custom relation in this parser. Note that when a custom relation
+     * is registered the parser is no longer strictly compliant with the chosen spec.
+     * @param relation
+     * @return true if custom relation has not been registered already
+     */
+    public boolean registerCustomRelation(String relation) {
+      return customRelations.add(relation);
+    }
+    
+    /**
+     * Unregisters previously registered custom relation in this instance of the parser.
+     * @param relation
+     * @return true is relation has been previously registered
+     */
+    public boolean unregisterCustomRelation(String relation) {
+      return customRelations.remove(relation);
+    }
+    
+    /**
+     * 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 {
+        return parse(new StringReader(cql));
+    }
 
     /**
      * Compiles a CQL query.
@@ -71,16 +131,18 @@ public class CQLParser {
      * @param cql      The query
      * @return         A CQLNode object which is the root of a parse
      * tree representing the query.  */
-    public CQLNode parse(String cql)
+    public CQLNode parse(Reader cql)
        throws CQLParseException, IOException {
-       lexer = new CQLLexer(cql, LEXDEBUG);
+        par = new PositionAwareReader(cql);
+       lexer = new CQLLexer(par, LEXDEBUG);
 
        lexer.nextToken();
        debug("about to parseQuery()");
        CQLNode root = parseTopLevelPrefixes("cql.serverChoice",
                new CQLRelation(compat == V1POINT2 ? "=" : "scr"));
-       if (lexer.ttype != lexer.TT_EOF)
-           throw new CQLParseException("junk after end: " + lexer.render());
+       if (lexer.ttype != CQLLexer.TT_EOF)
+           throw new CQLParseException("junk after end: " + lexer.render(), 
+              par.getPosition());
 
        return root;
     }
@@ -95,19 +157,19 @@ public class CQLParser {
 
        CQLNode node = parseQuery(index, relation);
        if ((compat == V1POINT2 || compat == V1POINT1SORT) &&
-           lexer.ttype == lexer.TT_SORTBY) {
+           lexer.ttype == CQLLexer.TT_SORTBY) {
            match(lexer.ttype);
            debug("sortspec");
 
            CQLSortNode sortnode = new CQLSortNode(node);
-           while (lexer.ttype != lexer.TT_EOF) {
+           while (lexer.ttype != CQLLexer.TT_EOF) {
                String sortindex = matchSymbol("sort index");
                ModifierSet ms = gatherModifiers(sortindex);
                sortnode.addSortIndex(ms);
            }
 
            if (sortnode.keys.size() == 0) {
-               throw new CQLParseException("no sort keys");
+               throw new CQLParseException("no sort keys", par.getPosition());
            }
 
            node = sortnode;
@@ -121,25 +183,25 @@ public class CQLParser {
        debug("in parseQuery()");
 
        CQLNode term = parseTerm(index, relation);
-       while (lexer.ttype != lexer.TT_EOF &&
+       while (lexer.ttype != CQLLexer.TT_EOF &&
               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) {
+              lexer.ttype != CQLLexer.TT_SORTBY) {
+           if (lexer.ttype == CQLLexer.TT_AND ||
+               lexer.ttype == CQLLexer.TT_OR ||
+               lexer.ttype == CQLLexer.TT_NOT ||
+               lexer.ttype == CQLLexer.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) :
+               term = ((type == CQLLexer.TT_AND) ? new CQLAndNode(term, term2, ms) :
+                       (type == CQLLexer.TT_OR)  ? new CQLOrNode (term, term2, ms) :
+                       (type == CQLLexer.TT_NOT) ? new CQLNotNode(term, term2, ms) :
                                                 new CQLProxNode(term, term2, ms));
            } else {
                throw new CQLParseException("expected boolean, got " +
-                                           lexer.render());
+                                           lexer.render(), par.getPosition());
            }
        }
 
@@ -154,9 +216,10 @@ public class CQLParser {
        ModifierSet ms = new ModifierSet(base);
        while (lexer.ttype == '/') {
            match('/');
-           if (lexer.ttype != lexer.TT_WORD)
+           if (lexer.ttype != CQLLexer.TT_WORD)
                throw new CQLParseException("expected modifier, "
-                                           + "got " + lexer.render());
+                                           + "got " + lexer.render(), 
+                  par.getPosition());
            String type = lexer.sval.toLowerCase();
            match(lexer.ttype);
            if (!isSymbolicRelation()) {
@@ -192,21 +255,21 @@ public class CQLParser {
 
            debug("non-parenthesised term");
            word = matchSymbol("index or term");
-            while (lexer.ttype == lexer.TT_WORD && !isRelation()) {
+            while (lexer.ttype == CQLLexer.TT_WORD && !isRelation()) {
               word = word + " " + lexer.sval;
-              match(lexer.TT_WORD);
+              match(CQLLexer.TT_WORD);
             }
 
            if (!isRelation())
                break;
 
            index = word;
-           String relstr = (lexer.ttype == lexer.TT_WORD ?
+           String relstr = (lexer.ttype == CQLLexer.TT_WORD ?
                             lexer.sval : lexer.render(lexer.ttype, false));
            relation = new CQLRelation(relstr);
            match(lexer.ttype);
            ModifierSet ms = gatherModifiers(relstr);
-           relation.setModifiers(ms);
+           relation.ms = ms;
            debug("index='" + index + ", " +
                  "relation='" + relation.toCQL() + "'");
        }
@@ -239,15 +302,16 @@ public class CQLParser {
     private boolean isRelation() {
        debug("isRelation: checking ttype=" + lexer.ttype +
              " (" + lexer.render() + ")");
-        if (lexer.ttype == lexer.TT_WORD &&
+        if (lexer.ttype == CQLLexer.TT_WORD &&
             (lexer.sval.indexOf('.') >= 0 ||
              lexer.sval.equals("any") ||
              lexer.sval.equals("all") ||
              lexer.sval.equals("within") ||
              lexer.sval.equals("encloses") ||
-             lexer.sval.equals("exact") ||
+             (lexer.sval.equals("exact") && compat != V1POINT2) ||
              (lexer.sval.equals("scr") && compat != V1POINT2) ||
-             (lexer.sval.equals("adj") && compat == V1POINT2)))
+             (lexer.sval.equals("adj") && compat == V1POINT2) ||
+             customRelations.contains(lexer.sval)))
           return true;
 
         return isSymbolicRelation();
@@ -259,10 +323,10 @@ public class CQLParser {
        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_EQEQ);
+               lexer.ttype == CQLLexer.TT_LE ||
+               lexer.ttype == CQLLexer.TT_GE ||
+               lexer.ttype == CQLLexer.TT_NE ||
+               lexer.ttype == CQLLexer.TT_EQEQ);
     }
 
     private void match(int token)
@@ -271,7 +335,8 @@ public class CQLParser {
        if (lexer.ttype != token)
            throw new CQLParseException("expected " +
                                        lexer.render(token, true) +
-                                       ", " + "got " + lexer.render());
+                                       ", " + "got " + lexer.render(), 
+              par.getPosition());
        int tmp = lexer.nextToken();
        debug("match() got token=" + lexer.ttype + ", " +
              "nval=" + lexer.nval + ", sval='" + lexer.sval + "'" +
@@ -282,27 +347,28 @@ public class CQLParser {
        throws CQLParseException, IOException {
 
        debug("in matchSymbol()");
-       if (lexer.ttype == lexer.TT_WORD ||
-           lexer.ttype == lexer.TT_NUMBER ||
+       if (lexer.ttype == CQLLexer.TT_WORD ||
+           lexer.ttype == CQLLexer.TT_NUMBER ||
            lexer.ttype == '"' ||
            // The following is a complete list of keywords.  Because
            // they're listed here, they can be used unquoted as
            // 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_SORTBY) {
-           String symbol = (lexer.ttype == lexer.TT_NUMBER) ?
+            (allowKeywordTerms &&
+           lexer.ttype == CQLLexer.TT_AND ||
+           lexer.ttype == CQLLexer.TT_OR ||
+           lexer.ttype == CQLLexer.TT_NOT ||
+           lexer.ttype == CQLLexer.TT_PROX ||
+           lexer.ttype == CQLLexer.TT_SORTBY)) {
+           String symbol = (lexer.ttype == CQLLexer.TT_NUMBER) ?
                lexer.render() : lexer.sval;
            match(lexer.ttype);
            return symbol;
        }
 
        throw new CQLParseException("expected " + expected + ", " +
-                                   "got " + lexer.render());
+                                   "got " + lexer.render(), par.getPosition());
     }
 
 
@@ -369,7 +435,7 @@ public class CQLParser {
        char mode = 'x';        // x=XCQL, c=CQL, p=PQF
        String pfile = null;
 
-       Vector<String> argv = new Vector<String>();
+       List<String> argv = new ArrayList<String>();
        for (int i = 0; i < args.length; i++) {
            argv.add(args[i]);
        }
@@ -442,7 +508,7 @@ public class CQLParser {
                f.close();
                System.out.println(root.toPQF(config));
            } else {
-               System.out.print(root.toXCQL(0));
+               System.out.print(root.toXCQL());
            }
        } catch (IOException ex) {
            System.err.println("Can't render query: " + ex.getMessage());