/* GanyQueryTransmuter.java This is the textual query processor for the Ganymede server. It takes a string query, applies an ANTLR grammar to it, and generates a traditional Ganymede-style arlut.csd.ganymede.common.Query out of it. Created: 31 August 2004 Module By: Deepak Giridharagopal, Jonathan Abbey ----------------------------------------------------------------------- Ganymede Directory Management System Copyright (C) 1996-2014 The University of Texas at Austin Ganymede is a registered trademark of The University of Texas at Austin Contact information Web site: http://www.arlut.utexas.edu/gash2 Author Email: ganymede_author@arlut.utexas.edu Email mailing list: ganymede@arlut.utexas.edu US Mail: Computer Science Division Applied Research Laboratories The University of Texas at Austin PO Box 8029, Austin TX 78713-8029 Telephone: (512) 835-3200 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package arlut.csd.ganymede.server; import arlut.csd.ganymede.common.GanyParseException; import arlut.csd.ganymede.common.DumpResult; import arlut.csd.ganymede.common.FieldType; import arlut.csd.ganymede.common.Invid; import arlut.csd.ganymede.common.NotLoggedInException; import arlut.csd.ganymede.common.Query; import arlut.csd.ganymede.common.QueryAndNode; import arlut.csd.ganymede.common.QueryDeRefNode; import arlut.csd.ganymede.common.QueryDataNode; import arlut.csd.ganymede.common.QueryNode; import arlut.csd.ganymede.common.QueryNotNode; import arlut.csd.ganymede.common.QueryOrNode; import arlut.csd.Util.StringUtils; import arlut.csd.Util.TranslationService; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.antlr.runtime.tree.CommonTree; import org.antlr.runtime.tree.Tree; import java.io.StringReader; import java.text.DateFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /*------------------------------------------------------------------------------ class GanyQueryTransmuter ------------------------------------------------------------------------------*/ /** * <p>This class processes textual queries using an ANTLR-generated * parser, and generates an old, Ganymede-style * arlut.csd.ganymede.common.Query, with attendant * arlut.csd.ganymede.common.QueryNode tree.</p> * * @see arlut.csd.ganymede.common.QueryNode * @see arlut.csd.ganymede.common.Query * * @author Deepak Giridharagopal, deepak@arlut.utexas.edu * @author Jonathan Abbey, jonabbey@arlut.utexas.edu */ public final class GanyQueryTransmuter { private static Map<String, Byte> op_scalar_mapping; private static Map<String, Byte> op_vector_mapping; private static Map<Integer, String[]> validity_mapping; /** * TranslationService object for handling string localization in the Ganymede * server. */ static TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.GanyQueryTransmuter"); static { // initialize op_scalar_mapping op_scalar_mapping = new HashMap(); op_scalar_mapping.put("=~", Byte.valueOf(QueryDataNode.MATCHES)); op_scalar_mapping.put("=~_ci", Byte.valueOf(QueryDataNode.NOCASEMATCHES)); op_scalar_mapping.put("==", Byte.valueOf(QueryDataNode.EQUALS)); op_scalar_mapping.put("==_ci", Byte.valueOf(QueryDataNode.NOCASEEQ)); op_scalar_mapping.put("<", Byte.valueOf(QueryDataNode.LESS)); op_scalar_mapping.put("<=", Byte.valueOf(QueryDataNode.LESSEQ)); op_scalar_mapping.put(">", Byte.valueOf(QueryDataNode.GREAT)); op_scalar_mapping.put(">=", Byte.valueOf(QueryDataNode.GREATEQ)); op_scalar_mapping.put("starts", Byte.valueOf(QueryDataNode.STARTSWITH)); op_scalar_mapping.put("ends", Byte.valueOf(QueryDataNode.ENDSWITH)); op_scalar_mapping.put("defined", Byte.valueOf(QueryDataNode.DEFINED)); op_scalar_mapping = Collections.unmodifiableMap(op_scalar_mapping); // initialize op_vector_mapping op_vector_mapping = new HashMap(); op_vector_mapping.put("len<", Byte.valueOf(QueryDataNode.LENGTHLE)); op_vector_mapping.put("len<=", Byte.valueOf(QueryDataNode.LENGTHLEEQ)); op_vector_mapping.put("len>", Byte.valueOf(QueryDataNode.LENGTHGR)); op_vector_mapping.put("len>=", Byte.valueOf(QueryDataNode.LENGTHGREQ)); op_vector_mapping.put("len==", Byte.valueOf(QueryDataNode.LENGTHEQ)); op_vector_mapping = Collections.unmodifiableMap(op_vector_mapping); validity_mapping = new HashMap(); validity_mapping.put(Integer.valueOf(FieldType.DATE), new String[] {"<", ">", "<=", ">=", "==", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.NUMERIC), new String[] {"<", ">", "<=", ">=", "==", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.FLOAT), new String[] {"<", ">", "<=", ">=", "==", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.BOOLEAN), new String[] {"==", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.IP), new String[] {"==", "=~", "==_ci", "=~_ci", "starts", "ends", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.STRING), new String[] {"<", ">", "<=", ">=", "=~", "=~_ci", "==", "==_ci", "starts", "ends", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.INVID), new String[] {"<", ">", "<=", ">=", "=~", "=~_ci", "==", "==_ci", "starts", "ends", "defined"}); validity_mapping.put(Integer.valueOf(FieldType.PASSWORD), new String[] {"defined"}); validity_mapping.put(Integer.valueOf(FieldType.PERMISSIONMATRIX), new String[] {"defined"}); validity_mapping.put(Integer.valueOf(FieldType.FIELDOPTIONS), new String[] {"defined"}); validity_mapping = Collections.unmodifiableMap(validity_mapping); } // --- DBObjectBase objectBase = null; List<DBObjectBaseField> selectFields = null; boolean editableFilter = false; String myQueryString = null; CommonTree myQueryTree = null; public GanyQueryTransmuter() { } public Query transmuteQueryString(String queryString) throws GanyParseException { myQueryString = queryString; QueryLexer lexer = new QueryLexer(new ANTLRStringStream(queryString)); CommonTokenStream tokens = new CommonTokenStream(lexer); QueryParser parser = new QueryParser(tokens); try { myQueryTree = (CommonTree) parser.query().getTree(); } catch (RecognitionException ex) { } if (myQueryTree == null) { // "Error parsing GanyQL query string. Make sure you've parenthesized and quoted everything properly.\n\n{0}" throw new GanyParseException(ts.l("transmuteQueryString.global_parse_error", queryString)); } QueryNode root = null; try { root = parse_tree(myQueryTree); } catch (RuntimeException ex) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" String mesg = ts.l("global.parse_exception", ex.getMessage(), queryString, myQueryTree.toStringTree()); throw new RuntimeException(mesg, ex); } Query query = new Query(objectBase.getName(), root, this.editableFilter); if (selectFields == null) { query.resetPermitSet(); // return the default list of fields } else { for (int i = 0; i < selectFields.size(); i ++) { DBObjectBaseField field = selectFields.get(i); query.addField(field.getID()); } } // clear out our refs for GC objectBase = null; if (selectFields != null) { selectFields.clear(); selectFields = null; } // et voila return query; } private QueryNode parse_tree(Tree ast) throws GanyParseException { this.objectBase = parse_from_tree(ast.getChild(1)); // we have to look up the object type first this.selectFields = parse_select_tree(ast.getChild(0)); if (ast.getChildCount() > 2) { Tree whereTokenNode = ast.getChild(2); if (whereTokenNode != null && whereTokenNode.getType() == QueryParser.WHERE) { Tree where_node = whereTokenNode.getChild(0); if (where_node != null) { QueryNode where_tree = parse_where_clause(where_node, objectBase); return where_tree; } } } return null; } private DBObjectBase parse_from_tree(Tree ast) throws GanyParseException { if (ast.getChildCount() == 0) // the grammar _should_ prevent this { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "Missing from clause." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_from_tree.missing_from"), myQueryString, myQueryTree.toStringTree())); } String from_objectbase = null; for (int i = 0; i < ast.getChildCount(); i++) { Tree node = ast.getChild(i); if (node.getType() == QueryParser.EDITABLE) { this.editableFilter = true; } else { if (node.getType() == QueryParser.STRING_VALUE) { from_objectbase = StringUtils.dequote(node.getText()); } else if (node.getType() == QueryParser.TOKEN) { from_objectbase = node.getText(); } // we don't allow underscores in object and field type names, // because we reserve that character for representing spaces // in our XML. A user might be querying using the underscore // variant, so take care of that here. from_objectbase = from_objectbase.replace('_', ' '); } } if (from_objectbase == null) // the grammar _should_ prevent this { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "From clause does not contain an object type to search." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_from_tree.no_objectbase"), myQueryString, myQueryTree.toStringTree())); } this.objectBase = Ganymede.db.getObjectBase(from_objectbase); if (objectBase == null) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "The object type "{0}" in the query''s from clause does not exist." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_from_tree.bad_objectbase", from_objectbase), myQueryString, myQueryTree.toStringTree())); } return this.objectBase; } private List<DBObjectBaseField> parse_select_tree(Tree ast) throws GanyParseException { if (ast.getChildCount() == 0) { return null; // "select from", with no field list } if (ast.getChild(0).getType() == QueryParser.OBJECT) { return null; // "select object from", also no field list. } List<DBObjectBaseField> selectFields = new ArrayList<DBObjectBaseField>(); for (int i = 0; i < ast.getChildCount(); i++) { Tree select_node = ast.getChild(i); String field_name = StringUtils.dequote(select_node.getText()); field_name = field_name.replace('_', ' '); DBObjectBaseField field = objectBase.getField(field_name); if (field == null) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "Can''t find field "{0}" in the "{1}" object type. Make sure you have capitalized the field name correctly." throw new GanyParseException(ts.l("global.parse_exception", ts.l("global.no_such_field", field_name, objectBase.getName()), myQueryString, myQueryTree.toStringTree())); } selectFields.add(field); } return selectFields; } private QueryNode parse_where_clause(Tree ast, DBObjectBase base) throws GanyParseException { int root_type; QueryNode child1 = null, child2 = null; DBObjectBase target_objectbase = null; String field_name = null; DBObjectBaseField field = null; byte scalar_operator = -1, vector_operator = -1; int field_type = -1, argument_type = -1; String op; Object argument; Tree field_node, argument_node; /* -- */ root_type = ast.getType(); switch (root_type) { case QueryParser.NOT: return new QueryNotNode(parse_where_clause(ast.getChild(0), base)); case QueryParser.AND: child1 = parse_where_clause(ast.getChild(0), base); child2 = parse_where_clause(ast.getChild(1), base); return new QueryAndNode(child1, child2); case QueryParser.OR: child1 = parse_where_clause(ast.getChild(0), base); child2 = parse_where_clause(ast.getChild(1), base); return new QueryOrNode(child1, child2); case QueryParser.DEREF: field_name = StringUtils.dequote(ast.getChild(0).getText()); field_name = field_name.replace('_', ' '); if (base != null) { field = base.getField(field_name); if (field == null) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "Can''t find field "{0}" in the "{1}" object type. Make sure you have capitalized the field name correctly." throw new GanyParseException(ts.l("global.parse_exception", ts.l("global.no_such_field", field_name, base.getName()), myQueryString, myQueryTree.toStringTree())); } if (field.getType() != FieldType.INVID) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "The "{0}" field can''t be dereferenced. Not an invid field." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_where_clause.not_invid", field_name), myQueryString, myQueryTree.toStringTree())); } short target_objectbase_id = field.getTargetBase(); if (target_objectbase_id >= 0) { target_objectbase = Ganymede.db.getObjectBase(target_objectbase_id); } else { target_objectbase = null; } } else { target_objectbase = null; } child2 = parse_where_clause(ast.getChild(1), target_objectbase); return new QueryDeRefNode(field_name, child2); case QueryParser.BINARY_OPERATOR: case QueryParser.UNARY_OPERATOR: op = ast.getText(); field_node = ast.getChild(0); field_name = StringUtils.dequote(field_node.getText()); field_name = field_name.replace('_', ' '); if (base == null) { field = null; } else { field = base.getField(field_name); if (field == null) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "Can''t find field "{0}" in the "{1}" object type. Make sure you have capitalized the field name correctly." throw new GanyParseException(ts.l("global.parse_exception", ts.l("global.no_such_field", field_name, base.getName()), myQueryString, myQueryTree.toStringTree())); } field_type = field.getType(); } if (root_type == QueryParser.BINARY_OPERATOR) { argument_node = ast.getChild(1); argument_type = argument_node.getType(); argument = parse_argument(op, argument_node.getText(), argument_type, field); } else { argument = null; } /* * If we don't know the objectbase, then we are chasing a * vague pointer derefence, and we have to think. We can't * check and see if the field we're querying is a scalar or a * vector field. Thus, we'll have to trust that the user knows * what he wants. If he's wrong about the vector/scalar type * of the field he's querying, then the query engine will try * its best to figure out what the user really meant. */ if (base == null) { if (op_vector_mapping.containsKey(op)) { Byte opI = (Byte) op_vector_mapping.get(op); vector_operator = opI.byteValue(); scalar_operator = QueryDataNode.NONE; } else if (op_scalar_mapping.containsKey(op)) { Byte opI = (Byte) op_scalar_mapping.get(op); vector_operator = QueryDataNode.NONE; scalar_operator = opI.byteValue(); } else { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "The "{0}" operator makes no sense to me. GanyQueryTransmuter.java has not been kept up to date with the grammar." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_where_clause.mystery_operator", op), myQueryString, myQueryTree.toStringTree())); } } else { if (field.isArray()) { if (op_vector_mapping.containsKey(op)) { Byte opI = (Byte) op_vector_mapping.get(op); vector_operator = opI.byteValue(); scalar_operator = QueryDataNode.NONE; } else if (op_scalar_mapping.containsKey(op)) { Byte opI = (Byte) op_scalar_mapping.get(op); scalar_operator = opI.byteValue(); vector_operator = QueryDataNode.CONTAINS; } else { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "The "{0}" operator makes no sense to me. GanyQueryTransmuter.java has not been kept up to date with the grammar." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_where_clause.mystery_operator", op), myQueryString, myQueryTree.toStringTree())); } } else { if (op_scalar_mapping.containsKey(op)) { Byte opI = (Byte) op_scalar_mapping.get(op); scalar_operator = opI.byteValue(); vector_operator = QueryDataNode.NONE; } } } if (base != null && scalar_operator != QueryDataNode.NONE && !valid_op(op, field_type)) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "The "{0}" operator is not valid on a "{1}" object''s "{2}" field. {2} is of type {3}." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_where_clause.bad_operator", op, base.getName(), field_name, field.getTypeDesc()), myQueryString, myQueryTree.toStringTree())); } return new QueryDataNode(field_name, scalar_operator, vector_operator, argument); default: // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "I couldn''t process parser node type {0}. GanyQueryTransmuter.java has probably not been kept up to date with the grammar." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_where_clause.bad_type", Integer.valueOf(root_type)), myQueryString, myQueryTree.toStringTree())); } } private boolean valid_op(String op, int field_type) { String[] ops = (String[]) validity_mapping.get(Integer.valueOf(field_type)); if (ops == null) // should only happen if field_type is invalid { return false; } for (int i = 0; i < ops.length; i++) { if (ops[i].equals(op)) { return true; } } return false; } private Object parse_argument(String operator, String argument, int argument_type, DBObjectBaseField field) throws GanyParseException { if (field == null) { switch (argument_type) { case QueryParser.INT_VALUE: return Integer.valueOf(argument); case QueryParser.DECIMAL_VALUE: return Double.valueOf(argument); case QueryParser.STRING_VALUE: return StringUtils.dequote(argument); default: // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "Unrecognized argument type parsing argument {0}." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_argument.unrecognized_argument", Integer.valueOf(argument)), myQueryString, myQueryTree.toStringTree())); } } if (field.isArray() && argument_type == QueryParser.INT_VALUE && op_vector_mapping.containsKey(operator)) { return Integer.valueOf(argument); } int field_type = field.getType(); if (field_type == FieldType.NUMERIC && argument_type == QueryParser.INT_VALUE) { return Integer.valueOf(argument); } else if (field_type == FieldType.FLOAT && argument_type == QueryParser.DECIMAL_VALUE) { return new Double(argument); } else if (argument_type == QueryParser.BOOLEAN_VALUE) { if (argument.toLowerCase().equals("true")) { return Boolean.TRUE; } else { return Boolean.FALSE; } } else if (argument_type == QueryParser.STRING_VALUE) { switch (field_type) { case FieldType.STRING: case FieldType.INVID: case FieldType.IP: return StringUtils.dequote(argument); case FieldType.DATE: DateFormat format = DateFormat.getInstance(); try { return format.parse(StringUtils.dequote(argument)); } catch (ParseException ex) { // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "I couldn''t make any sense of "{0}" as a date value." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_argument.bad_date", argument), myQueryString, myQueryTree.toStringTree())); } } } // "An exception was encountered parsing your query string: {0}\nQuery: "{1}"\nExpanded Parse Tree: "{2}"" // "Error, field "{0}" requires a {1} argument type." throw new GanyParseException(ts.l("global.parse_exception", ts.l("parse_argument.bad_argument_type", field.getName(), field.getTypeDesc()), myQueryString, myQueryTree.toStringTree())); } }