All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.babyfish.hibernate.internal.QueryTemplateImpl Maven / Gradle / Ivy

The newest version!
/*
 * BabyFish, Object Model Framework for Java and JPA.
 * https://github.com/babyfish-ct/babyfish
 *
 * Copyright (c) 2008-2015, Tao Chen
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * Please visit "http://opensource.org/licenses/LGPL-3.0" to know more.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
 * for more details.
 */
package org.babyfish.hibernate.internal;

import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.persistence.Tuple;
import javax.persistence.TupleElement;
import javax.persistence.criteria.CompoundSelection;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Selection;
import javax.persistence.criteria.Subquery;

import org.babyfish.collection.ArrayList;
import org.babyfish.collection.LinkedHashMap;
import org.babyfish.collection.MACollections;
import org.babyfish.collection.XOrderedMap;
import org.babyfish.hibernate.ejb.HibernateXEntityManagerFactory;
import org.babyfish.lang.Arguments;
import org.babyfish.lang.Nulls;
import org.babyfish.persistence.XTypedQuery;
import org.babyfish.persistence.criteria.Assignment;
import org.babyfish.persistence.criteria.XAbstractQuery;
import org.babyfish.persistence.criteria.XCommonAbstractCriteria;
import org.babyfish.persistence.criteria.XCriteriaDelete;
import org.babyfish.persistence.criteria.XCriteriaQuery;
import org.babyfish.persistence.criteria.XCriteriaUpdate;
import org.babyfish.persistence.criteria.XSubquery;
import org.babyfish.persistence.criteria.expression.Aggregation;
import org.babyfish.persistence.criteria.expression.AsExpression;
import org.babyfish.persistence.criteria.expression.BetweenPredicate;
import org.babyfish.persistence.criteria.expression.BinaryArithmeticExpression;
import org.babyfish.persistence.criteria.expression.CoalesceExpression;
import org.babyfish.persistence.criteria.expression.ComparisonPredicate;
import org.babyfish.persistence.criteria.expression.CompoundPredicate;
import org.babyfish.persistence.criteria.expression.ConcatExpression;
import org.babyfish.persistence.criteria.expression.ConstantExpression;
import org.babyfish.persistence.criteria.expression.ConvertExpression;
import org.babyfish.persistence.criteria.expression.ExistsPredicate;
import org.babyfish.persistence.criteria.expression.InPredicate;
import org.babyfish.persistence.criteria.expression.InPredicate.Partition;
import org.babyfish.persistence.criteria.expression.IsEmptyPredicate;
import org.babyfish.persistence.criteria.expression.IsMemberPredicate;
import org.babyfish.persistence.criteria.expression.IsTruePredicate;
import org.babyfish.persistence.criteria.expression.LikePredicate;
import org.babyfish.persistence.criteria.expression.ListIndexExpression;
import org.babyfish.persistence.criteria.expression.LiteralExpression;
import org.babyfish.persistence.criteria.expression.MapEntryExpression;
import org.babyfish.persistence.criteria.expression.NullLiteralExpression;
import org.babyfish.persistence.criteria.expression.NullifExpression;
import org.babyfish.persistence.criteria.expression.NullnessPredicate;
import org.babyfish.persistence.criteria.expression.PathTypeExpression;
import org.babyfish.persistence.criteria.expression.PriorityConstants;
import org.babyfish.persistence.criteria.expression.SearchedCaseExpression;
import org.babyfish.persistence.criteria.expression.SimpleCaseExpression;
import org.babyfish.persistence.criteria.expression.SizeExpression;
import org.babyfish.persistence.criteria.expression.SubqueryComparisonModifierExpression;
import org.babyfish.persistence.criteria.expression.TrimExpression;
import org.babyfish.persistence.criteria.expression.UnaryArithmeticExpression;
import org.babyfish.persistence.criteria.spi.AbstractExpression;
import org.babyfish.persistence.criteria.spi.AbstractFunction;
import org.babyfish.persistence.criteria.spi.AbstractNode;
import org.babyfish.persistence.criteria.spi.AbstractQueryTemplate;
import org.babyfish.persistence.criteria.spi.MapKeyPath;
import org.babyfish.persistence.criteria.spi.PathId;
import org.babyfish.persistence.criteria.spi.QueryContext;
import org.babyfish.persistence.criteria.spi.QueryContext.Entity;
import org.babyfish.persistence.criteria.spi.QueryContext.PathNode;
import org.babyfish.persistence.criteria.spi.Visitor;
import org.babyfish.util.GraphTravelAction;
import org.babyfish.util.GraphTravelContext;
import org.babyfish.util.GraphTraveler;
import org.babyfish.util.LazyResource;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.transform.ResultTransformer;
import org.hibernate.type.Type;

/**
 * @author Tao Chen
 */
public class QueryTemplateImpl extends AbstractQueryTemplate {
    
    public QueryTemplateImpl(XCommonAbstractCriteria commonAbstractCriteria) {
        super(commonAbstractCriteria);
    }
    
    @Override
    protected void init(XCommonAbstractCriteria commonAbstractCriteria) {
        try (QueryContext queryContext = new QueryContext(commonAbstractCriteria)) {
            VisitorImpl visitorImpl = new VisitorImpl(queryContext);
            visitorImpl.visit(commonAbstractCriteria);
            if (!visitorImpl.pathIdAllocator.isEmpty()) {
                throw new AssertionError();
            }
            this.init(visitorImpl.toJPQL(), visitorImpl.getLiteralParameters());
        }
    }
    
    @Override
    protected void setTupleTransfromer(XTypedQuery query, CompoundSelection tupleSelection) {
        org.hibernate.Query hqlQuery = query.unwrap(org.hibernate.Query.class);
        hqlQuery.setResultTransformer(new TupleResultTransformer(tupleSelection));
    }

    protected static class VisitorImpl implements Visitor {
        
        protected QueryContext queryContext;
        
        protected StringBuilder jpqlBuilder = new StringBuilder(256);
        
        protected XOrderedMap literalParameters;
        
        protected int currentPriority;
        
        protected PathId.Allocator pathIdAllocator;
        
        public VisitorImpl(QueryContext queryContext) {
            this.queryContext = queryContext;
            this.pathIdAllocator = queryContext.secondaryPathIdAllocator();
        }
        
        public String toJPQL() {
            return this.jpqlBuilder.toString();
        }
        
        public Collection getLiteralParameters() {
            if (this.literalParameters == null) {
                return MACollections.emptySet();
            }
            return MACollections.unmodifiable(this.literalParameters.values());
        }
    
        @Override
        public void visitCriteriaQuery(XCriteriaQuery query) {
            /*
             * It is very important to keep the visit order as:
             * selection, restriction, grouping, groupRestriction, orders
             * 
             * QueryContext must visit the tree by the same order
             * 
             * because the PathId.getAppearancePosition() is very important.
             */
            this.visitAbstractQuery(query);
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            
            this.pathIdAllocator.push(PathId.Allocator.ORDER_LIST);
            try {
                List orderList = query.getOrderList();
                if (!orderList.isEmpty()) {
                    jpqlBuilder.append(" order by ");
                    boolean addComma = false;
                    for (Order order : orderList) {
                        if (addComma) {
                            jpqlBuilder.append(", ");
                        } else {
                            addComma = true;
                        }
                        this.visit(order);
                    }
                }
            } finally {
                this.pathIdAllocator.pop();
            }
        }
    
        @Override
        public void visitSubquery(XSubquery query) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder.append('(');
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                this.visitAbstractQuery(query);
            } finally {
                this.currentPriority = oldPriority;
            }
            jpqlBuilder.append(')');
        }
    
        @Override
        public void visitCriteriaUpdate(XCriteriaUpdate update) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            
            jpqlBuilder.append("update ");
            this.pathIdAllocator.push(PathId.Allocator.ON_TREE);
            try {
                Entity entity = this.queryContext.getRootEntities(update).get(0);
                jpqlBuilder
                .append(entity.getManagedType().getJavaType().getName())
                .append(' ')
                .append(entity.getRenderAlias());
                this.renderJoin(entity);
            } finally {
                this.pathIdAllocator.pop();
            }
            
            jpqlBuilder.append(" set ");
            this.pathIdAllocator.push(PathId.Allocator.ASSIGNMENT_LIST);
            try {
                boolean addComma = false;
                for (Assignment assignment : update.getAssignments()) {
                    if (addComma) {
                        jpqlBuilder.append(", ");
                    } else {
                        addComma = true;
                    }
                    this.visit(assignment);
                }
            } finally {
                this.pathIdAllocator.pop();
            }
            
            this.pathIdAllocator.push(PathId.Allocator.RESTRICTION);
            try {
                Predicate restriction = update.getRestriction();
                if (restriction != null) {
                    jpqlBuilder.append(" where ");
                    this.visit(restriction);
                }
            } finally {
                this.pathIdAllocator.pop();
            }
        }

        @Override
        public void visitCriteriaDelete(XCriteriaDelete delete) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            
            jpqlBuilder.append("delete from ");
            this.pathIdAllocator.push(PathId.Allocator.ON_TREE);
            try {
                Entity entity = this.queryContext.getRootEntities(delete).get(0);
                jpqlBuilder
                .append(entity.getManagedType().getJavaType().getName())
                .append(' ')
                .append(entity.getRenderAlias());
                this.renderJoin(entity);
            } finally {
                this.pathIdAllocator.pop();
            }
            
            this.pathIdAllocator.push(PathId.Allocator.RESTRICTION);
            try {
                Predicate restriction = delete.getRestriction();
                if (restriction != null) {
                    jpqlBuilder.append(" where ");
                    this.visit(restriction);
                }
            } finally {
                this.pathIdAllocator.pop();
            }
        }

        @Override
        public void visitOrder(Order order) {
            this.visit(order.getExpression());
            this.jpqlBuilder.append(order.isAscending() ? " asc" : " desc");
        }
    
        @Override
        public void visitAssignment(Assignment assignment) {
            this.visit(assignment.getPath());
            this.jpqlBuilder.append(" = ");
            this.visit(assignment.getExpression());
        }

        @Override
        public void visitPath(Path path) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (path instanceof MapKeyPath) {
                jpqlBuilder.append("key(");
                this.visit(path.getParentPath());
                jpqlBuilder.append(')');
            } else {
                PathId pathId = this.pathIdAllocator.allocate();
                if (pathId.getPath() != path) {
                    throw new AssertionError();
                }
                PathNode pathNode = this.queryContext.getPathNodes().get(pathId);
                if (pathNode == null) {
                    throw new AssertionError();
                }
                this.renderPathNode(pathNode);
            }
        }
    
        @Override
        public void visitCompoundSelection(CompoundSelection compoundSelection) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            boolean constructor = 
                    !compoundSelection.getJavaType().isArray() &&
                    !Tuple.class.isAssignableFrom(compoundSelection.getJavaType());
            if (constructor) {
                jpqlBuilder
                .append("new ")
                .append(compoundSelection.getJavaType().getName())
                .append('(');
            }
            boolean addComma = false;
            for (Selection selection : compoundSelection.getCompoundSelectionItems()) {
                if (addComma) {
                    jpqlBuilder.append(", ");
                } else {
                    addComma = true;
                }
                this.visit(selection);
            }
            if (constructor) {
                jpqlBuilder.append(')');
            }
        }
    
        @Override
        public void visitFunction(AbstractFunction function) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            boolean addComma = false;
            jpqlBuilder
            .append(function.getFunctionName())
            .append('(');
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                for (Expression expr : function.getArguments()) {
                    if (addComma) {
                        jpqlBuilder.append(", ");
                    } else {
                        addComma = true;
                    }
                    this.visit(expr);
                }
            } finally {
                this.currentPriority = oldPriority;
            }
            jpqlBuilder.append(')');
        }
    
        @Override
        public void visitAggregation(Aggregation aggregation) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (aggregation instanceof Aggregation.Count) {
                jpqlBuilder.append("count(");
                if (((Aggregation.Count)aggregation).isDistinct()) {
                    jpqlBuilder.append("distinct ");
                }
            } else if (aggregation instanceof Aggregation.Sum) {
                jpqlBuilder.append("sum(");
            } else if (aggregation instanceof Aggregation.SumAsLong) {
                jpqlBuilder.append("sum(");
            } else if (aggregation instanceof Aggregation.SumAsDouble) {
                jpqlBuilder.append("sum(");
            } else if (aggregation instanceof Aggregation.Least) {
                jpqlBuilder.append("min(");
            } else if (aggregation instanceof Aggregation.Greatest) {
                jpqlBuilder.append("max(");
            } else if (aggregation instanceof Aggregation.Min) {
                jpqlBuilder.append("min(");
            } else if (aggregation instanceof Aggregation.Max) {
                jpqlBuilder.append("max(");
            } else if (aggregation instanceof Aggregation.Avg) {
                jpqlBuilder.append("avg(");
            }else {
                throw new AssertionError("Internal bug");
            }
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                this.visit(aggregation.getOperand());
                jpqlBuilder.append(')');
            } finally {
                this.currentPriority = oldPriority;
            }
        }
    
        @Override
        public void visitConvertExpression(ConvertExpression convertExpression) {
            this.visit(convertExpression.getOperand());
        }
    
        @Override
        public void visitAsExpression(AsExpression asExpression) {
            SessionFactoryImplementor factory = 
                    (SessionFactoryImplementor)
                    ((HibernateXEntityManagerFactory)asExpression.getCriteriaBuilder().getEntityManagerFactory())
                    .getSessionFactoryImplementor();
            Class javaType = asExpression.getJavaType();
            Type hibernateType = factory.getTypeResolver().heuristicType(javaType.getName());
            if ( hibernateType == null ) {
                throw new IllegalArgumentException(
                        "Could not convert java type [" + javaType.getName() + "] to Hibernate type"
                );
            }
            String targetTypeName = hibernateType.getName();
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder.append("cast(");
            this.visit(asExpression.getOperand());
            jpqlBuilder
            .append(" as ")
            .append(targetTypeName)
            .append(")");
        }
    
        @Override
        public void visitLiteralExpression(LiteralExpression literalExpression) {
            LiteralParameter parameter = this.getLiteralParameter(literalExpression);
            this.jpqlBuilder.append(':').append(parameter.getName());
        }
    
        @Override
        public void visitNullLiteralExpression(
                NullLiteralExpression nullLiteralExpression) {
            this.jpqlBuilder.append("null");
        }
    
        @Override
        public void visitConstantExpression(ConstantExpression constantExpression) {
            this.renderConstant(constantExpression.getValue());
        }
    
        @Override
        public void visitBinaryArithmeticExpression(
                BinaryArithmeticExpression binaryArithmeticExpression) {
            this.visit(binaryArithmeticExpression.getLeftOperand());
            this
            .jpqlBuilder
            .append(' ')
            .append(binaryArithmeticExpression.getOperator())
            .append(' ');
            this.visit(binaryArithmeticExpression.getRightOperand());
        }
    
        @Override
        public void visitUnaryArithmeticExpression(
                UnaryArithmeticExpression unaryArithmeticExpression) {
            this.jpqlBuilder.append(unaryArithmeticExpression.getOperator());
            this.visit(unaryArithmeticExpression.getOperand());
        }
    
        @Override
        public void visitTrimExpression(TrimExpression trimExpression) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder
            .append("trim(" );
            
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                jpqlBuilder
                .append(trimExpression.getTrimspec().name().toLowerCase())
                .append(' ');
                this.visit(trimExpression.getTrimCharacter());
                jpqlBuilder.append(" from ");
                this.visit(trimExpression.getTrimSource());
                jpqlBuilder.append(')');
            } finally {
                this.currentPriority = oldPriority;
            }
        }
    
        @Override
        public void visitNullifExpression(NullifExpression nullifExpression) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder.append("nullif(");
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                this.visit(nullifExpression.getPrimaryOperand());
                jpqlBuilder.append(", ");
                this.visit(nullifExpression.getSecondaryOperand());
            } finally {
                this.currentPriority = oldPriority;
            }
            jpqlBuilder.append(')');
        }
    
        @Override
        public void visitSubqueryComparisonModifierExpression(
                SubqueryComparisonModifierExpression subqueryComparisonModifierExpression) {
            this.jpqlBuilder.append(subqueryComparisonModifierExpression.getClass().getSimpleName().toLowerCase());
            this.visit(subqueryComparisonModifierExpression.getSubquery());
        }
    
        @Override
        public void visitSizeExpression(SizeExpression sizeExpression) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder.append("size(");
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                this.visit(sizeExpression.getCollection());
                jpqlBuilder.append(')');
            } finally {
                this.currentPriority = oldPriority;
            }
        }
    
        @Override
        public void visitSimpleCaseExpression(
                SimpleCaseExpression simpleCaseExpression) {
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                StringBuilder jpqlBuilder = this.jpqlBuilder;
                jpqlBuilder.append("case "); //here must append a space for simple case
                this.visit(simpleCaseExpression.getExpression());
                for (SimpleCaseExpression.WhenClause whenClause : simpleCaseExpression.getWhenClauses()) {
                    jpqlBuilder
                    .append(" when ");
                    this.renderConstant(whenClause.getCondition());
                    jpqlBuilder
                    .append(" then ");
                    this.visit(whenClause.getResult());
                }
                jpqlBuilder.append(" else ");
                this.visit(simpleCaseExpression.getOtherwiseResult());
                jpqlBuilder.append(" end");
            } finally {
                this.currentPriority = oldPriority;
            }
        }
    
        @Override
        public void visitSearchedCaseExpression(
                SearchedCaseExpression searchedCaseExpression) {
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                StringBuilder jpqlBuilder = this.jpqlBuilder;
                jpqlBuilder.append("case");  //here must not append a space for searched case
                for (SearchedCaseExpression.WhenClause whenClause : searchedCaseExpression.getWhenClauses()) {
                    jpqlBuilder.append(" when ");
                    this.visit(whenClause.getCondition());
                    jpqlBuilder.append(" then ");
                    this.visit(whenClause.getResult());
                }
                jpqlBuilder.append(" else ");
                this.visit(searchedCaseExpression.getOtherwiseResult());
                jpqlBuilder.append(" end");
            } finally {
                this.currentPriority = oldPriority;
            }
        }
        
        @Override
        public void visitConcatExpression(ConcatExpression concatExpression) {
            List> values = concatExpression.getValues();
            int size = values.size();
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            int oldPriority = this.currentPriority;
            this.currentPriority = PriorityConstants.LOWEST;
            try {
                if (size == 1) {
                    this.visit(values.get(0));
                } else {
                    for (int i = size - 2; i >= 0; i--) {
                        jpqlBuilder.append("concat(");
                    }
                    this.visit(values.get(0));
                    jpqlBuilder.append(", ");
                    this.visit(values.get(1));
                    jpqlBuilder.append(')');
                    for (int i = 2; i < size; i++) {
                        jpqlBuilder.append(", ");
                        this.visit(values.get(i));
                        jpqlBuilder.append(')');
                    }
                }
            } finally {
                this.currentPriority = oldPriority;
            }
        }
    
        @Override
        public void visitPathTypeExpression(PathTypeExpression pathTypeExpression) {
            this.jpqlBuilder.append("type(");
            this.visit(pathTypeExpression.getPath());
            this.jpqlBuilder.append(')');
        }
    
        @Override
        public void visitParameterExpression(
                ParameterExpression parameterExpression) {
            if (parameterExpression.getName() != null) {
                this.jpqlBuilder.append(':').append(parameterExpression.getName());
            } else {
                this.jpqlBuilder.append('?').append(parameterExpression.getPosition());
            }
        }
    
        @Override
        public void visitCoalesceExpression(CoalesceExpression coalesceExpression) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (coalesceExpression.getExpressions().isEmpty()) {
                jpqlBuilder.append("null");
            } else {
                jpqlBuilder.append("coalesce(");
                int oldPriority = this.currentPriority;
                this.currentPriority = PriorityConstants.LOWEST;
                try {
                    boolean addComma = false;
                    for (Expression expr : coalesceExpression.getExpressions()) {
                        if (addComma) {
                            jpqlBuilder.append(", ");
                        } else {
                            addComma = true;
                        }
                        this.visit(expr);
                    }
                } finally {
                    this.currentPriority = oldPriority;
                }
                
                jpqlBuilder.append(')');
            }
        }
    
        @Override
        public void visitMapEntryExpression(MapEntryExpression mapEntryExpression) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder.append("entry(");
            this.visit(mapEntryExpression.getMapAttributeJoin());
            jpqlBuilder.append(')');
        }

        @Override
        public void visitListIndexExpression(ListIndexExpression listIndexExpression) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            jpqlBuilder.append("index(");
            this.visit(listIndexExpression.getListAttributeJoin());
            jpqlBuilder.append(')');
        }

        @Override
        public void visitCompoundPredicate(CompoundPredicate compoundPredicate) {
            String op = compoundPredicate.getOperator().name().toLowerCase();
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            boolean addOp = false;
            for (Expression expr : compoundPredicate.getExpressions()) {
                if (addOp) {
                    jpqlBuilder.append(' ').append(op).append(' ');
                } else {
                    addOp = true;
                }
                this.visit(expr);
            }
        }
    
        @Override
        public void visitComparisonPredicate(ComparisonPredicate comparisonPredicate) {
            String op;
            if (comparisonPredicate instanceof ComparisonPredicate.Equal) {
                op = "=";
            } else if (comparisonPredicate instanceof ComparisonPredicate.NotEqual) {
                op = "!=";
            } else if (comparisonPredicate instanceof ComparisonPredicate.LessThan) {
                op = "<";
            } else if (comparisonPredicate instanceof ComparisonPredicate.LessThanOrEqual) {
                op = "<=";
            } else if (comparisonPredicate instanceof ComparisonPredicate.GreaterThan) {
                op = ">";
            } else if (comparisonPredicate instanceof ComparisonPredicate.GreaterThanOrEqual) {
                op = ">=";
            } else {
                throw new AssertionError();
            }
            this.visit(comparisonPredicate.getLeftOperand());
            this.jpqlBuilder.append(' ').append(op).append(' ');
            this.visit(comparisonPredicate.getRightOperand());
        }
    
        @Override
        public void visitBetweenPredicate(BetweenPredicate betweenPredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (betweenPredicate.getLowerBound() == null) {
                this.visit(betweenPredicate.getSource());
                if (betweenPredicate.isNegated()) {
                    jpqlBuilder.append(" > ");
                } else {
                    jpqlBuilder.append(" <= ");
                }
                this.visit(betweenPredicate.getUpperBound());
            } else if (betweenPredicate.getUpperBound() == null) {
                this.visit(betweenPredicate.getSource());
                if (betweenPredicate.isNegated()) {
                    jpqlBuilder.append(" < ");
                } else {
                    jpqlBuilder.append(" >= ");
                }
                this.visit(betweenPredicate.getLowerBound());
            } else {
                this.visit(betweenPredicate.getSource());
                jpqlBuilder.append(betweenPredicate.isNegated() ? " not between " : " between ");
                this.visit(betweenPredicate.getLowerBound());
                jpqlBuilder.append(" and ");
                this.visit(betweenPredicate.getUpperBound());
            }
        }
    
        @Override
        public void visitLikePredicate(LikePredicate likePredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            this.visit(likePredicate.getSource());
            if (likePredicate.isNegated()) {
                jpqlBuilder.append(" not");
            }
            jpqlBuilder.append(" like ");
            this.visit(likePredicate.getPattern());
            if (likePredicate.getEscapeCharacter() != null) {
                jpqlBuilder.append(" escape ");
                this.visit(likePredicate.getEscapeCharacter());
            }
        }
    
        @SuppressWarnings({ "unchecked", "rawtypes" })
        @Override
        public void visitInPredicate(InPredicate inPredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            List> partitions = (List)inPredicate.getPartitions();
            String renderedExpression;
            /*
             * Disable the jpqlBuilder add visit the main expression
             * in another StringBuilder.
             * Because the main expression should not be rendered or
             * should be be rendered for several times but the path id
             * should only be allocated once.
             */
            this.jpqlBuilder = new StringBuilder();
            try {
                this.visit(inPredicate.getExpression());
                renderedExpression = this.jpqlBuilder.toString();
            } finally {
                this.jpqlBuilder = jpqlBuilder;
            }
            
            if (partitions.size() == 0) {
                jpqlBuilder.append(inPredicate.isNegated() ? "1 = 1" : "1 = 0");
            } else if (partitions.size() == 1 && partitions.get(0).getValues().size() == 1) {
                Expression firstElementExpression = partitions.get(0).getValues().get(0);
                jpqlBuilder.append(renderedExpression);
                if (firstElementExpression instanceof Subquery) {
                    if (inPredicate.isNegated()) {
                        jpqlBuilder.append(" not");
                    }
                    jpqlBuilder.append(" in");
                    this.visit(firstElementExpression);
                } else {
                    if (inPredicate.isNegated()) {
                        jpqlBuilder.append(" != ");
                    } else {
                        jpqlBuilder.append(" = ");
                    }
                    this.visit(firstElementExpression);
                }
            } else {
                boolean addOuterParentheses = partitions.size() > 1;
                if (addOuterParentheses) {
                    jpqlBuilder.append('(');
                }
                String op = inPredicate.isNegated() ? " and " : " or ";
                boolean addOp = false;
                for (Partition partition : partitions) {
                    if (addOp) {
                        jpqlBuilder.append(op);
                    } else {
                        addOp = true;
                    }
                    jpqlBuilder.append(renderedExpression);
                    if (inPredicate.isNegated()) {
                        jpqlBuilder.append(" not");
                    }
                    List> values = (List)partition.getValues();
                    jpqlBuilder.append(" in(");
                    if (!partition.isNeedExpand()) {
                        jpqlBuilder
                        .append(':')
                        .append(this.getLiteralParameter(partition).getName());
                    } else {
                        int oldPriority = this.currentPriority;
                        this.currentPriority = PriorityConstants.LOWEST;
                        try {
                            boolean addComma = false;
                            for (Expression value : values) {
                                if (addComma) {
                                    jpqlBuilder.append(", ");
                                } else {
                                    addComma = true;
                                }
                                this.visit(value);
                            }
                        } finally {
                            this.currentPriority = oldPriority;
                        }
                    }
                    jpqlBuilder.append(')');
                }
                if (addOuterParentheses) {
                    jpqlBuilder.append(')');
                }
            }
        }
    
        @Override
        public void visitExistsPredicate(ExistsPredicate existsPredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (existsPredicate.isNegated()) {
                jpqlBuilder.append("not ");
            }
            jpqlBuilder.append("exists");
            this.visit(existsPredicate.getSubquery());
        }
    
        @Override
        public void visitIsEmptyPredicate(IsEmptyPredicate isEmptyPredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            this.visit(isEmptyPredicate.getCollection());
            jpqlBuilder.append(" is ");
            if (isEmptyPredicate.isNegated()) {
                jpqlBuilder.append("not ");
            }
            jpqlBuilder.append("empty");
        }
    
        @Override
        public void visitIsMemberPredicate(IsMemberPredicate isMemberPredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            this.visit(isMemberPredicate.getElement());
            if (isMemberPredicate.isNegated()) {
                jpqlBuilder.append(" not");
            }
            jpqlBuilder.append(" member of ");
            this.visit(isMemberPredicate.getCollection());
        }
    
        @Override
        public void visitIsTruePredicate(IsTruePredicate isTruePredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            this.visit(isTruePredicate.getOperand());
            if (isTruePredicate.isNegated()) {
                jpqlBuilder.append(" = 0");
            } else {
                jpqlBuilder.append(" = 1");
            }
        }
    
        @Override
        public void visitNullnessPredicate(NullnessPredicate nullnessPredicate) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            this.visit(nullnessPredicate.getOperand());
            if (nullnessPredicate.isNegated()) {
                jpqlBuilder.append(" is not null");
            } else {
                jpqlBuilder.append(" is null");
            }
        }
        
        protected void visitAbstractQuery(XAbstractQuery query) {
            final StringBuilder jpqlBuilder = this.jpqlBuilder;
            boolean addComma;
            
            /*
             * It is very important to keep the visit order as:
             * selection, restriction, grouping, groupRestriction, orders
             * 
             * QueryContext must visit the tree by the same order
             * 
             * because the PathId.getAppearancePosition() is very important.
             */
            jpqlBuilder.append("select ");
            if (query.isDistinct()) {
                jpqlBuilder.append("distinct ");
            }
            
            this.pathIdAllocator.push(PathId.Allocator.SELECTION);
            try {
                if (query.getSelection() != null) {
                    this.visit(query.getSelection());
                } else {
                    GraphTraveler graphTraveler = new GraphTraveler() {
                        
                        private boolean addComma;
                        
                        @Override
                        protected Iterator getNeighborNodeIterator(Entity node) {
                            List entities = node.getEntities();
                            return entities != null ? entities.iterator() : null;
                        }

                        @Override
                        protected void preTravelNeighborNodes(
                                GraphTravelContext ctx,
                                GraphTravelAction optionalGraphTravelAction) {
                            Entity entity = ctx.getNode();
                            if (entity.isUsed() && entity.isExplicit() && !entity.isFetch()) {
                                if (this.addComma) {
                                    jpqlBuilder.append(", ");
                                } else {
                                    this.addComma = true;
                                }
                                jpqlBuilder.append(entity.getRenderAlias());
                            }
                        }
                    };
                    for (Entity entity : this.queryContext.getRootEntities(query)) {
                        graphTraveler.depthFirstTravel(entity);
                    }
                }
            } finally {
                this.pathIdAllocator.pop();
            }
            
            this.pathIdAllocator.push(PathId.Allocator.ON_TREE);
            try {
                jpqlBuilder.append(" from ");
                addComma = false;
                for (Entity entity : this.queryContext.getRootEntities(query)) {
                    if (addComma) {
                        jpqlBuilder.append(", ");
                    } else {
                        addComma = true;
                    }
                    jpqlBuilder
                    .append(entity.getManagedType().getJavaType().getName())
                    .append(' ')
                    .append(entity.getRenderAlias());
                }
                for (Entity entity : this.queryContext.getRootEntities(query)) {
                    this.renderJoin(entity);
                }
            } finally {
                this.pathIdAllocator.pop();
            }
            
            this.pathIdAllocator.push(PathId.Allocator.RESTRICTION);
            try {
                Predicate restriction = query.getRestriction();
                if (restriction != null) {
                    jpqlBuilder.append(" where ");
                    this.visit(restriction);
                }
            } finally {
                this.pathIdAllocator.pop();
            }
            
            this.pathIdAllocator.push(PathId.Allocator.GROUP_LIST);
            try {
                List> groupList = query.getGroupList();
                if (!groupList.isEmpty()) {
                    jpqlBuilder.append(" group by ");
                    addComma = false;
                    for (Expression expr : groupList) {
                        if (addComma) {
                            jpqlBuilder.append(", ");
                        } else {
                            addComma = true;
                        }
                        this.visit(expr);
                    }
                }
            } finally {
                this.pathIdAllocator.pop();
            }
            
            this.pathIdAllocator.push(PathId.Allocator.GROUP_RESTRICTION);
            try {
            Predicate groupRestriction = query.getGroupRestriction();
                if (groupRestriction != null) {
                    jpqlBuilder.append(" having ");
                    this.visit(groupRestriction);
                } 
            } finally {
                this.pathIdAllocator.pop();
            }
        }
        
        protected void renderJoin(Entity entity) {
            if (!entity.isUsed()) {
                return;
            }
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (entity.getAttribute() != null) {
                jpqlBuilder
                .append(' ')
                .append(entity.getJoinType().name().toLowerCase())
                .append(" join ");
                if (entity.isFetch()) {
                    jpqlBuilder.append("fetch ");
                }
                jpqlBuilder
                .append(entity.getParent().getRenderAlias())
                .append('.')
                .append(entity.getAttribute().getName())
                .append(' ')
                .append(entity.getRenderAlias());
                
                Predicate on = entity.getOn();
                if (on != null) {
                    jpqlBuilder.append(" on ");
                    this.visit(entity.getOn());
                }
            }
            for (Entity childEntity : entity.getEntities()) {
                this.renderJoin(childEntity);
            }
        }
        
        protected void renderPathNode(PathNode pathNode) {
            Class treatAsType = pathNode.getTreatAsType();
            if (treatAsType != null) {
                StringBuilder jpqlBuilder = this.jpqlBuilder;
                jpqlBuilder.append("treat(");
                this.renderNonTreatedPathNode(pathNode);
                jpqlBuilder.append(" as ");
                jpqlBuilder.append(treatAsType.getName());
                jpqlBuilder.append(')');
            } else {
                this.renderNonTreatedPathNode(pathNode);
            }
        }
        
        protected void renderNonTreatedPathNode(PathNode pathNode) {
            if (pathNode.getEntity() != null) {
                this.jpqlBuilder.append(pathNode.getEntity().getRenderAlias());
            } else {
                this.renderPathNode(pathNode.getParent());
                this
                .jpqlBuilder
                .append('.')
                .append(pathNode.getAttribute().getName());
            }
        }
        
        protected void renderConstant(Object constant) {
            StringBuilder jpqlBuilder = this.jpqlBuilder;
            if (constant instanceof String) {
                String str = (String)constant;
                constant = str.replaceAll("'", "''");
                jpqlBuilder.append('\'').append(constant).append('\'');
            } else if (constant instanceof Character) {
                char ch = (Character)constant;
                if (ch == '\'') {
                    jpqlBuilder.append("''''");
                } else {
                    jpqlBuilder.append('\'').append(constant).append('\'');
                }
            } else {
                jpqlBuilder.append(constant);
                if (constant instanceof Long) {
                    jpqlBuilder.append('L');
                }
                if (constant instanceof Float) {
                    jpqlBuilder.append('F');
                }
                if (constant instanceof Double) {
                    jpqlBuilder.append('D');
                }
            }
        }
        
        protected LiteralParameter getLiteralParameter(Object object) {
            XOrderedMap literalParameters = this.literalParameters;
            if (literalParameters == null) {
                this.literalParameters = literalParameters = new LinkedHashMap<>();
            }
            LiteralParameter literalParameter = literalParameters.get(object);
            if (literalParameter == null) {
                if (object instanceof LiteralExpression) {
                    literalParameter = new LiteralParameter(literalParameters.size(), 
                            ((LiteralExpression)object).getValue());
                } else if (object instanceof Partition) {
                    Collection c = new ArrayList<>();
                    for (Expression value : ((Partition)object).getValues()) {
                        if (value instanceof LiteralExpression) {
                            c.add(((LiteralExpression)value).getValue());
                        } else {
                            c.add(((ConstantExpression)value).getValue());
                        }
                    }
                    literalParameter = new LiteralParameter(literalParameters.size(), c);
                } else {
                    Arguments.mustBeInstanceOfAnyOfValue("object", object, LiteralExpression.class, Partition.class);
                }
                literalParameters.put(object, literalParameter);
            }
            return literalParameter;
        }
        
        protected void visit(Object o) {
            if (o != null) {
                Object parent = this.pathIdAllocator.peek();
                this.pathIdAllocator.push(o);
                try {
                    if (o instanceof AbstractExpression) {
                        AbstractExpression abstractExpression = (AbstractExpression)o;
                        int priority = abstractExpression.getPriority();
                        if (priority >= this.currentPriority) {
                            int oldPriority = this.currentPriority;
                            this.currentPriority = priority;
                            try {
                                abstractExpression.accept(this);
                            } finally {
                                this.currentPriority = oldPriority;
                            }
                        } else {
                            int oldPriority = this.currentPriority;
                            this.currentPriority = priority;
                            try {
                                this.jpqlBuilder.append('(');
                                abstractExpression.accept(this);
                                this.jpqlBuilder.append(')');
                            } finally {
                                this.currentPriority = oldPriority;
                            }
                        }
                    } else {
                        ((AbstractNode)o).accept(this);
                    }
                    if (o instanceof TupleElement  && !(o instanceof CompoundSelection)) {
                        String alias = ((TupleElement)o).getAlias();
                        if (!Nulls.isNullOrEmpty(alias) && !(o instanceof From)) {
                            if (parent == PathId.Allocator.SELECTION || parent instanceof CompoundSelection) {
                                this.jpqlBuilder.append(" as ").append(alias);
                            }
                        }
                    }
                } finally {
                    this.pathIdAllocator.pop();
                }
            }
        } 
    }
    
    private static class TupleResultTransformer implements ResultTransformer {
        
        private static final long serialVersionUID = -2248010661387946493L;
        
        private CompoundSelection tupleSelection;

        public TupleResultTransformer(CompoundSelection tupleSelection) {
            this.tupleSelection = tupleSelection;
        }

        @Override
        public Object transformTuple(Object[] tuple, String[] aliases) {
            return new TupleImpl(this.tupleSelection, tuple);
        }

        @SuppressWarnings("rawtypes")
        @Override
        public List transformList(List collection) {
            return collection;
        }
    }
    
    private static class TupleImpl implements Tuple, Serializable {
        
        private static final long serialVersionUID = 9017189381422990789L;
        
        private static final LazyResource LAZY_RESOURCE = LazyResource.of(Resource.class);

        private Object[] values;
        
        private String[] aliases;
        
        private String[] nonNullAliases;
        
        private int size;
        
        private transient TupleElement[] tupleElements;
        
        public TupleImpl(CompoundSelection tupleSelection, Object[] values) {
            this.size = tupleSelection.getCompoundSelectionItems().size();
            TupleElement[] tupleElements = tupleSelection.getCompoundSelectionItems().toArray(new TupleElement[this.size]);
            String[] aliases = new String[this.size];
            Collection nonNullList = new ArrayList<>(this.size);
            for (int i = this.size - 1; i >= 0; i--) {
                String alias = tupleElements[i].getAlias();
                aliases[i] = alias; 
                if (alias != null) {
                    nonNullList.add(alias);
                }
            }
            this.aliases = aliases;
            this.nonNullAliases = nonNullList.toArray(new String[nonNullList.size()]);
            this.tupleElements = tupleElements;
            this.values = values;
        }
        
        @SuppressWarnings("unchecked")
        @Override
        public  X get(TupleElement tupleElement) {
            int size = this.size;
            TupleElement[] tupleElements = this.tupleElements;
            if (tupleElements != null) {
                for (int i = 0; i < size; i++) {
                    if (tupleElements[i] == tupleElement) {
                        return (X)this.values[i];
                    }
                }
            }
            String alias = tupleElement.getAlias();
            if (alias != null) {
                String[] aliases = this.aliases;
                for (int i = 0; i < size; i++) {
                    if (alias.equals(aliases[i])) {
                        return (X)this.values[i];
                    }
                }
            }
            throw new IllegalArgumentException(LAZY_RESOURCE.get().missTupleElementOfThisIsDeserialized());
        }

        @Override
        public Object get(String alias) {
            if (alias != null) {
                int size = this.size;
                String[] aliases = this.aliases;
                for (int i = 0; i < size; i++) {
                    if (alias.equals(aliases[i])) {
                        return this.values[i];
                    }
                }
                Arguments.mustBeAnyOfValue("alias", alias, this.nonNullAliases);
            }
            Arguments.mustNotBeNull("alias", alias);
            throw new AssertionError(/* impossible */);
        }

        @Override
        public Object get(int index) {
            try {
                return this.values[index];
            } catch (ArrayIndexOutOfBoundsException ex) {
                Arguments.indexMustBetweenValue("index", index, 0, true, this.values.length, false);
                throw new AssertionError(/* impossible */);
            }
        }

        @SuppressWarnings("unchecked")
        @Override
        public  X get(String alias, Class type) {
            return (X)this.get(alias);
        }

        @SuppressWarnings("unchecked")
        @Override
        public  X get(int index, Class type) {
            return (X)this.get(index);
        }

        @Override
        public List> getElements() {
            if (this.tupleElements == null) {
                throw new IllegalStateException(LAZY_RESOURCE.get().thisIsDeserialized());
            }
            return MACollections.wrap(this.tupleElements);
        }

        @Override
        public Object[] toArray() {
            return this.values;
        }
        
        private interface Resource {
            
            String missTupleElementOfThisIsDeserialized();
            
            String thisIsDeserialized();
        }
    }
}