Package org.apache.ojb.jdo.jdoql

Source Code of org.apache.ojb.jdo.jdoql.QueryTreeResolver

package org.apache.ojb.jdo.jdoql;

/* Copyright 2003-2005 The Apache Software Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.jdo.JDOUserException;

import org.apache.ojb.broker.metadata.*;
import org.apache.ojb.broker.util.ClassHelper;
import org.apache.ojb.jdo.QueryImpl;

/**
* Resolves and checks the query trees. This involves the following things:<br/>
* <ul>
* <li>Replace {@link NameExpression NameExpression} with {@link LocalVariableAccess VariableAccess},
*     {@link FieldAccess FieldAccess}, or {@link TypeAccess TypeAccess}</li>
* <li>Resolve types referenced by {@link Type Type} and {@link TypeAccess TypeAccess}</li>
* <li>Resolve features/variables referenced by {@link LocalVariableAccess VariableAccess},
*     {@link FieldAccess FieldAccess}, and {@link MethodInvocation MethodInvocation}</li>
* <li>Check that expression types are valid, e.g. that the types of the two sides of a binary
*     expression are compatible to each other and that this binary operator can be applied
*     to them</li>
* <li>Check that the accessed methods are allowed to be accessed as per JDO spec (or
*     extensions allowed by OJB)</li>
* <li>Check that for each accessed variable there is a variable or parameter defined</li>
* <li>Check that each accessed field is persistent</li>
* </ul>
*
* @author <a href="mailto:tomdz@apache.org">Thomas Dudziak</a>
*/
public class QueryTreeResolver extends VisitorBase
{
    // TODO: Integrate position info into the exceptions

    /** Defines the ordering of primitive types */
    private static HashMap _primitiveTypes = new HashMap();

    static
    {
        _primitiveTypes.put(byte.class,       new Integer(0));
        _primitiveTypes.put(char.class,       new Integer(1));
        _primitiveTypes.put(short.class,      new Integer(2));
        _primitiveTypes.put(int.class,        new Integer(3));
        _primitiveTypes.put(long.class,       new Integer(4));
        _primitiveTypes.put(BigInteger.class, new Integer(5));
        _primitiveTypes.put(float.class,      new Integer(6));
        _primitiveTypes.put(double.class,     new Integer(7));
        _primitiveTypes.put(BigDecimal.class, new Integer(8));
    }
   
    /** The currently resolved query */
    private QueryImpl _query;

    /**
     * Resolves and checks the given query. Results from a previous resolving are
     * overwritten.
     * 
     * @param query The query
     * @exception JDOUserException If the query is invalid
     */
    public void resolveAndCheck(QueryImpl query) throws JDOUserException
    {
        _query = query;
        try
        {
            checkImports(query.getImports());
            visit(query.getVariables());
            visit(query.getParameters());
            if (query.getFilterExpression() != null)
            {
                query.getFilterExpression().accept(this);
            }
            visit(query.getOrderings());
        }
        finally
        {
            _query = null;
        }
    }

    /**
     * Checks the imports. We're a bit more foregiving as the JDO Spec because
     * we don't demand that all imported types/packages exists as long as they
     * are not used.
     *
     * @param imports The imports to check
     * @throws JDOUserException If two direct imports imports types with the same
     *                          short name
     */
    private void checkImports(Collection imports) throws JDOUserException
    {
        // It is an error if two direct imports import classes with the same short name
        HashMap directlyImportedClasses = new HashMap();
        Import  importDecl;
        String  shortName;

        for (Iterator it = imports.iterator(); it.hasNext();)
        {
            importDecl = (Import)it.next();
            if (!importDecl.isOnDemand())
            {
                shortName = importDecl.getSpec().substring(importDecl.getSpec().lastIndexOf('.'));
                if (directlyImportedClasses.containsKey(shortName))
                {
                    throw new JDOUserException("Multiple direct imports of classes with the same unqualified name "+shortName);
                }
                else
                {
                    directlyImportedClasses.put(shortName, null);
                }
            }
        }
    }
   
    /**
     * Visits all elements of the given map.
     *
     * @param objects The objects
     * @exception JDOUserException If one of the elements is invalid
     */
    private void visit(Map objects) throws JDOUserException
    {
        if (objects != null)
        {
            for (Iterator it = objects.values().iterator(); it.hasNext();)
            {
                ((Acceptor)it.next()).accept(this);
            }
        }
    }
   
    /**
     * Visits all elements of the given collection.
     *
     * @param objects The objects
     * @exception JDOUserException If one of the elements is invalid
     */
    private void visit(Collection objects) throws JDOUserException
    {
        if (objects != null)
        {
            for (Iterator it = objects.iterator(); it.hasNext();)
            {
                ((Acceptor)it.next()).accept(this);
            }
        }
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.BinaryExpression)
     */
    public void visit(BinaryExpression binExpr) throws JDOUserException
    {
        super.visit(binExpr);

        // for the types of the left and right expressions one of the following must hold:
        // * the operator is '+' and both are String
        // * the operator is arithmethic and both are numeric (Character, Long or Double)
        // * the operator is bitwise and both are Character or Long
        // * the operator is logic and both are Boolean
        // * the operator is comparative and the both are String, or both are Date, or both are Numeric
        Class   leftType   = binExpr.getLeftSide().getType();
        Class   rightType  = binExpr.getRightSide().getType();
        boolean typeWasSet = false;

        switch (binExpr.getOperator())
        {
            case BinaryExpression.OPERATOR_MULTIPLY :
            case BinaryExpression.OPERATOR_DIVIDE :
            case BinaryExpression.OPERATOR_PLUS :
            case BinaryExpression.OPERATOR_MINUS :
                if (binExpr.getOperator() == BinaryExpression.OPERATOR_PLUS)
                {
                    if ((leftType == String.class) && (rightType == String.class))
                    {
                        binExpr.setType(String.class);
                        typeWasSet = true;
                        break;
                    }
                }
                if (isNumeric(leftType) && isNumeric(rightType))
                {
                    binExpr.setType(getBroaderType(leftType, rightType));
                    typeWasSet = true;
                }
                break;
            case BinaryExpression.OPERATOR_BITWISE_AND :
            case BinaryExpression.OPERATOR_BITWISE_XOR :
            case BinaryExpression.OPERATOR_BITWISE_OR :
                if (isInteger(leftType) && isInteger(rightType))
                {
                    binExpr.setType(getBroaderType(leftType, rightType));
                    typeWasSet = true;
                }
                else if ((leftType == boolean.class) && (rightType == boolean.class))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                break;
            case BinaryExpression.OPERATOR_LOWER :
            case BinaryExpression.OPERATOR_GREATER :
            case BinaryExpression.OPERATOR_LOWER_OR_EQUAL :
            case BinaryExpression.OPERATOR_GREATER_OR_EQUAL :
                if (isNumeric(leftType) && isNumeric(rightType))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                else if ((leftType == String.class) && (rightType == String.class))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                else if ((leftType == Date.class) && (rightType == Date.class))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                break;
            case BinaryExpression.OPERATOR_EQUAL :
            case BinaryExpression.OPERATOR_NOT_EQUAL :
                if (isNumeric(leftType) && isNumeric(rightType))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                else if ((leftType == boolean.class) && (rightType == boolean.class))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                else if (!leftType.isPrimitive() && !rightType.isPrimitive())
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                break;
            case BinaryExpression.OPERATOR_AND :
            case BinaryExpression.OPERATOR_OR :
                if ((leftType == boolean.class) && (rightType == boolean.class))
                {
                    binExpr.setType(boolean.class);
                    typeWasSet = true;
                }
                break;
        }
        if (!typeWasSet)
        {
            throw new JDOUserException("Binary expression cannot be applied to expressions of types "+leftType.getName()+" and "+rightType.getName());
        }
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.MethodInvocation)
     */
    public void visit(MethodInvocation methodInvoc) throws JDOUserException
    {
        super.visit(methodInvoc);

        Class  type = methodInvoc.getBaseExpression().getType();
        String name = methodInvoc.getName();

        if (Collection.class.isAssignableFrom(type))
        {
            if ("contains".equals(name))
            {
                if (methodInvoc.getArguments().size() != 1)
                {
                    throw new JDOUserException("Illegal number of arguments to method Collection.contains");
                }
            }
            else if ("isEmpty".equals(name))
            {
                if (!methodInvoc.getArguments().isEmpty())
                {
                    throw new JDOUserException("Illegal number of arguments to method Collection.isEmpty");
                }
            }
            else
            {
                throw new JDOUserException("Only methods 'contains' and 'isEmpty' are allowed to be called at collection objects");
            }
            methodInvoc.setType(boolean.class);
        }
        else if (type == String.class)
        {
            if (!"startsWith".equals(name) && !"endsWith".equals(name))
            {
                throw new JDOUserException("Only methods 'contains' and 'isEmpty' are allowed to be called at collection objects");
            }
            if (methodInvoc.getArguments().size() != 1)
            {
                throw new JDOUserException("Illegal number of arguments to method String."+name);
            }
            if (((Expression)methodInvoc.getArguments().get(0)).getType() != String.class)
            {
                throw new JDOUserException("Illegal argument to method Collection."+name);
            }
            methodInvoc.setType(boolean.class);
        }
        else
        {
            throw new JDOUserException("Invocation of method "+methodInvoc.getName()+" at type "+type.getName()+" is not allowed");
        }
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.NameExpression)
     */
    public void visit(NameExpression nameExpr) throws JDOUserException
    {
        super.visit(nameExpr);

        Expression newExpr  = null;
        Class      baseType = null;

        // we search in this order (which corresponds to Java's name resolution order):
        //   * variables (only if without base expression)
        //   * parameters (only if without base expression)
        //   * fields/references/collections (at searched type if without base expression)
        // types are currently not searched for as they are not allowed to appear directly
        // except in casts
        if (!nameExpr.hasBaseExpression())
        {
            // no base expressiom
            LocalVariable var = _query.getVariable(nameExpr.getName());

            if (var == null)
            {
                var = _query.getParameter(nameExpr.getName());
            }
            if (var != null)
            {
                LocalVariableAccess varAccess = new LocalVariableAccess(nameExpr.getName());

                varAccess.setAccessedVariable(var);
                newExpr = varAccess;
            }
            else
            {
                // neither a variable nor parameter, so we must search in the class
                // whose objects the query searches for
                baseType = _query.getSearchedClass();
            }
        }
        else
        {
            // we have a base expression which means that we follow a reference
            baseType = nameExpr.getBaseExpression().getType();
        }
        if (newExpr == null)
        {
            // so we determine the persistent type of the base expression
            ClassDescriptor classDesc = findClassDescriptorFor(baseType);
           
            if (classDesc == null)
            {
                throw new JDOUserException("Access to type "+baseType.getName()+" is not allowed because the type is not persistent");
            }

            FieldAccess fieldAccess = new FieldAccess(nameExpr.getBaseExpression(), nameExpr.getName());
           
            // it may be either a field, reference or collection descriptor -
            // this depends on whether the name expression is a base expression
            // to another name expression (not a method invocation or other expression)
            ObjectReferenceDescriptor refDesc = classDesc.getObjectReferenceDescriptorByName(nameExpr.getName());
           
            if (refDesc != null)
            {
                fieldAccess.setFieldDescriptor(refDesc);
            }
            else if (nameExpr.hasParent() && (nameExpr.getParent() instanceof NameExpression))
            {
                // if we are the base expression of another name expression, then it must be a reference
                throw new JDOUserException("Cannot find reference "+nameExpr.getName()+" in type "+baseType.getName()+" because it is not defined, not persistent or not a reference");
            }
            else
            {
                // it can be a field or collection
                CollectionDescriptor collDesc = classDesc.getCollectionDescriptorByName(nameExpr.getName());
                if (collDesc != null)
                {
                    fieldAccess.setFieldDescriptor(collDesc);
                }
                else
                {
                    FieldDescriptor fieldDesc = classDesc.getFieldDescriptorByName(nameExpr.getName());
   
                    if (fieldDesc == null)
                    {
                        throw new JDOUserException("Cannot find feature "+nameExpr.getName()+" in type "+baseType.getName()+" because it is not defined or not persistent");
                    }
                    fieldAccess.setFieldDescriptor(fieldDesc);
                }
            }
            newExpr = fieldAccess;
        }

        if (nameExpr.hasParent())
        {
            nameExpr.getParent().replaceChild(nameExpr, newExpr);
        }
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.NullLiteral)
     */
    public void visit(NullLiteral nullLit)
    {
        Expression parent = nullLit.getParent();

        if (parent == null)
        {
            return;
        }

        // the only interesting case is if the parent is a binary expression (e.g.
        // a '==' expression) in which case the null literal shall have the same
        // type as the other operand
        if (parent instanceof BinaryExpression)
        {
            Class type = null;

            if (((BinaryExpression)parent).getLeftSide() == nullLit)
            {
                type = ((BinaryExpression)parent).getRightSide().getType();
            }
            else
            {
                type = ((BinaryExpression)parent).getLeftSide().getType();
            }
            if (type.isPrimitive())
            {
                throw new JDOUserException("Illegal binary expression with a 'null' and a primitive operand");
            }
            nullLit.setType(type);
        }
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.ThisExpression)
     */
    public void visit(ThisExpression thisExpr)
    {
        super.visit(thisExpr);
        thisExpr.setType(_query.getSearchedClass());
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.Type)
     */
    public void visit(Type type) throws JDOUserException
    {
        ClassLoader loader = ClassHelper.getClassLoader();
        Class       result = null;
        String      name   = type.getName();
        int         pos    = name.indexOf('.');

        if (pos >= 0)
        {
            // its either a qualified name or refers to an inner class
            // we ignore inner classes here as they are handled by the import
            // resolution below
            // [tomdz] we might have to resolve inner/nested classes manually
            //         if the query should be executed in the scope of the
            //         searched class (which makes private/protected/friendly
            //         inner/nested classes visible)
            //         for now we assume that this is not the case
            try
            {
                result = Class.forName(name, true, loader);
            }
            catch (ClassNotFoundException ex)
            {
                // ignored
            }
        }
        if (result == null)
        {
            result = resolveUnqualifiedClassName(loader, name);
        }
        if (result == null)
        {
            throw new JDOUserException("No such class "+name);
        }
        else
        {
            type.setType(result);
        }
    }

    /**
     * Resolves the given unqualified class name against the imports of the query.
     *
     * @param loader          The class loader to use
     * @param unqualifiedName The unqualified class name
     * @return The class if it has been found
     * @exception JDOUserException If a direct import declaration is invalid
     */
    private Class resolveUnqualifiedClassName(ClassLoader loader, String unqualifiedName) throws JDOUserException
    {
        // A direct import has precedence over on-demand imports of packages that contain
        // a class of the same short name (JLS 7.5.1)
        // If multiple on-demand imports import packages that have a class with a given
        // short name, then the last such import defines the class to be used
        Import importDecl;
        Class  result = null;
        int    pos;

        // we first try to resolve it against java.lang
        try
        {
            result = Class.forName("java.lang." + unqualifiedName, true, loader);
        }
        catch (ClassNotFoundException ex)
        {
            // ignored
        }
        for (Iterator it = _query.getImports().iterator(); it.hasNext();)
        {
            importDecl = (Import)it.next();
            if (importDecl.isOnDemand())
            {
                try
                {
                    result = Class.forName(importDecl.getSpec() + "." + unqualifiedName, true, loader);
                }
                catch (ClassNotFoundException ex)
                {
                    // ignored
                }
            }
            else
            {
                pos = importDecl.getSpec().lastIndexOf('.');
                if (unqualifiedName.equals(importDecl.getSpec().substring(pos + 1)))
                {
                    try
                    {
                        // there can only be one direct import of a class with this
                        // unqualified name (imports have already been checked), and
                        // no on-demand import can shadow it, so we can simply return it
                        return Class.forName(importDecl.getSpec() + "." + unqualifiedName, true, loader);
                    }
                    catch (ClassNotFoundException ex)
                    {
                        // we have a direct import for the class but the import is invalid
                        throw new JDOUserException("The import "+importDecl.getSpec()+" is invalid");
                    }
                }
            }
        }
        return result;
    }

    /* (non-Javadoc)
     * @see org.apache.ojb.jdo.jdoql.Visitor#visit(org.apache.ojb.jdo.jdoql.UnaryExpression)
     */
    public void visit(UnaryExpression unaryExpr) throws JDOUserException
    {
        super.visit(unaryExpr);

        // one of the following must hold:
        // * the operator is arithmetic and the inner type is numeric
        // * the operator is bitwise and the inner type is Character or Long
        // * the operator is logic and the inner type is Boolean
        // * it is a cast and the cast type is assignment compatible to the inner type
        Class   innerType  = unaryExpr.getInnerExpression().getType();
        boolean typeWasSet = false;

        switch (unaryExpr.getOperator())
        {
            case UnaryExpression.OPERATOR_PLUS :
            case UnaryExpression.OPERATOR_MINUS :
                if (isNumeric(innerType))
                {
                    unaryExpr.setType(innerType);
                    typeWasSet = true;
                }
                break;
            case UnaryExpression.OPERATOR_BITWISE_COMPLEMENT :
                if (isInteger(innerType))
                {
                    unaryExpr.setType(innerType);
                    typeWasSet = true;
                }
                break;
            case UnaryExpression.OPERATOR_NOT :
                if (innerType == boolean.class)
                {
                    unaryExpr.setType(innerType);
                    typeWasSet = true;
                }
                break;
            case UnaryExpression.OPERATOR_CAST :
                Class castType = unaryExpr.getCastType().getType();

                if (isNumeric(castType) && isNumeric(innerType))
                {
                    unaryExpr.setType(castType);
                    typeWasSet = true;
                }
                else
                {
                    // check for narrowing or widening reference conversion
                    if (castType.isAssignableFrom(innerType) || innerType.isAssignableFrom(castType))
                    {
                        unaryExpr.setType(castType);
                        typeWasSet = true;
                    }
                }
                break;

        }
        if (!typeWasSet)
        {
            if (unaryExpr.getOperator() == UnaryExpression.OPERATOR_CAST)
            {
                throw new JDOUserException("Invalid cast expression because inner expression of type "+innerType.getName()+" cannot be cast to "+unaryExpr.getCastType().getName());
            }
            else
            {
                throw new JDOUserException("Invalid unary expression");
            }
        }
    }

    // Helper methods

    /**
     * Retrieves OJB's class descriptor for the given type.
     *
     * @param The type to lookup
     * @return The class descriptor or <code>null</code> if the class is not persistent
     */
    private ClassDescriptor findClassDescriptorFor(Class type)
    {
        return MetadataManager.getInstance().getRepository().getDescriptorFor(type);
    }

    /**
     * Determines whether the given class denotes an integer primitive type.
     *
     * @param type The type
     * @return <code>true</code> if the type is an integer type
     */
    private static boolean isInteger(Class type)
    {
        if (type.isPrimitive())
        {
            return (type != boolean.class) && (type != float.class) && (type != double.class);
        }
        else
        {
            return type == BigInteger.class;
        }
    }

    /**
     * Determines whether the given class denotes a floating point primitive type.
     *
     * @param type The type
     * @return <code>true</code> if the type is a floating point type
     */
    private static boolean isFloatingPoint(Class type)
    {
        if (type.isPrimitive())
        {
            return (type == float.class) || (type == double.class);
        }
        else
        {
            return type == BigDecimal.class;
        }
    }

    /**
     * Determines whether the given class denotes a numeric primitive type.
     *
     * @param type The type
     * @return <code>true</code> if the type is a numeric type
     */
    private static boolean isNumeric(Class type)
    {
        if (type.isPrimitive())
        {
            return type != boolean.class;
        }
        else
        {
            return (type == BigDecimal.class) || (type == BigInteger.class);
        }
    }

    /**
     * Determines the broader of the two given numeric types.
     *
     * @param typeA The first type
     * @param typeB The seconf type
     * @return The broader of the two types
     */
    private static Class getBroaderType(Class typeA, Class typeB)
    {
        Integer numA = (Integer)_primitiveTypes.get(typeA);
        Integer numB = (Integer)_primitiveTypes.get(typeB);

        return numA.intValue() < numB.intValue() ? typeB : typeA;
    }
}
TOP

Related Classes of org.apache.ojb.jdo.jdoql.QueryTreeResolver

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.