org.apache.cayenne.access.translator.select.QualifierTranslator.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.cayenne.access.translator.select.QualifierTranslator.java

Source

/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.
 ****************************************************************/

package org.apache.cayenne.access.translator.select;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.dba.TypesMapping;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.TraversalHandler;
import org.apache.cayenne.exp.parser.ASTDbPath;
import org.apache.cayenne.exp.parser.ASTExtract;
import org.apache.cayenne.exp.parser.ASTFunctionCall;
import org.apache.cayenne.exp.parser.ASTObjPath;
import org.apache.cayenne.exp.parser.PatternMatchNode;
import org.apache.cayenne.exp.parser.SimpleNode;
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.JoinType;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.query.Query;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.reflect.ClassDescriptor;
import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.collections.Transformer;

/**
 * Translates query qualifier to SQL. Used as a helper class by query
 * translators.
 */
public class QualifierTranslator extends QueryAssemblerHelper implements TraversalHandler {

    protected DataObjectMatchTranslator objectMatchTranslator;
    protected boolean matchingObject;
    protected boolean caseInsensitive;

    /**
     * @since 4.0
     */
    protected boolean useAliasForExpressions;

    /**
     * @since 4.0
     */
    protected Expression waitingForEndNode;

    /**
     * @since 4.0
     */
    protected Expression qualifier;

    public QualifierTranslator(QueryAssembler queryAssembler) {
        super(queryAssembler);

        caseInsensitive = false;
    }

    /**
     * Translates query qualifier to SQL WHERE clause. Qualifier is obtained
     * from the parent queryAssembler.
     * 
     * @since 3.0
     */
    @Override
    protected void doAppendPart() {
        doAppendPart(extractQualifier());
    }

    public void setCaseInsensitive(boolean caseInsensitive) {
        this.caseInsensitive = caseInsensitive;
    }

    /**
     * Explicitly set qualifier.
     * It will be used instead of extracting qualifier from the query itself.
     * @since 4.0
     */
    public void setQualifier(Expression qualifier) {
        this.qualifier = qualifier;
    }

    /**
     * @since 4.0
     */
    public void setUseAliasForExpressions(boolean useAliasForExpressions) {
        this.useAliasForExpressions = useAliasForExpressions;
    }

    /**
     * Translates query qualifier to SQL WHERE clause. Qualifier is a method
     * parameter.
     * 
     * @since 3.0
     */
    protected void doAppendPart(Expression rootNode) {
        if (rootNode == null) {
            return;
        }
        rootNode.traverse(this);
    }

    protected Expression extractQualifier() {
        // if additional qualifier is set, use it
        if (this.qualifier != null) {
            return this.qualifier;
        }

        Query q = queryAssembler.getQuery();

        Expression qualifier = ((SelectQuery<?>) q).getQualifier();

        // append Entity qualifiers, taking inheritance into account
        ObjEntity entity = getObjEntity();

        if (entity != null) {

            ClassDescriptor descriptor = queryAssembler.getEntityResolver().getClassDescriptor(entity.getName());
            Expression entityQualifier = descriptor.getEntityInheritanceTree().qualifierForEntityAndSubclasses();
            if (entityQualifier != null) {
                qualifier = (qualifier != null) ? qualifier.andExp(entityQualifier) : entityQualifier;
            }
        }

        // Attaching root Db entity's qualifier
        if (getDbEntity() != null) {
            Expression dbQualifier = getDbEntity().getQualifier();
            if (dbQualifier != null) {
                dbQualifier = dbQualifier.transform(new DbEntityQualifierTransformer());

                qualifier = qualifier == null ? dbQualifier : qualifier.andExp(dbQualifier);
            }
        }

        return qualifier;
    }

    /**
     * Called before processing an expression to initialize
     * objectMatchTranslator if needed.
     */
    protected void detectObjectMatch(Expression exp) {
        // On demand initialization of
        // objectMatchTranslator is not possible since there may be null
        // object values that would not allow to detect the need for
        // such translator in the right time (e.g.: null = dbpath)

        matchingObject = false;

        if (exp.getOperandCount() != 2) {
            // only binary expressions are supported
            return;
        }

        // check if there are DataObjects among direct children of the
        // Expression
        for (int i = 0; i < 2; i++) {
            Object op = exp.getOperand(i);
            if (op instanceof Persistent || op instanceof ObjectId) {
                matchingObject = true;

                if (objectMatchTranslator == null) {
                    objectMatchTranslator = new DataObjectMatchTranslator();
                } else {
                    objectMatchTranslator.reset();
                }
                break;
            }
        }
    }

    protected void appendObjectMatch() throws IOException {
        if (!matchingObject || objectMatchTranslator == null) {
            throw new IllegalStateException("An invalid attempt to append object match.");
        }

        // turn off special handling, so that all the methods behave as a
        // superclass's
        // impl.
        matchingObject = false;

        boolean first = true;

        DbRelationship relationship = objectMatchTranslator.getRelationship();
        if (!relationship.isToMany() && !relationship.isToPK()) {
            queryAssembler.dbRelationshipAdded(relationship, JoinType.INNER,
                    objectMatchTranslator.getJoinSplitAlias());
        }

        Iterator<String> it = objectMatchTranslator.keys();
        while (it.hasNext()) {
            if (first) {
                first = false;
            } else {
                out.append(" AND ");
            }

            String key = it.next();
            DbAttribute attr = objectMatchTranslator.getAttribute(key);
            Object val = objectMatchTranslator.getValue(key);

            processColumn(attr);
            out.append(objectMatchTranslator.getOperation());
            appendLiteral(val, attr, objectMatchTranslator.getExpression());
        }

        objectMatchTranslator.reset();
    }

    @Override
    public void finishedChild(Expression node, int childIndex, boolean hasMoreChildren) {

        if (waitingForEndNode != null) {
            return;
        }

        if (!hasMoreChildren) {
            return;
        }

        Appendable out = (matchingObject) ? new StringBuilder() : this.out;

        try {
            switch (node.getType()) {
            case Expression.AND:
                out.append(" AND ");
                break;
            case Expression.OR:
                out.append(" OR ");
                break;
            case Expression.EQUAL_TO:
                // translate NULL as IS NULL
                if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
                    out.append(" IS ");
                } else {
                    out.append(" = ");
                }
                break;
            case Expression.NOT_EQUAL_TO:
                // translate NULL as IS NOT NULL
                if (childIndex == 0 && node.getOperandCount() == 2 && node.getOperand(1) == null) {
                    out.append(" IS NOT ");
                } else {
                    out.append(" <> ");
                }
                break;
            case Expression.LESS_THAN:
                out.append(" < ");
                break;
            case Expression.GREATER_THAN:
                out.append(" > ");
                break;
            case Expression.LESS_THAN_EQUAL_TO:
                out.append(" <= ");
                break;
            case Expression.GREATER_THAN_EQUAL_TO:
                out.append(" >= ");
                break;
            case Expression.IN:
                out.append(" IN ");
                break;
            case Expression.NOT_IN:
                out.append(" NOT IN ");
                break;
            case Expression.LIKE:
                out.append(" LIKE ");
                break;
            case Expression.NOT_LIKE:
                out.append(" NOT LIKE ");
                break;
            case Expression.LIKE_IGNORE_CASE:
                if (caseInsensitive) {
                    out.append(" LIKE ");
                } else {
                    out.append(") LIKE UPPER(");
                }
                break;
            case Expression.NOT_LIKE_IGNORE_CASE:
                if (caseInsensitive) {
                    out.append(" NOT LIKE ");
                } else {
                    out.append(") NOT LIKE UPPER(");
                }
                break;
            case Expression.ADD:
                out.append(" + ");
                break;
            case Expression.SUBTRACT:
                out.append(" - ");
                break;
            case Expression.MULTIPLY:
                out.append(" * ");
                break;
            case Expression.DIVIDE:
                out.append(" / ");
                break;
            case Expression.BETWEEN:
                if (childIndex == 0)
                    out.append(" BETWEEN ");
                else if (childIndex == 1)
                    out.append(" AND ");
                break;
            case Expression.NOT_BETWEEN:
                if (childIndex == 0)
                    out.append(" NOT BETWEEN ");
                else if (childIndex == 1)
                    out.append(" AND ");
                break;
            case Expression.BITWISE_OR:
                out.append(" ").append(operandForBitwiseOr()).append(" ");
                break;
            case Expression.BITWISE_AND:
                out.append(" ").append(operandForBitwiseAnd()).append(" ");
                break;
            case Expression.BITWISE_XOR:
                out.append(" ").append(operandForBitwiseXor()).append(" ");
                break;
            case Expression.BITWISE_LEFT_SHIFT:
                out.append(" ").append(operandForBitwiseLeftShift()).append(" ");
                break;
            case Expression.BITWISE_RIGHT_SHIFT:
                out.append(" ").append(operandForBitwiseRightShift()).append("");
                break;
            }
        } catch (IOException ioex) {
            throw new CayenneRuntimeException("Error appending content", ioex);
        }

        if (matchingObject) {
            objectMatchTranslator.setOperation(out.toString());
            objectMatchTranslator.setExpression(node);
        }
    }

    /**
     * @since 3.1
     */
    protected String operandForBitwiseNot() {
        return "~";
    }

    /**
     * @since 3.1
     */
    protected String operandForBitwiseOr() {
        return "|";
    }

    /**
     * @since 3.1
     */
    protected String operandForBitwiseAnd() {
        return "&";
    }

    /**
     * @since 3.1
     */
    protected String operandForBitwiseXor() {
        return "^";
    }

    /**
     * @since 4.0
     */
    protected String operandForBitwiseLeftShift() {
        return "<<";
    }

    /**
     * @since 4.0
     */
    protected String operandForBitwiseRightShift() {
        return ">>";
    }

    @Override
    public void startNode(Expression node, Expression parentNode) {

        if (waitingForEndNode != null) {
            return;
        }

        if (useAliasForExpressions) {
            String alias = queryAssembler.getAliasForExpression(node);
            if (alias != null) {
                out.append(alias);
                waitingForEndNode = node;
                return;
            }
        }

        boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);

        if (node.getType() == Expression.FUNCTION_CALL) {
            if (node instanceof ASTExtract) {
                appendExtractFunction((ASTExtract) node);
            } else {
                appendFunction((ASTFunctionCall) node);
            }
            if (parenthesisNeeded) {
                out.append("(");
            }
            return;
        }

        if (node.getType() == Expression.FULL_OBJECT && parentNode != null) {
            throw new CayenneRuntimeException("Expression is not supported in where clause.");
        }

        int count = node.getOperandCount();

        if (count == 2) {
            // binary nodes are the only ones that currently require this
            detectObjectMatch(node);
        }

        if (parenthesisNeeded) {
            out.append('(');
        }

        if (count == 0) {
            // not all databases handle true/false
            if (node.getType() == Expression.TRUE) {
                out.append("1 = 1");
            } else if (node.getType() == Expression.FALSE) {
                out.append("1 = 0");
            } else if (node.getType() == Expression.ASTERISK) {
                out.append("*");
            }
        }

        if (count == 1) {
            if (node.getType() == Expression.NEGATIVE) {
                out.append('-');
            } else if (node.getType() == Expression.NOT) {
                out.append("NOT ");
            } else if (node.getType() == Expression.BITWISE_NOT) {
                out.append(operandForBitwiseNot());
            }
        } else if ((node.getType() == Expression.LIKE_IGNORE_CASE
                || node.getType() == Expression.NOT_LIKE_IGNORE_CASE) && !caseInsensitive) {
            out.append("UPPER(");
        }

    }

    /**
     * @since 1.1
     */
    @Override
    public void endNode(Expression node, Expression parentNode) {

        if (waitingForEndNode != null) {
            if (node == waitingForEndNode) {
                waitingForEndNode = null;
            }
            return;
        }

        try {
            // check if we need to use objectMatchTranslator to finish building the expression
            if (node.getOperandCount() == 2 && matchingObject) {
                appendObjectMatch();
            }

            boolean parenthesisNeeded = parenthesisNeeded(node, parentNode);
            boolean likeIgnoreCase = (node.getType() == Expression.LIKE_IGNORE_CASE
                    || node.getType() == Expression.NOT_LIKE_IGNORE_CASE);
            boolean isPatternMatchNode = PatternMatchNode.class.isAssignableFrom(node.getClass());

            // closing UPPER parenthesis
            if (likeIgnoreCase && !caseInsensitive) {
                out.append(')');
            }

            if (isPatternMatchNode) {
                appendLikeEscapeCharacter((PatternMatchNode) node);
            }

            // clean up trailing comma in function argument list
            if (node.getType() == Expression.FUNCTION_CALL) {
                clearLastFunctionArgDivider((ASTFunctionCall) node);
            }

            // closing LIKE parenthesis
            if (parenthesisNeeded) {
                out.append(')');
            }

            // if inside function call, put comma between arguments
            if (parentNode != null && parentNode.getType() == Expression.FUNCTION_CALL) {
                appendFunctionArgDivider((ASTFunctionCall) parentNode);
            }
        } catch (IOException ioex) {
            throw new CayenneRuntimeException("Error appending content", ioex);
        }
    }

    @Override
    public void objectNode(Object leaf, Expression parentNode) {
        if (waitingForEndNode != null) {
            return;
        }

        try {
            switch (parentNode.getType()) {
            case Expression.OBJ_PATH:
                appendObjPath(parentNode);
                break;
            case Expression.DB_PATH:
                appendDbPath(parentNode);
                break;
            case Expression.LIST:
                appendList(parentNode, paramsDbType(parentNode));
                break;
            case Expression.FUNCTION_CALL:
                appendFunctionArg(leaf, (ASTFunctionCall) parentNode);
                break;
            default:
                appendLiteral(leaf, paramsDbType(parentNode), parentNode);
            }
        } catch (IOException ioex) {
            throw new CayenneRuntimeException("Error appending content", ioex);
        }
    }

    protected boolean parenthesisNeeded(Expression node, Expression parentNode) {
        if (node.getType() == Expression.FUNCTION_CALL) {
            return ((ASTFunctionCall) node).needParenthesis();
        }

        if (parentNode == null) {
            return false;
        }

        // only unary expressions can go w/o parenthesis
        if (node.getOperandCount() > 1) {
            return true;
        }

        if (node.getType() == Expression.OBJ_PATH || node.getType() == Expression.DB_PATH
                || node.getType() == Expression.ASTERISK) {
            return false;
        }

        return true;
    }

    private final void appendList(Expression listExpr, DbAttribute paramDesc) throws IOException {
        Iterator<?> it;
        Object list = listExpr.getOperand(0);
        if (list instanceof List) {
            it = ((List<?>) list).iterator();
        } else if (list instanceof Object[]) {
            it = IteratorUtils.arrayIterator((Object[]) list);
        } else {
            String className = (list != null) ? list.getClass().getName() : "<null>";
            throw new IllegalArgumentException("Unsupported type for the list expressions: " + className);
        }

        // process first element outside the loop
        // (unroll loop to avoid condition checking
        if (it.hasNext()) {
            appendLiteral(it.next(), paramDesc, listExpr);
        } else {
            return;
        }

        while (it.hasNext()) {
            out.append(", ");
            appendLiteral(it.next(), paramDesc, listExpr);
        }
    }

    @Override
    protected void appendLiteral(Object val, DbAttribute attr, Expression parentExpression) throws IOException {

        if (!matchingObject) {
            super.appendLiteral(val, attr, parentExpression);
        } else if (val == null || (val instanceof Persistent)) {
            objectMatchTranslator.setDataObject((Persistent) val);
        } else if (val instanceof ObjectId) {
            objectMatchTranslator.setObjectId((ObjectId) val);
        } else {
            throw new IllegalArgumentException("Attempt to use literal other than DataObject during object match.");
        }
    }

    @Override
    protected void processRelTermination(DbRelationship rel, JoinType joinType, String joinSplitAlias) {

        if (!matchingObject) {
            super.processRelTermination(rel, joinType, joinSplitAlias);
        } else {
            if (rel.isToMany()) {
                // append joins
                queryAssembler.dbRelationshipAdded(rel, joinType, joinSplitAlias);
            }
            objectMatchTranslator.setRelationship(rel, joinSplitAlias);
        }
    }

    /**
     * Append function name to result SQL
     * Override this method to rename or skip function if generic name isn't supported on target DB.
     * @since 4.0
     */
    protected void appendFunction(ASTFunctionCall functionExpression) {
        out.append(functionExpression.getFunctionName());
    }

    /**
     * Special case for extract date/time parts functions as they have many variants
     * @since 4.0
     */
    protected void appendExtractFunction(ASTExtract functionExpression) {
        appendFunction(functionExpression);
    }

    /**
     * Append scalar argument of a function call
     * Used only for values stored in ASTScalar other
     * expressions appended in objectNode() method
     *
     * @since 4.0
     */
    protected void appendFunctionArg(Object value, ASTFunctionCall functionExpression) throws IOException {
        // Create fake DbAttribute to pass argument info down to bind it to SQL prepared statement
        DbAttribute dbAttrForArg = new DbAttribute();
        dbAttrForArg.setType(TypesMapping.getSqlTypeByJava(value.getClass()));
        super.appendLiteral(value, dbAttrForArg, functionExpression);
        appendFunctionArgDivider(functionExpression);
    }

    /**
     * Append divider between function arguments.
     * In overriding methods can be replaced e.g. for " || " for CONCAT operation
     * @since 4.0
     */
    protected void appendFunctionArgDivider(ASTFunctionCall functionExpression) {
        out.append(", ");
    }

    /**
     * Clear last divider as we currently don't now position of argument until parent element is ended.
     * @since 4.0
     */
    protected void clearLastFunctionArgDivider(ASTFunctionCall functionExpression) {
        if (functionExpression.getOperandCount() > 0) {
            out.delete(out.length() - 2, out.length());
        }
    }

    /**
     * Class to translate DB Entity qualifiers annotation to Obj-entity
     * qualifiers annotation This is done by changing all Obj-paths to Db-paths
     * and rejecting all original Db-paths
     */
    class DbEntityQualifierTransformer implements Transformer {

        public Object transform(Object input) {
            if (input instanceof ASTObjPath) {
                return new ASTDbPath(((SimpleNode) input).getOperand(0));
            }
            return input;
        }
    }
}