Flag to control if keyword terms are allowed
[cql-java-moved-to-github.git] / src / main / java / org / z3950 / zing / cql / CQLParser.java
index 795b2f1..d577cd9 100644 (file)
@@ -1,4 +1,3 @@
-// $Id: CQLParser.java,v 1.39 2007-08-06 15:54:48 mike Exp $
 
 package org.z3950.zing.cql;
 import java.io.IOException;
@@ -9,22 +8,27 @@ 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
+    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;
@@ -44,13 +48,27 @@ 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 = V1POINT2;
+        this.allowKeywordTerms = allowKeywordTerms;
+    }
+    
     /**
      * The new parser implements CQL 1.2
      */
     public CQLParser() {
        this.compat = V1POINT2;
+        this.allowKeywordTerms = true;
     }
 
     private static void debug(String str) {
@@ -59,6 +77,25 @@ public class CQLParser {
     }
     
     /**
+     * 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
@@ -96,14 +133,16 @@ public class CQLParser {
      * tree representing the query.  */
     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 != CQLLexer.TT_EOF)
-           throw new CQLParseException("junk after end: " + lexer.render());
+           throw new CQLParseException("junk after end: " + lexer.render(), 
+              par.getPosition());
 
        return root;
     }
@@ -130,7 +169,7 @@ public class CQLParser {
            }
 
            if (sortnode.keys.size() == 0) {
-               throw new CQLParseException("no sort keys");
+               throw new CQLParseException("no sort keys", par.getPosition());
            }
 
            node = sortnode;
@@ -162,7 +201,7 @@ public class CQLParser {
                                                 new CQLProxNode(term, term2, ms));
            } else {
                throw new CQLParseException("expected boolean, got " +
-                                           lexer.render());
+                                           lexer.render(), par.getPosition());
            }
        }
 
@@ -179,7 +218,8 @@ public class CQLParser {
            match('/');
            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()) {
@@ -270,7 +310,8 @@ public class CQLParser {
              lexer.sval.equals("encloses") ||
              (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();
@@ -294,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 + "'" +
@@ -313,11 +355,12 @@ public class CQLParser {
            // 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.
+            (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) {
+           lexer.ttype == CQLLexer.TT_SORTBY)) {
            String symbol = (lexer.ttype == CQLLexer.TT_NUMBER) ?
                lexer.render() : lexer.sval;
            match(lexer.ttype);
@@ -325,7 +368,7 @@ public class CQLParser {
        }
 
        throw new CQLParseException("expected " + expected + ", " +
-                                   "got " + lexer.render());
+                                   "got " + lexer.render(), par.getPosition());
     }