/**
* Redistribution and use of this software and associated documentation
* ("Software"), with or without modification, are permitted provided
* that the following conditions are met:
*
* 1. Redistributions of source code must retain copyright
* statements and notices. Redistributions must also contain a
* copy of this document.
*
* 2. Redistributions in binary form must reproduce the
* above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The name "Exolab" must not be used to endorse or promote
* products derived from this Software without prior written
* permission of Intalio, Inc. For written permission,
* please contact info@exolab.org.
*
* 4. Products derived from this Software may not be called "Exolab"
* nor may "Exolab" appear in their names without prior written
* permission of Intalio, Inc. Exolab is a registered
* trademark of Intalio, Inc.
*
* 5. Due credit should be given to the Exolab Project
* (http://www.exolab.org/).
*
* THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Copyright 1999 (C) Intalio, Inc. All Rights Reserved.
*
* $Id: ParseTreeWalker.java,v 1.12 2004/11/29 11:01:01 wguttmann Exp $
*/
package org.exolab.castor.jdo.oql;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import org.exolab.castor.jdo.DbMetaInfo;
import org.exolab.castor.jdo.QueryException;
import org.exolab.castor.jdo.engine.JDOClassDescriptor;
import org.exolab.castor.jdo.engine.JDOFieldDescriptor;
import org.exolab.castor.jdo.engine.SQLEngine;
import org.exolab.castor.mapping.loader.Types;
import org.exolab.castor.persist.LockEngine;
import org.exolab.castor.persist.spi.QueryExpression;
/**
* A class which walks the parse tree created by the parser to check for errors
* and translate to SQL.
*
* @author <a href="mailto:nissim@nksystems.com">Nissim Karpenstein</a>
* @version $Revision: 1.12 $ $Date: 2004/11/29 11:01:01 $
*/
public class ParseTreeWalker implements TokenTypes
{
private LockEngine _dbEngine;
private ParseTreeNode _parseTree;
private String _projectionName;
private String _projectionAlias;
private int _projectionType;
private String _fromClassName;
private String _fromClassAlias;
private ClassLoader _classLoader;
private Class _objClass;
private QueryExpression _queryExpr;
private DbMetaInfo _dbInfo;
private Hashtable _paramInfo;
private Hashtable _fieldInfo;
private Hashtable _pathInfo;
private Hashtable _allPaths;
private SQLEngine _engine;
private JDOClassDescriptor _clsDesc;
//projection types
public static final int AGGREGATE = 1;
public static final int FUNCTION = 2;
public static final int PARENT_OBJECT = 3;
public static final int DEPENDANT_OBJECT = 4;
public static final int DEPENDANT_OBJECT_VALUE = 5;
public static final int DEPENDANT_VALUE = 6;
public static final int MAX_TABLE_LENGTH = 30;
/**
* Creates a new parse tree walker. Which checks the tree for errors, and
* generates a QueryExpression containing the SQL translation.
*
* @param dbEngine The Persistence Engine
* @param parseTree The parse tree to walk
* @param classLoader A ClassLoader instance to load classes.
* @throws QueryException Thrown by checkErrors.
*/
public ParseTreeWalker(LockEngine dbEngine, ParseTreeNode parseTree, ClassLoader classLoader, DbMetaInfo dbInfo)
throws QueryException
{
_dbEngine = dbEngine;
_parseTree = parseTree;
_classLoader = classLoader;
_dbInfo = dbInfo;
_paramInfo = new Hashtable();
_fieldInfo = new Hashtable();
_pathInfo = new Hashtable();
_allPaths = new Hashtable();
if ( ! _parseTree.isRoot() )
throw (new QueryException( "ParseTreeWalker must be created with the root node of the parse tree."));
//_log.debug (ParseTest.treeToString(_parseTree, ParseTest.NODE_TYPES));
//_log.debug (ParseTest.treeToString(_parseTree, ParseTest.NODE_VALUES));
checkErrors();
createQueryExpression();
}
/**
* Accessor method for _objClass.
*
* @return The _objClass member.
*/
public Class getObjClass() {
return _objClass;
}
/**
* Accessor method for _projectionType.
*
* @return The _projectionType member.
*/
public int getProjectionType() {
return _projectionType;
}
/**
* Accessor method for private _queryExpr member.
*
* @return private _queryExpr member
*/
public QueryExpression getQueryExpression() {
return _queryExpr;
}
/**
* Accessor method for _paramInfo.
*
* @return The _paramInfo member.
*/
public Hashtable getParamInfo() {
return _paramInfo;
}
/**
* Accessor method for _clsDesc.
*
* @return The _clsDesc member.
*/
public JDOClassDescriptor getClassDescriptor() {
return _clsDesc;
}
/**
* Method to get path info for the selected object. This is the path which
* will be used by the QueryResults to follow the path if the object
* selected is a DEPENDANT_OBJECT or DEPENDANT_OBJECT_VALUE. Any other
* projectionTypes do not need this, so null will be returned.
* @return Path info for the selected element, null otherwise.
*/
public Vector getPathInfo() {
switch ( _projectionType ) {
case DEPENDANT_OBJECT:
case DEPENDANT_OBJECT_VALUE:
ParseTreeNode projectionNode;
if ( _parseTree.getChild(0).getToken().getTokenType() == KEYWORD_DISTINCT )
projectionNode = _parseTree.getChild(1);
else
projectionNode = _parseTree.getChild(0);
if ( projectionNode.getToken().getTokenType() == KEYWORD_AS )
projectionNode = projectionNode.getChild(0);
return ( (Vector) _pathInfo.get( projectionNode ) );
default:
return null;
}
}
/**
* Traverses the tree checking for errors.
*
* @throws QueryException if there is an error.
*/
private void checkErrors() throws QueryException {
for (Enumeration e = _parseTree.children(); e.hasMoreElements(); ) {
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
if ( curChild.getToken().getTokenType() == KEYWORD_FROM ) {
checkFromPart( curChild.getChild(0) );
break;
}
}
if ( _parseTree.getChild(0).getToken().getTokenType() == KEYWORD_DISTINCT )
checkSelectPart( _parseTree.getChild(1) );
else
checkSelectPart( _parseTree.getChild(0) );
for ( int curChild = 2; curChild <= _parseTree.getChildCount() - 1; curChild++ ) {
int tokenType = _parseTree.getChild(curChild).getToken().getTokenType();
switch ( tokenType ) {
case KEYWORD_WHERE:
checkWhereClause( _parseTree.getChild(curChild) );
break;
case KEYWORD_ORDER:
checkOrderClause( _parseTree.getChild(curChild) );
break;
case KEYWORD_LIMIT:
checkLimitClause( _parseTree.getChild(curChild) );
break;
case KEYWORD_OFFSET:
checkOffsetClause( _parseTree.getChild(curChild) );
break;
}
}
}
/**
* Checks the from parts of the query.
*
* @param fromPart is the ParseTreeNode containing the from part of the
* queryTree.
* @throws QueryException if there is an error.
*/
private void checkFromPart( ParseTreeNode fromPart )
throws QueryException {
//get the class name from the from clause
if (fromPart.getToken().getTokenType() == KEYWORD_AS) {
ParseTreeNode classNameNode = fromPart.getChild(0);
if ( classNameNode.getToken().getTokenType() == DOT ) {
StringBuffer sb = new StringBuffer();
for (Enumeration e = classNameNode.children(); e.hasMoreElements(); ) {
ParseTreeNode theChild = (ParseTreeNode) e.nextElement();
sb.append( theChild.getToken().getTokenValue() ).append(".");
}
sb.setLength( sb.length() - 1 );
_fromClassName = sb.toString();
}
else
_fromClassName = classNameNode.getToken().getTokenValue();
_fromClassAlias = fromPart.getChild(1).getToken().getTokenValue();
}
else {
if ( fromPart.getToken().getTokenType() == DOT ) {
StringBuffer sb = new StringBuffer();
for (Enumeration e = fromPart.children(); e.hasMoreElements(); ) {
ParseTreeNode theChild = (ParseTreeNode) e.nextElement();
sb.append( theChild.getToken().getTokenValue() ).append(".");
}
_fromClassName = sb.deleteCharAt( sb.length() - 1 ).toString();
}
else
_fromClassName = fromPart.getToken().getTokenValue();
_fromClassAlias = _fromClassName;
}
try {
if ( _classLoader == null )
_objClass = Class.forName( _fromClassName );
else
_objClass = _classLoader.loadClass( _fromClassName );
}
catch ( ClassNotFoundException except ) {
throw new QueryException( "Could not find class " + _fromClassName );
}
_engine = (SQLEngine) _dbEngine.getPersistence( _objClass );
if ( _engine == null )
throw new QueryException( "Could not find mapping for class " + _fromClassName );
_clsDesc = _engine.getDescriptor();
// This should never happen
if ( _clsDesc == null )
throw new QueryException( "Could not get a descriptor for class " + _fromClassName );
}
/**
* Checks the select part of the query.
*
* @param selectPart is the ParseTreeNode containing the select part of the
* queryTree.
* @throws QueryException if there is an error.
*/
private void checkSelectPart( ParseTreeNode selectPart )
throws QueryException {
if (selectPart.getToken().getTokenType() == KEYWORD_AS) {
checkProjection( selectPart.getChild(0), true, false );
_projectionAlias = selectPart.getChild(1).getToken().getTokenValue();
}
else {
checkProjection( selectPart, true, false );
_projectionAlias = "";
}
//if ( ! _projectionName.equals(_fromClassAlias) )
// throw new QueryException( "Object name not the same in SELECT and FROM - select: " + _projectionName + ", from: " + _fromClassAlias );
}
/**
* Search for the field in the given class descriptor and descriptors of the superclasses,
* return null if not found.
* @param fieldName The field name.
* @param clsDesc A JDO class descriptor.
* @return JDOFieldDescriptor for the specified field, null if not found.
*/
private JDOFieldDescriptor getFieldDesc( String fieldName, JDOClassDescriptor clsDesc ) {
JDOFieldDescriptor field;
for ( JDOClassDescriptor cd = clsDesc; cd != null; cd = (JDOClassDescriptor) cd.getExtends() ) {
field = cd.getField(fieldName);
if ( field != null )
return field;
}
return null;
}
/**
* Search for the field in the given class descriptor and descriptors of the superclasses,
* return null if not found.
* Returns new Object[2] {fieldDesc, classDesc}, where classDesc is a descriptor of the class where
* the field was found.
* Also adds inner joins to the QueryExpression.
* @param path The path info vector to build the alias with
* @param tableIndex Field index in the path info
*/
private Object[] getFieldAndClassDesc(String fieldName, JDOClassDescriptor clsDesc, QueryExpression expr,
Vector path, int tableIndex) {
JDOFieldDescriptor field = null;
JDOClassDescriptor cd = clsDesc;
JDOFieldDescriptor tempField = null;
JDOClassDescriptor tempCd = clsDesc;
Object[] retVal;
while (tempCd != null) {
tempField = tempCd.getField(fieldName);
if (tempField != null) {
field = tempField;
cd = tempCd;
}
tempCd = (JDOClassDescriptor) tempCd.getExtends();
}
if (field == null) {
return null;
}
// prepare the return value
retVal = new Object[] {field, cd};
if (cd != clsDesc) {
// now add inner join for "extends"
String tableAlias1 = clsDesc.getTableName();
String tableAlias2 = cd.getTableName();
if (tableIndex > 0) {
tableAlias1 = buildTableAlias(tableAlias1, path, tableIndex);
tableAlias2 = buildTableAlias(tableAlias2, path, tableIndex);
}
expr.addTable(cd.getTableName(), tableAlias2);
expr.addInnerJoin(clsDesc.getTableName(), clsDesc.getIdentityColumnNames(), tableAlias1,
cd.getTableName(), cd.getIdentityColumnNames(), tableAlias2);
}
return retVal;
}
/**
* Checks a projection (one of the items selected). Determines the
* _projectionType.
*
* @param projection The ParseTreeNode containing the projection.
* @param topLevel pass true when calling on the top level of projection
* in the parse tree. False when recursing
* @param onlySimple pass true if you want to throw errors if the
* object passed as the ParseTreeNode is not a simple type (use this
* when recursing on the arguments passed to Aggregate and SQL functions).
* @throws QueryException if there is an error.
* @return a JDOFieldDescriptor representation of the field, if it represents a field.
*/
private JDOFieldDescriptor checkProjection( ParseTreeNode projection,
boolean topLevel,
boolean onlySimple )
throws QueryException {
JDOFieldDescriptor field = null;
if ( projection.getChildCount() == 0 ) {
if ( topLevel ) {
_projectionType = PARENT_OBJECT;
_projectionName = projection.getToken().getTokenValue();
if ( ! _projectionName.equals(_fromClassAlias) )
throw new QueryException( "Object name not the same in SELECT and FROM - select: " + _projectionName + ", from: " + _fromClassAlias );
}
else
if ( onlySimple )
throw new QueryException( "Only primitive values are allowed to be passed as parameters to Aggregate and SQL functions." );
else
return null;
}
else {
int tokenType = projection.getToken().getTokenType();
switch( tokenType ) {
case IDENTIFIER:
//a SQL function call -- check the arguments
_projectionType = FUNCTION;
for (Enumeration e = projection.getChild(0).children();
e.hasMoreElements(); )
checkProjection( (ParseTreeNode) e.nextElement(), false, true );
break;
case DOT:
//a path expression -- check if it is valid, and create a paramInfo
Enumeration e = projection.children();
ParseTreeNode curNode = null;
String curName = null;
StringBuffer projectionName = new StringBuffer();
Vector projectionInfo = new Vector();
//check that the first word before the dot is our class
if ( e.hasMoreElements() ) {
curNode = (ParseTreeNode) e.nextElement();
curName = curNode.getToken().getTokenValue();
if ( ( ! curName.equals(_projectionName) ) &&
( ! curName.equals(_projectionAlias) ) &&
( ! curName.equals(_fromClassName) ) &&
( ! curName.equals(_fromClassAlias) ) ) {
// reset the enumeration
e = projection.children();
curName = _fromClassAlias;
}
projectionName.append(curName);
projectionInfo.addElement(curName);
}
//use the ClassDescriptor to check that the rest of the path is valid.
JDOClassDescriptor curClassDesc = _clsDesc;
JDOFieldDescriptor curField = null;
int count = 0;
String curToken;
while ( e.hasMoreElements() ) {
// there may be nested attribute name
curField = null;
curName = null;
while ( curField == null && e.hasMoreElements() ) {
curNode = (ParseTreeNode) e.nextElement();
curToken = curNode.getToken().getTokenValue();
if ( curName == null ) {
curName = curToken;
} else {
curName = curName + "." + curToken;
}
curField = getFieldDesc(curName, curClassDesc);
}
if ( curField == null )
throw new QueryException( "An unknown field was requested: " + curName + " (" + curClassDesc + ")" );
projectionName.append(".").append(curName);
projectionInfo.addElement(curName);
curClassDesc = (JDOClassDescriptor) curField.getClassDescriptor();
if ( curClassDesc == null && e.hasMoreElements() )
throw new QueryException( "An non-reference field was requested: " + curName + " (" + curClassDesc + ")" );
count++;
}
field = curField;
_pathInfo.put( projection, projectionInfo );
_fieldInfo.put( projection, curField );
Class theClass = curField.getFieldType();
// is it actually a Java primitive, or String,
// or a subclass of Number
boolean isSimple = Types.isSimpleType(theClass);
if ( topLevel ) {
_projectionName = projectionName.toString();
if ( isSimple )
if ( count > 1 || field.getContainingClassDescriptor() != _clsDesc)
_projectionType = DEPENDANT_OBJECT_VALUE;
else
_projectionType = DEPENDANT_VALUE;
else
_projectionType = DEPENDANT_OBJECT;
}
else
if ( ! isSimple && onlySimple )
throw new QueryException( "Only primitive values are allowed to be passed as parameters to Aggregate and SQL functions." );
break;
case KEYWORD_COUNT:
//count a special case
_projectionType = AGGREGATE;
if ( projection.getChild(0).getToken().getTokenType() == TIMES )
//count(*)
break;
else
//can call count on object types -- recurse over child
checkProjection( projection.getChild(0), false, false );
break;
case KEYWORD_SUM:
case KEYWORD_MIN:
case KEYWORD_MAX:
case KEYWORD_AVG:
//an aggregate -- recurse over the child
_projectionType = AGGREGATE;
checkProjection( projection.getChild(0), false, true );
break;
default:
//we use function to describe sql expressions also, like
// SELECT s.age - 5 FROM Student s
_projectionType = FUNCTION;
for (e = projection.children(); e.hasMoreElements(); )
checkProjection( (ParseTreeNode) e.nextElement(), false, false );
}
}
return field;
}
/**
* Traverses the where clause sub-tree and checks for errors. Creates a
* Hashtables with FieldInfo for fields of selected objects which are
* mentioned in the where clause (i.e. nodes with tokenType of IDENTIFIER
* or (DOT, IDENTIFIER, IDENTIFIER)). Als creates a Hashtable of paramInfo
* with type information for query parameters (i.e. $(Class)1 or $1).
*
* @param whereClause WHERE-clause to traverse.
* @throws QueryException if an error is detected.
*/
private void checkWhereClause(ParseTreeNode whereClause)
throws QueryException {
Enumeration e;
int tokenType = whereClause.getToken().getTokenType();
switch (tokenType) {
case DOT:
checkProjection( whereClause, false, false );
break;
case IDENTIFIER:
e = whereClause.children();
if ( e.hasMoreElements() ) {
if ( whereClause.getChild(0).getToken().getTokenType() == LPAREN ) {
// A function.
while ( e.hasMoreElements() ) {
checkWhereClause( (ParseTreeNode) e.nextElement() );
}
}
} else {
checkField(whereClause);
}
break;
case DOLLAR:
checkParameter(whereClause);
break;
case KEYWORD_IN:
checkField(whereClause.getChild(0));
checkInClauseRightSide(whereClause.getChild(1));
default:
for (e = whereClause.children(); e.hasMoreElements(); ) {
checkWhereClause( (ParseTreeNode) e.nextElement() );
}
}
}
/**
* Traverses the limit clause sub-tree and checks for errors. Creates
* a Hashtable of paramInfo with type information for query parameters
* (i.e. $1).
* @param limitClause LIMIT-clause to traverse.
* @throws QueryException if an error is detected.
*/
private void checkLimitClause(ParseTreeNode limitClause)
throws QueryException {
int tokenType = limitClause.getToken().getTokenType();
switch (tokenType) {
case DOLLAR:
checkParameter(limitClause);
break;
default:
for (Enumeration e = limitClause.children(); e.hasMoreElements(); ) {
checkLimitClause( (ParseTreeNode) e.nextElement() );
}
}
}
/**
* Traverses the offset clause sub-tree and checks for errors. Creates
* a Hashtable of paramInfo with type information for query parameters
* (i.e. $1).
* @param offsetClause OFFSET-clause to traverse.
* @throws QueryException if an error is detected.
*/
private void checkOffsetClause(ParseTreeNode offsetClause)
throws QueryException {
int tokenType = offsetClause.getToken().getTokenType();
switch (tokenType) {
case DOLLAR:
checkParameter(offsetClause);
break;
default:
for (Enumeration e = offsetClause.children(); e.hasMoreElements(); ) {
checkLimitClause( (ParseTreeNode) e.nextElement() );
}
}
}
/**
* Checks whether the field passed in is valid within this object. Also
* adds this field to a Hashtable.
*
* @param fieldTree A leaf node containing an identifier, or a tree with DOT
* root, and two IDENTIFIER children (for fields that look like
* Person.name or Person->name)
* @return a JDOFieldDescriptor representation of this field.
* @throws QueryException if the field does not exist.
*/
private JDOFieldDescriptor checkField(ParseTreeNode fieldTree)
throws QueryException {
//see if we've checked this field before.
JDOFieldDescriptor field = (JDOFieldDescriptor)_fieldInfo.get(fieldTree);
if ( field != null )
return field;
if ( fieldTree.getToken().getTokenType() == DOT ) {
field = checkProjection( fieldTree, false, false );
} else {
field = getFieldDesc( fieldTree.getToken().getTokenValue(), _clsDesc );
if (field != null)
_fieldInfo.put(fieldTree, field);
}
if (field == null)
throw new QueryException( "The field " + fieldTree.getToken().getTokenValue() + " was not found." );
return field;
}
/**
* Checks a numbered parameter from an OQL Parse Tree. Creates a {@link ParamInfo}
* object which stores the user or system defined class for this parameter.
* If there's a user defined type for this parameter it is compared to see if
* it is castable from the system defined class. If not, an exception is
* thrown. If a user defined type is specified for a numbered parameter which
* has already been examined, and the user defined types don't match an
* exception is thrown.
*
* @param paramTree the Tree node containing DOLLAR, with children user
* defined class (if available) and parameter number.
* @throws QueryException if an invalid class is specified by the user.
*/
private void checkParameter(ParseTreeNode paramTree) throws QueryException {
//get the parameter number and user defined type
Integer paramNumber;
String userDefinedType = "";
if (paramTree.getChildCount() == 1)
paramNumber = Integer.decode(paramTree.getChild(0)
.getToken().getTokenValue());
else {
paramNumber = Integer.decode(paramTree.getChild(1)
.getToken().getTokenValue());
userDefinedType = paramTree.getChild(0).getToken().getTokenValue();
}
//Get the system defined type
String systemType = "";
// Get the SQL type if known
JDOFieldDescriptor desc = null;
int operation = paramTree.getParent().getToken().getTokenType();
switch (operation) {
case PLUS: case MINUS: case TIMES:
case DIVIDE: case KEYWORD_MOD: case KEYWORD_ABS:
case KEYWORD_LIMIT: //Alex
systemType = "java.lang.Number";
break;
case KEYWORD_OFFSET:
systemType = "java.lang.Number";
break;
case KEYWORD_LIKE: case CONCAT:
systemType = "java.lang.String";
break;
case KEYWORD_AND: case KEYWORD_OR: case KEYWORD_NOT:
systemType = "java.lang.Boolean";
break;
case EQUAL: case NOT_EQUAL: case GT:
case GTE: case LT: case LTE: case KEYWORD_BETWEEN:
systemType = getParamTypeForComparison(paramTree.getParent());
desc = getJDOFieldDescriptor(paramTree.getParent());
break;
case KEYWORD_LIST:
systemType = getParamTypeForList(paramTree.getParent());
break;
}
//get the param info for this numbered param
ParamInfo paramInfo = (ParamInfo) _paramInfo.get(paramNumber);
if ( paramInfo == null ) {
paramInfo = new ParamInfo(userDefinedType, systemType, desc, _classLoader);
_paramInfo.put(paramNumber, paramInfo);
}
else
paramInfo.check(userDefinedType, systemType);
}
private String getParamTypeForComparison(ParseTreeNode comparisonTree)
throws QueryException
{
for (Enumeration e = comparisonTree.children(); e.hasMoreElements(); ) {
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
int tokenType = curChild.getToken().getTokenType();
switch(tokenType) {
case STRING_LITERAL: return "java.lang.String";
case DOUBLE_LITERAL: return "java.lang.Double";
case LONG_LITERAL: return "java.lang.Long";
case BOOLEAN_LITERAL: return "java.lang.Boolean";
case CHAR_LITERAL: return "java.lang.Character";
case DATE_LITERAL: return "java.util.Date";
case TIME_LITERAL:
case TIMESTAMP_LITERAL: return "java.util.Time";
case DOT:
case IDENTIFIER: {
JDOFieldDescriptor field = checkField(curChild);
return field.getFieldType().getName();
}
}
}
throw new QueryException( "Could not get type for comparison." );
}
/**
* Determine type of the list expression from the first contained
* constant. If there are only bind variables without user defined
* type present in the list, default to String as type.
*
* @param listTree non-empty parser tree of list expression
* @return class name of the list type
* @throws QueryException
*/
private String getParamTypeForList(ParseTreeNode listTree) throws QueryException
{
for (Enumeration e = listTree.children(); e.hasMoreElements(); ) {
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
int tokenType = curChild.getToken().getTokenType();
switch(tokenType) {
case STRING_LITERAL: return "java.lang.String";
case DOUBLE_LITERAL: return "java.lang.Double";
case LONG_LITERAL: return "java.lang.Long";
case BOOLEAN_LITERAL: return "java.lang.Boolean";
case CHAR_LITERAL: return "java.lang.Character";
case DATE_LITERAL: return "java.util.Date";
case TIME_LITERAL:
case TIMESTAMP_LITERAL: return "java.util.Time";
case DOLLAR: // look for bind parameters with user defined type
if (curChild.getChildCount() == 2) {
String userDefinedType = curChild.getChild(0).getToken().getTokenValue();
try {
return Types.typeFromName(_classLoader, userDefinedType).getName();
} catch(ClassNotFoundException e1) {
throw new QueryException("Could not find class " + userDefinedType);
}
}
}
}
return "java.lang.String"; // default to String, if only simple bind variables are present in the list
}
private JDOFieldDescriptor getJDOFieldDescriptor(ParseTreeNode comparisonTree)
throws QueryException
{
for (Enumeration e = comparisonTree.children(); e.hasMoreElements(); ) {
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
int tokenType = curChild.getToken().getTokenType();
if ((tokenType == DOT) || (tokenType == IDENTIFIER)) {
return checkField(curChild);
}
}
return null;
}
/**
* Checks the right side of an IN clause. it must be a LIST, and
* the children must all be literals.
*
* @param theList the ParseTreeNode containing the list which is the
* right side argument to IN.
* @throws QueryException if theList is not a list, or the list
* contains non literals.
*/
private void checkInClauseRightSide(ParseTreeNode theList)
throws QueryException
{
if ( theList.getToken().getTokenType() != KEYWORD_LIST )
throw new QueryException( "The right side of the IN operator must be a LIST." );
for ( Enumeration e = theList.children(); e.hasMoreElements(); ) {
ParseTreeNode node = (ParseTreeNode) e.nextElement();
int tokenType = node.getToken().getTokenType();
switch(tokenType) {
case KEYWORD_NIL: case KEYWORD_UNDEFINED:
case BOOLEAN_LITERAL: case LONG_LITERAL:
case DOUBLE_LITERAL: case CHAR_LITERAL:
case STRING_LITERAL: case DATE_LITERAL:
case TIME_LITERAL: case TIMESTAMP_LITERAL:
break;
case DOLLAR: // bind variable parameter
checkParameter(node);
break;
default:
throw new QueryException( "The LIST can only contain literals, bind variables and the keywords 'nil' and 'undefined'." );
}
}
}
/**
* Traverses the order by clause sub-tree and checks for errors.
*
* @param orderClause ORDER-BY clause to traverse.
* @throws QueryException if an error is detected.
*/
private void checkOrderClause(ParseTreeNode orderClause)
throws QueryException {
if ( orderClause.getToken().getTokenType() != KEYWORD_ORDER )
throw new QueryException( "checkOrderClause was called on a subtree which is not an order clause.");
ParseTreeNode prevChild = null;
for (Enumeration e = orderClause.children(); e.hasMoreElements(); ) {
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
int tokenType = curChild.getToken().getTokenType();
switch (tokenType) {
case KEYWORD_ASC:
case KEYWORD_DESC:
// iterate on child
curChild = curChild.getChild(0);
tokenType = curChild.getToken().getTokenType();
}
switch (tokenType) {
case DOT:
checkProjection( curChild, false, false );
break;
case IDENTIFIER:
if ( curChild.children().hasMoreElements() ) {
if ( curChild.getChild(0).getToken().getTokenType() == LPAREN ) {
// A function, skip to next element
Enumeration arguments = curChild.getChild(0).children();
while(arguments.hasMoreElements()) {
ParseTreeNode nn = (ParseTreeNode)arguments.nextElement();
checkWhereClause(nn);
}
}
} else {
checkField(curChild);
}
break;
case LPAREN:
if ( ( prevChild == null ) || ( prevChild.getToken().getTokenType() != IDENTIFIER ) )
throw new QueryException("Illegal use of left parenthesis in ORDER BY clause.");
break;
default:
throw new QueryException( "Only identifiers, path expressions, and the keywords ASC and DESC are allowed in the ORDER BY clause." );
}
prevChild = curChild;
}
}
/**
* Generates the QueryExpression which is an SQL representation or the OQL
* parse tree.
* @throws SyntaxNotSupportedException If a specific OQL feature is not supported by a RDBMS.
*/
private void createQueryExpression()
throws SyntaxNotSupportedException
{
//We use the SQLEngine buildfinder for any queries which require
//us to load the entire object from the database. Otherwise
//we use the local addSelectFromJoins method.
//System.out.println( _projectionType );
switch ( _projectionType ) {
case PARENT_OBJECT:
case DEPENDANT_OBJECT:
case DEPENDANT_OBJECT_VALUE:
_queryExpr = _engine.getFinder();
break;
default:
_queryExpr = _engine.getQueryExpression();
addSelectFromJoins();
}
_queryExpr.setDbMetaInfo(_dbInfo);
//check for DISTINCT
if ( _parseTree.getChild(0).getToken().getTokenType() == KEYWORD_DISTINCT )
_queryExpr.setDistinct(true);
//process where clause and order clause
for ( Enumeration e = _parseTree.children(); e.hasMoreElements(); ) {
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
int tokenType = curChild.getToken().getTokenType();
switch ( tokenType ) {
case KEYWORD_WHERE:
addWhereClause(curChild);
break;
case KEYWORD_ORDER:
_queryExpr.addOrderClause( getOrderClause( curChild ) );
break;
case KEYWORD_LIMIT:
addLimitClause(curChild);
break;
case KEYWORD_OFFSET:
addOffsetClause(curChild);
break;
}
}
}
/**
* Adds the selected fields, and the necessary joins to the QueryExpression.
* This method is used when the query is for dependant values, aggregates,
* or SQL functions, where we're just gonna query the item directly.
*
*/
private void addSelectFromJoins() {
ParseTreeNode selectPart = null;
if ( _parseTree.getChild(0).getToken().getTokenType() == KEYWORD_DISTINCT )
selectPart = _parseTree.getChild(1);
else
selectPart = _parseTree.getChild(0);
_queryExpr.addTable( _clsDesc.getTableName() );
_queryExpr.addSelect( getSQLExpr( selectPart ) );
}
/**
* Builds the alias name for a table from the path info.
*
* @param tableName The name of the table to add to the select clause
* @param path The path info vector to build the alias with
* @param tableIndex Field index in the path info
* @return Alias name for a given table.
*/
public String buildTableAlias( String tableName, Vector path, int tableIndex )
{
/* FIXME This aliasing will cause problems if a (different) table
with the same name as our alias already exists (or will be added
at a later time) in the query. Catching this might become a bit
tricky ... */
String tableAlias = tableName;
int index;
Integer i;
/* We don't just alias all tables here, but only those where some
attribute is queried (not those where we query a whole object).
The reasons for this are impossible for me to explain right now,
as I am rather tired; I'll write some better explanation later -
I promise! :-) */
if( path != null && path.size() > 2 ) {
Vector tablePath = new Vector( path.subList( 0, tableIndex + 1 ) );
i = (Integer) _allPaths.get( tablePath );
if ( i == null ) {
index = _allPaths.size();
_allPaths.put( tablePath, new Integer( index ) );
} else {
index = i.intValue();
}
// fix for oracle support. If we have a 30 character table name, adding
// anything to it (_#) will cause it to be too long and will make oracle
// barf. the length we are trying to evaluate is:
// length of the table name + the length of the index number we are using
// + the _ character.
String stringIndex = String.valueOf(index);
if(tableAlias.length() + stringIndex.length() + 1 > MAX_TABLE_LENGTH) {
// use a substring of the original table name, rather than the table alias
// because if we truncate the table alias it could become "ununique"
// (by truncating the _# we add) by truncating the table name
// beforehand we are guaranteed uniqueness as long as the index is
// not reused.
tableAlias = tableAlias.substring(MAX_TABLE_LENGTH - (stringIndex.length() + 1));
}
// If table name contains '.', it should be replaced, since such names aren't allowed for aliases
tableAlias = tableAlias.replace('.', '_') + "_" + index;
}
//System.out.println( "TableAlias: " + tableAlias );
return tableAlias;
}
/**
* Adds joins to the queryExpr for path expressions in the OQL.
*
* @param path Path expression to be added to JOIN clause.
*/
private void addJoinsForPathExpression( Vector path ) {
if ( path == null )
throw new IllegalStateException( "path = null !" );
// the class for the join is even this class
// or one of the base classes
JDOClassDescriptor sourceClass = _clsDesc;
for ( int i = 1; i < path.size() - 1; i++ ) {
JDOFieldDescriptor fieldDesc = null;
// Find the sourceclass and the fielsddescriptor
// in the class hierachie
Object[] fieldAndClass = getFieldAndClassDesc((String) path.elementAt(i), sourceClass,
_queryExpr, path, i - 1);
if (fieldAndClass == null) {
throw new IllegalStateException( "Field not found:" + path.elementAt(i));
}
fieldDesc = (JDOFieldDescriptor) fieldAndClass[0];
sourceClass = (JDOClassDescriptor) fieldAndClass[1];
JDOClassDescriptor clsDesc = (JDOClassDescriptor) fieldDesc.getClassDescriptor();
if ( clsDesc != null /*&& clsDesc != sourceClass*/ ) {
//we must add this table as a join
if ( fieldDesc.getManyKey() == null ) {
//a many -> one relationship
JDOFieldDescriptor foreignKey = (JDOFieldDescriptor) clsDesc.getIdentity();
String sourceTableAlias = sourceClass.getTableName();
if ( i > 1 )
sourceTableAlias = buildTableAlias( sourceTableAlias, path, i - 1 );
_queryExpr.addInnerJoin( sourceClass.getTableName(),
fieldDesc.getSQLName(),
sourceTableAlias,
clsDesc.getTableName(),
foreignKey.getSQLName(),
buildTableAlias( clsDesc.getTableName(), path, i ) );
} else if ( fieldDesc.getManyTable() == null ) {
//a one -> many relationship
JDOFieldDescriptor identity = (JDOFieldDescriptor) sourceClass.getIdentity();
String sourceTableAlias = sourceClass.getTableName();
if ( i > 1 )
sourceTableAlias = buildTableAlias( sourceTableAlias, path, i - 1 );
_queryExpr.addInnerJoin( sourceClass.getTableName(),
identity.getSQLName(),
sourceTableAlias,
clsDesc.getTableName(),
fieldDesc.getManyKey(),
buildTableAlias( clsDesc.getTableName(), path, i ) );
} else {
//a many -> many relationship
JDOFieldDescriptor identity = (JDOFieldDescriptor) sourceClass.getIdentity();
JDOFieldDescriptor foreignKey = (JDOFieldDescriptor) clsDesc.getIdentity();
String manyTableAlias = fieldDesc.getManyTable();
String sourceTableAlias = sourceClass.getTableName();
if ( i > 1 ) {
manyTableAlias = buildTableAlias( manyTableAlias, path, i - 1 );
sourceTableAlias = buildTableAlias( sourceTableAlias, path, i - 1 );
}
_queryExpr.addInnerJoin( sourceClass.getTableName(),
identity.getSQLName(),
sourceTableAlias,
fieldDesc.getManyTable(),
fieldDesc.getManyKey(),
manyTableAlias);
_queryExpr.addInnerJoin( fieldDesc.getManyTable(),
fieldDesc.getSQLName(),
manyTableAlias,
clsDesc.getTableName(),
foreignKey.getSQLName(),
buildTableAlias( clsDesc.getTableName(), path, i ) );
}
sourceClass = clsDesc;
}
}
}
/**
* Adds a SQL version of an OQL where clause.
*
* @param whereClause the parse tree node with the where clause
*/
private void addWhereClause(ParseTreeNode whereClause)
{
String sqlExpr = getSQLExpr(whereClause.getChild(0));
_queryExpr.addWhereClause(sqlExpr);
}
/**
* Adds a SQL version of an OQL limit clause.
*
* @param limitClause the parse tree node with the limit clause
* @throws SyntaxNotSupportedException If the LIMIT clause is not supported by a RDBMS.
*/
private void addLimitClause(ParseTreeNode limitClause)
throws SyntaxNotSupportedException
{
String sqlExpr = getSQLExpr(limitClause/*.getChild(0)*/);
_queryExpr.addLimitClause(sqlExpr);
}
/**
* Adds a SQL version of an OQL offset clause.
* @param offsetClause The parse tree node with the offset clause
* @throws SyntaxNotSupportedException If the LIMIT clause is not supported by a RDBMS.
*/
private void addOffsetClause(ParseTreeNode offsetClause)
throws SyntaxNotSupportedException
{
String sqlExpr = getSQLExpr(offsetClause/*.getChild(0)*/);
_queryExpr.addOffsetClause(sqlExpr);
}
/**
* Returns a SQL version of an OQL expr.
*
* @param exprTree the parse tree node with the expr
* @return The SQL translation of the expr.
*/
private String getSQLExpr(ParseTreeNode exprTree) {
StringBuffer sb = null;
int tokenType = exprTree.getToken().getTokenType();
switch (tokenType) {
//Parens passed through from where clause in SQL
case LPAREN:
return "( " + getSQLExpr( exprTree.getChild(0) ) + " )";
//(possible) unary operators
case PLUS: case MINUS: case KEYWORD_ABS: case KEYWORD_NOT:
if ( exprTree.getChildCount() == 1 )
return exprTree.getToken().getTokenValue() + " "
+ getSQLExpr( exprTree.getChild(0) );
else
//this was binary PLUS or MINUS
return getSQLExpr( exprTree.getChild(0) ) + " "
+ exprTree.getToken().getTokenValue() + " "
+ getSQLExpr( exprTree.getChild(1) );
//binary operators
case KEYWORD_AND: case KEYWORD_OR:
case EQUAL: case NOT_EQUAL: case CONCAT:
case GT: case GTE: case LT: case LTE:
case TIMES: case DIVIDE: case KEYWORD_MOD:
case KEYWORD_LIKE: case KEYWORD_IN:
return getSQLExpr( exprTree.getChild(0) ) + " "
+ exprTree.getToken().getTokenValue() + " "
+ getSQLExpr( exprTree.getChild(1) );
//tertiary BETWEEN operator
case KEYWORD_BETWEEN:
return getSQLExpr( exprTree.getChild(0) ) + " "
+ exprTree.getToken().getTokenValue() + " "
+ getSQLExpr( exprTree.getChild(1) ) + " AND "
+ getSQLExpr( exprTree.getChild(2) );
//built in functions
case KEYWORD_IS_DEFINED:
return getSQLExpr( exprTree.getChild(0) ) + " IS NOT NULL ";
case KEYWORD_IS_UNDEFINED:
return getSQLExpr( exprTree.getChild(0) ) + " IS NULL ";
case KEYWORD_COUNT:
if ( exprTree.getChild(0).getToken().getTokenType() == TIMES )
return " COUNT(*) ";
else
return " COUNT(" + getSQLExpr(exprTree.getChild(0)) +") ";
case KEYWORD_SUM:
case KEYWORD_MIN:
case KEYWORD_MAX:
case KEYWORD_AVG:
return " " + exprTree.getToken().getTokenValue() + "(" +
getSQLExpr(exprTree.getChild(0)) + ") ";
//List creation
case KEYWORD_LIST:
sb = new StringBuffer("( ");
for (Enumeration e = exprTree.children(); e.hasMoreElements(); )
sb.append( getSQLExpr( (ParseTreeNode) e.nextElement() ) )
.append(" , ");
//replace final comma space with close paren.
sb.replace(sb.length() - 2, sb.length() - 1, " )").append(" ");
return sb.toString();
//fields or SQL functions
case IDENTIFIER: case DOT:
if ( exprTree.getChildCount() > 0 &&
exprTree.getChild(0/* was '1' thth */).getToken().getTokenType() == LPAREN ) {
//An SQL function
sb = new StringBuffer(exprTree.getToken().getTokenValue()).append("(");
int paramCount = 0;
Enumeration e = exprTree.children();
e = ((ParseTreeNode) e.nextElement()).children(); // LPAREN's children
for (; e.hasMoreElements(); ) {
sb.append( getSQLExpr( (ParseTreeNode) e.nextElement() ) )
.append(" , ");
paramCount++;
}
if ( paramCount > 0 ) {
//replace final comma space with close paren.
sb.replace(sb.length() - 2, sb.length() - 1, " )");//.append(" ");
} else {
//there were no parameters, so no commas.
sb.append(") ");
}
//System.out.println( "Function found. '" + sb + "'" );
return sb.toString();
} else {
//a field
Vector path = (Vector) _pathInfo.get(exprTree);
if ( tokenType == DOT ) {
if ( path == null ) {
System.err.println( "exprTree=" + exprTree.toStringEx() + "\npathInfo = {" );
Enumeration enumeration = _pathInfo.keys();
ParseTreeNode n;
while ( enumeration.hasMoreElements() ) {
n = (ParseTreeNode)enumeration.nextElement();
System.err.println( "\t" + n.toStringEx() );
}
// Exception follows in addJoinsForPathExpression()
}
addJoinsForPathExpression( path );
}
JDOFieldDescriptor field = (JDOFieldDescriptor) _fieldInfo.get(exprTree);
if ( field == null ) {
throw new IllegalStateException( "fieldInfo for " + exprTree.toStringEx() + " not found" );
}
JDOClassDescriptor clsDesc = (JDOClassDescriptor) field.getContainingClassDescriptor();
if ( clsDesc == null ) {
throw new IllegalStateException( "ContainingClass of "+ field.toString()+" is null !" );
}
String clsTableAlias;
if ( tokenType == DOT && path != null && path.size() > 2 ) {
clsTableAlias = buildTableAlias( clsDesc.getTableName(), path, path.size() - 2 );
JDOClassDescriptor srcDesc = _clsDesc;
for ( int i = 1; i < path.size(); i++ ) {
Object[] fieldAndClass = getFieldAndClassDesc((String) path.elementAt(i), srcDesc,
_queryExpr, path, i - 1);
if (fieldAndClass == null) {
throw new IllegalStateException( "Field not found: " + path.elementAt(i) + " class " + srcDesc.getJavaClass());
}
JDOFieldDescriptor fieldDesc = (JDOFieldDescriptor) fieldAndClass[0];
srcDesc = (JDOClassDescriptor) fieldDesc.getClassDescriptor();
}
} else {
clsTableAlias = buildTableAlias( clsDesc.getTableName(), path, 9999 );
}
return _queryExpr.encodeColumn(clsTableAlias, field.getSQLName()[0]);
}
//parameters
case DOLLAR:
//return a question mark with the parameter number. The calling function
//will do a mapping
return "?" + exprTree.getChild(exprTree.getChildCount() - 1)
.getToken().getTokenValue();
//literals which need no modification
case BOOLEAN_LITERAL: case LONG_LITERAL: case DOUBLE_LITERAL:
case CHAR_LITERAL:
return exprTree.getToken().getTokenValue();
//String literals: change \" to ""
case STRING_LITERAL:
//char replace function should really be somewhere else
//first change \" to "
sb = new StringBuffer();
String copy = new String(exprTree.getToken().getTokenValue());
int pos = copy.indexOf("\\\"", 1);
while ( pos != -1 ) {
sb.append(copy.substring(0, pos)).append("\"\"");
copy = copy.substring(pos + 2);
pos = copy.indexOf("\\\"");
}
sb.append(copy);
//Then change surrounding double quotes to single quotes, and change
//' to ''
copy = sb.deleteCharAt(0).toString();
sb.setLength(0);
sb.append("'");
pos = copy.indexOf("'", 1);
while ( pos != -1 ) {
sb.append(copy.substring(0, pos)).append("''");
copy = copy.substring(pos + 1);
pos = copy.indexOf("'");
}
sb.append(copy);
//replace final double quote with single quote
sb.replace(sb.length() - 1, sb.length(), "'");
return sb.toString();
//Date, time and timestamp literals...strip off keyword (?is that all?)
case DATE_LITERAL: case TIME_LITERAL:
//date and time both 4 chars long.
return exprTree.getToken().getTokenValue().substring(5);
case TIMESTAMP_LITERAL:
return exprTree.getToken().getTokenValue().substring(10);
case KEYWORD_NIL:
case KEYWORD_UNDEFINED:
return " NULL ";
case KEYWORD_LIMIT: //Proceed it with it's own getSQLExpr
case KEYWORD_OFFSET:
return getSQLExprForLimit(exprTree);
}
return "";
}
private String getSQLExprForLimit(ParseTreeNode limitClause) {
StringBuffer sb = new StringBuffer();
for (Enumeration e = limitClause.children(); e.hasMoreElements(); ) {
ParseTreeNode exprTree = (ParseTreeNode) e.nextElement();
int tokenType = exprTree.getToken().getTokenType();
switch( tokenType ) {
//parameters
case DOLLAR:
//return a question mark with the parameter number. The calling function
//will do a mapping
sb.append( "?" + exprTree.getChild(exprTree.getChildCount() - 1)
.getToken().getTokenValue());
break;
case COMMA:
sb.append(" , ");
break;
case BOOLEAN_LITERAL:
case LONG_LITERAL:
case DOUBLE_LITERAL:
case CHAR_LITERAL:
return exprTree.getToken().getTokenValue();
}
}
return sb.toString();
}
/**
* Returns a SQL version of an OQL order by clause.
*
* @param orderClause the parse tree node with the order by clause
* @return The SQL translation of the order by clause.
*/
private String getOrderClause(ParseTreeNode orderClause) {
StringBuffer sb = new StringBuffer();
for (Enumeration e = orderClause.children(); e.hasMoreElements(); ) {
sb.append(", ");
ParseTreeNode curChild = (ParseTreeNode) e.nextElement();
int tokenType = curChild.getToken().getTokenType();
switch ( tokenType ) {
case KEYWORD_ASC:
sb.append( getSQLExpr( curChild.getChild(0) ) ).append( " ASC " );
break;
case KEYWORD_DESC:
sb.append( getSQLExpr( curChild.getChild(0) ) ).append( " DESC " );
break;
case DOT: case IDENTIFIER:
sb.append( getSQLExpr( curChild ) ).append( " " );
break;
}
}
//remove the additional comma space at the beginning
sb.deleteCharAt(0).deleteCharAt(0);
return sb.toString();
}
}