com.amalto.core.storage.hibernate.StandardQueryHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.amalto.core.storage.hibernate.StandardQueryHandler.java

Source

/*
 * Copyright (C) 2006-2016 Talend Inc. - www.talend.com
 * 
 * This source code is available under agreement available at
 * %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt
 * 
 * You should have received a copy of the agreement along with this program; if not, write to Talend SA 9 rue Pages
 * 92150 Suresnes, France
 */

package com.amalto.core.storage.hibernate;

import static org.hibernate.criterion.Restrictions.and;
import static org.hibernate.criterion.Restrictions.eq;
import static org.hibernate.criterion.Restrictions.ge;
import static org.hibernate.criterion.Restrictions.gt;
import static org.hibernate.criterion.Restrictions.ilike;
import static org.hibernate.criterion.Restrictions.isNull;
import static org.hibernate.criterion.Restrictions.le;
import static org.hibernate.criterion.Restrictions.like;
import static org.hibernate.criterion.Restrictions.lt;
import static org.hibernate.criterion.Restrictions.not;
import static org.hibernate.criterion.Restrictions.or;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.criterion.AggregateProjection;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.PropertyProjection;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.SQLProjection;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.sql.JoinType;
import org.hibernate.transform.DistinctRootEntityResultTransformer;
import org.hibernate.type.IntegerType;
import org.talend.mdm.commmon.metadata.ComplexTypeMetadata;
import org.talend.mdm.commmon.metadata.CompoundFieldMetadata;
import org.talend.mdm.commmon.metadata.DefaultMetadataVisitor;
import org.talend.mdm.commmon.metadata.EnumerationFieldMetadata;
import org.talend.mdm.commmon.metadata.FieldMetadata;
import org.talend.mdm.commmon.metadata.ReferenceFieldMetadata;
import org.talend.mdm.commmon.metadata.SimpleTypeFieldMetadata;

import com.amalto.core.query.user.Alias;
import com.amalto.core.query.user.BigDecimalConstant;
import com.amalto.core.query.user.BinaryLogicOperator;
import com.amalto.core.query.user.BooleanConstant;
import com.amalto.core.query.user.ByteConstant;
import com.amalto.core.query.user.Compare;
import com.amalto.core.query.user.ComplexTypeExpression;
import com.amalto.core.query.user.Condition;
import com.amalto.core.query.user.ConstantCollection;
import com.amalto.core.query.user.ConstantCondition;
import com.amalto.core.query.user.Count;
import com.amalto.core.query.user.DateConstant;
import com.amalto.core.query.user.DateTimeConstant;
import com.amalto.core.query.user.Distinct;
import com.amalto.core.query.user.DoubleConstant;
import com.amalto.core.query.user.Expression;
import com.amalto.core.query.user.Field;
import com.amalto.core.query.user.FieldFullText;
import com.amalto.core.query.user.FloatConstant;
import com.amalto.core.query.user.FullText;
import com.amalto.core.query.user.Id;
import com.amalto.core.query.user.IndexedField;
import com.amalto.core.query.user.IntegerConstant;
import com.amalto.core.query.user.IsEmpty;
import com.amalto.core.query.user.IsNull;
import com.amalto.core.query.user.Isa;
import com.amalto.core.query.user.Join;
import com.amalto.core.query.user.LongConstant;
import com.amalto.core.query.user.Max;
import com.amalto.core.query.user.Min;
import com.amalto.core.query.user.NotIsEmpty;
import com.amalto.core.query.user.NotIsNull;
import com.amalto.core.query.user.OrderBy;
import com.amalto.core.query.user.Paging;
import com.amalto.core.query.user.Predicate;
import com.amalto.core.query.user.Range;
import com.amalto.core.query.user.Select;
import com.amalto.core.query.user.ShortConstant;
import com.amalto.core.query.user.StringConstant;
import com.amalto.core.query.user.TimeConstant;
import com.amalto.core.query.user.Type;
import com.amalto.core.query.user.TypedExpression;
import com.amalto.core.query.user.UnaryLogicOperator;
import com.amalto.core.query.user.VisitorAdapter;
import com.amalto.core.query.user.metadata.GroupSize;
import com.amalto.core.query.user.metadata.StagingBlockKey;
import com.amalto.core.query.user.metadata.StagingError;
import com.amalto.core.query.user.metadata.StagingSource;
import com.amalto.core.query.user.metadata.StagingStatus;
import com.amalto.core.query.user.metadata.TaskId;
import com.amalto.core.query.user.metadata.Timestamp;
import com.amalto.core.storage.CloseableIterator;
import com.amalto.core.storage.Storage;
import com.amalto.core.storage.StorageMetadataUtils;
import com.amalto.core.storage.StorageResults;
import com.amalto.core.storage.StorageType;
import com.amalto.core.storage.datasource.DataSource;
import com.amalto.core.storage.datasource.RDBMSDataSource;
import com.amalto.core.storage.exception.UnsupportedQueryException;
import com.amalto.core.storage.record.DataRecord;

class StandardQueryHandler extends AbstractQueryHandler {

    private static final Logger LOGGER = Logger.getLogger(StandardQueryHandler.class);

    private static final StringConstant EMPTY_STRING_CONSTANT = new StringConstant(StringUtils.EMPTY);

    private final CriterionAdapter criterionVisitor;

    private final StandardQueryHandler.CriterionFieldCondition criterionFieldCondition;

    private final Map<String, String> pathToAlias = new HashMap<>();

    protected final MappingRepository mappings;

    protected final TableResolver resolver;

    private Criteria criteria;

    private ProjectionList projectionList;

    private ComplexTypeMetadata mainType;

    private int aliasCount = 0;

    private String currentAliasName;

    private int countAggregateIndex = 0;

    public StandardQueryHandler(Storage storage, MappingRepository mappings, TableResolver resolver,
            StorageClassLoader storageClassLoader, Session session, Select select,
            List<TypedExpression> selectedFields, Set<ResultsCallback> callbacks) {
        super(storage, storageClassLoader, session, select, selectedFields, callbacks);
        this.mappings = mappings;
        this.resolver = resolver;
        criterionFieldCondition = new CriterionFieldCondition();
        DataSource dataSource = storage.getDataSource();
        if (!(dataSource instanceof RDBMSDataSource)) {
            throw new IllegalArgumentException(
                    "Storage '" + storage.getName() + "' is not using a RDBMS datasource."); //$NON-NLS-1$ //$NON-NLS-2$
        }
        criterionVisitor = new CriterionAdapter((RDBMSDataSource) dataSource);
    }

    @SuppressWarnings("rawtypes")
    protected StorageResults createResults(List list, boolean isProjection) {
        CloseableIterator<DataRecord> iterator;
        final Iterator listIterator = list.iterator();
        if (isProjection) {
            iterator = new ProjectionIterator(mappings, new CloseableIterator<Object>() {

                @Override
                public void close() throws IOException {
                }

                @Override
                public boolean hasNext() {
                    return listIterator.hasNext();
                }

                @Override
                public Object next() {
                    return listIterator.next();
                }

                @Override
                public void remove() {
                }
            }, selectedFields, callbacks);
        } else {
            iterator = new ListIterator(mappings, storageClassLoader, listIterator, callbacks);
        }

        return new HibernateStorageResults(storage, select, iterator);
    }

    private StorageResults createResults(ScrollableResults scrollableResults, boolean isProjection) {
        CloseableIterator<DataRecord> iterator;
        if (isProjection) {
            iterator = new ProjectionIterator(mappings, scrollableResults, selectedFields, callbacks);
        } else {
            iterator = new ScrollableIterator(mappings, storageClassLoader, scrollableResults, callbacks);
        }
        return new HibernateStorageResults(storage, select, iterator);
    }

    @Override
    public StorageResults visit(Join join) {
        FieldMetadata rightField = join.getRightField().getFieldMetadata();
        FieldMetadata leftField = join.getLeftField().getFieldMetadata();
        // Choose the right join alias
        String rightAlias = rightField.getContainingType().getName();
        if (rightField.getEntityTypeName().equals(leftField.getEntityTypeName())) {
            // TMDM-7170: use a new alias for recursive relations
            rightAlias = createNewAlias();
        }
        // Choose the right join type
        JoinType joinType;
        switch (join.getJoinType()) {
        case INNER:
            joinType = JoinType.INNER_JOIN;
            break;
        case LEFT_OUTER:
            joinType = JoinType.LEFT_OUTER_JOIN;
            break;
        case FULL:
            joinType = JoinType.FULL_JOIN;
            break;
        default:
            throw new NotImplementedException("No support for join type " + join.getJoinType()); //$NON-NLS-1$
        }
        // Select a path from mainType to the selected field (properties are '.' separated).
        List<FieldMetadata> path = StorageMetadataUtils.path(mainType, leftField);
        // Empty path means no path then this is an error (all joined entities should be reachable from main type).
        if (path.isEmpty()) {
            String destinationFieldName;
            try {
                destinationFieldName = rightField.getName();
            } catch (Exception e) {
                // Ignored
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Exception occurred during exception creation", e); //$NON-NLS-1$
                }
                destinationFieldName = String.valueOf(rightField);
            }
            throw new IllegalArgumentException("Join to '" + destinationFieldName + "' (in type '" //$NON-NLS-1$ //$NON-NLS-2$
                    + rightField.getContainingType().getName() + "') is invalid since there is no path from '" //$NON-NLS-1$
                    + mainType.getName() + "' to this field."); //$NON-NLS-1$
        }
        // Generate all necessary joins to go from main type to join right table.
        generateJoinPath(Collections.singleton(rightAlias), joinType, path);
        return null;
    }

    private String getReferenceFieldJoinPath(ReferenceFieldMetadata previousRefFieldMetadata,
            FieldMetadata nextField, String aliasPathKey) {
        if (previousRefFieldMetadata != null) {
            if (nextField instanceof ReferenceFieldMetadata) {
                ReferenceFieldMetadata referenceFieldMetadata = (ReferenceFieldMetadata) nextField;
                if (previousRefFieldMetadata.getReferencedType().getName()
                        .equals(referenceFieldMetadata.getContainingType().getName())) {
                    return aliasPathKey + "/" + nextField.getName(); //$NON-NLS-1$
                } else {
                    if (previousRefFieldMetadata.getReferencedType().getSubTypes()
                            .contains(referenceFieldMetadata.getContainingType())) {
                        return aliasPathKey + "/" + nextField.getName(); //$NON-NLS-1$
                    }
                }
            }
        }
        return nextField.getName();
    }

    private void generateJoinPath(Set<String> rightTableAliases, JoinType joinType, List<FieldMetadata> path) {
        Iterator<FieldMetadata> pathIterator = path.iterator();
        String previousAlias = mainType.getName();
        ReferenceFieldMetadata previousRefFieldMetadata = null;
        String aliasPathKey = ""; //$NON-NLS-1$

        while (pathIterator.hasNext()) {
            FieldMetadata nextField = pathIterator.next();
            String newAlias = createNewAlias();
            aliasPathKey = getReferenceFieldJoinPath(previousRefFieldMetadata, nextField, aliasPathKey);

            // TODO One interesting improvement here: can add conditions on rightTable when defining join.
            if (pathIterator.hasNext()) {
                if (!pathToAlias.containsKey(aliasPathKey)) {
                    criteria.createAlias(previousAlias + '.' + nextField.getName(), newAlias, joinType);
                    pathToAlias.put(aliasPathKey, newAlias);
                    previousAlias = newAlias;
                } else {
                    previousAlias = pathToAlias.get(aliasPathKey);
                }
            } else {
                if (!pathToAlias.containsKey(aliasPathKey)) {
                    for (String rightTableAlias : rightTableAliases) {
                        criteria.createAlias(previousAlias + '.' + nextField.getName(), rightTableAlias, joinType);
                        pathToAlias.put(aliasPathKey, rightTableAlias);
                    }
                    previousAlias = rightTableAliases.iterator().next();
                } else {
                    previousAlias = pathToAlias.get(aliasPathKey);
                }
            }
            if (nextField instanceof ReferenceFieldMetadata) {
                previousRefFieldMetadata = (ReferenceFieldMetadata) nextField;
            }
        }
    }

    private String createNewAlias() {
        return "a" + aliasCount++; //$NON-NLS-1$
    }

    @Override
    public StorageResults visit(Alias alias) {
        currentAliasName = alias.getAliasName();
        alias.getTypedExpression().accept(this);
        currentAliasName = null;
        return null;
    }

    @Override
    public StorageResults visit(Distinct distinct) {
        // Standard visit for the expression where distinct should be added
        distinct.getExpression().accept(this);
        // Wraps the last projection into a 'distinct' statement
        // Note: Hibernate does not provide projection editing functions... have to work around that with a new
        // projection list.
        ProjectionList newProjectionList = Projections.projectionList();
        int i = 0;
        for (; i < projectionList.getLength() - 1; i++) {
            newProjectionList.add(projectionList.getProjection(i));
        }
        newProjectionList.add(Projections.distinct(projectionList.getProjection(i)));
        projectionList = newProjectionList;
        return null;
    }

    @Override
    public StorageResults visit(Type type) {
        if (currentAliasName != null) {
            projectionList.add(new ClassNameProjection(currentAliasName), currentAliasName);
            ProjectionList previousList = projectionList;
            try {
                projectionList = ReadOnlyProjectionList.makeReadOnly(projectionList);
                type.getField().accept(this);
            } finally {
                projectionList = previousList;
            }
        } else {
            throw new IllegalStateException("Expected an alias for a type expression."); //$NON-NLS-1$
        }
        return null;
    }

    @Override
    public StorageResults visit(StringConstant constant) {
        Projection p = new ConstantStringProjection(currentAliasName, constant.getValue());
        if (currentAliasName != null) {
            projectionList.add(p, currentAliasName);
        } else {
            throw new IllegalStateException("Expected an alias for a constant expression."); //$NON-NLS-1$
        }
        return null;
    }

    @Override
    public StorageResults visit(Timestamp timestamp) {
        String timeStamp = mappings.getMappingFromDatabase(mainType).getDatabaseTimestamp();
        if (timeStamp != null) {
            projectionList.add(Projections.property(timeStamp));
        } else {
            EMPTY_STRING_CONSTANT.accept(this);
        }
        return null;
    }

    @Override
    public StorageResults visit(TaskId taskId) {
        String taskIdField = mappings.getMappingFromDatabase(mainType).getDatabaseTaskId();
        if (taskIdField != null) {
            projectionList.add(Projections.property(taskIdField));
        } else {
            EMPTY_STRING_CONSTANT.accept(this);
        }
        return null;
    }

    @Override
    public StorageResults visit(StagingStatus stagingStatus) {
        projectionList.add(Projections.property(Storage.METADATA_STAGING_STATUS));
        return null;
    }

    @Override
    public StorageResults visit(StagingError stagingError) {
        projectionList.add(Projections.property(Storage.METADATA_STAGING_ERROR));
        return null;
    }

    @Override
    public StorageResults visit(StagingSource stagingSource) {
        projectionList.add(Projections.property(Storage.METADATA_STAGING_SOURCE));
        return null;
    }

    @Override
    public StorageResults visit(StagingBlockKey stagingBlockKey) {
        projectionList.add(Projections.property(Storage.METADATA_STAGING_BLOCK_KEY));
        return null;
    }

    @Override
    public StorageResults visit(GroupSize groupSize) {
        Projection groupSizeProjection = Projections.sqlGroupProjection(
                "count(this_." + Storage.METADATA_TASK_ID + ") as talend_group_size", //$NON-NLS-1$ //$NON-NLS-2$ 
                "this_." + Storage.METADATA_TASK_ID, new String[] { "talend_group_size" }, //$NON-NLS-1$ //$NON-NLS-2$ 
                new org.hibernate.type.Type[] { new IntegerType() });
        projectionList.add(groupSizeProjection);
        return null;
    }

    @Override
    public StorageResults visit(final Field field) {
        final FieldMetadata userFieldMetadata = field.getFieldMetadata();
        ComplexTypeMetadata containingType = getContainingType(userFieldMetadata);
        final Set<String> aliases = getAliases(containingType, field);
        userFieldMetadata.accept(new DefaultMetadataVisitor<Void>() {

            @Override
            public Void visit(ReferenceFieldMetadata referenceField) {
                // Automatically selects referenced ID in case of FK.
                if (userFieldMetadata instanceof ReferenceFieldMetadata) {
                    referenceField.getReferencedField().accept(this);
                }
                return null;
            }

            @Override
            public Void visit(SimpleTypeFieldMetadata simpleField) {
                if (!simpleField.isMany()) {
                    for (String alias : aliases) {
                        projectionList.add(Projections.property(alias + '.' + simpleField.getName()));
                    }
                } else {
                    projectionList.add(new ManyFieldProjection(aliases, simpleField, resolver,
                            (RDBMSDataSource) storage.getDataSource()));
                }
                return null;
            }

            @Override
            public Void visit(EnumerationFieldMetadata enumField) {
                if (!enumField.isMany()) {
                    for (String alias : aliases) {
                        projectionList.add(Projections.property(alias + '.' + enumField.getName()));
                    }
                } else {
                    projectionList.add(new ManyFieldProjection(aliases, enumField, resolver,
                            (RDBMSDataSource) storage.getDataSource()));
                }
                return null;
            }
        });
        return null;
    }

    private ComplexTypeMetadata getContainingType(FieldMetadata userFieldMetadata) {
        ComplexTypeMetadata containingType = userFieldMetadata.getContainingType();
        if (!containingType.isInstantiable()) {
            if (containingType.getContainer() != null) {
                return getContainingType(containingType.getContainer());
            } else {
                return mainType;
            }
        }
        return containingType;
    }

    /**
     * Test if a path has elements part of a inheritance tree.
     * 
     * @param path A list of {@link org.talend.mdm.commmon.metadata.FieldMetadata fields} that represents a path from an
     * entity type down to a selected field (for projection or condition).
     * @return <code>true</code> if at least one element in the <code>path</code> is contained in a type part of an
     * inheritance tree, <code>false</code> otherwise. If path is empty, returns <code>true</code> as method cannot
     * decide.
     */
    private static boolean pathContainsInheritance(List<FieldMetadata> path) {
        if (path.isEmpty()) {
            // Path is empty: it may contain inheritance elements, can't decide.
            return true;
        }
        for (FieldMetadata fieldMetadata : path) {
            ComplexTypeMetadata containingType = fieldMetadata.getContainingType();
            if (!containingType.getSubTypes().isEmpty() || !containingType.getSuperTypes().isEmpty()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Test if a path has elements part of parent type
     * 
     * @param path A list of {@link org.talend.mdm.commmon.metadata.FieldMetadata fields} that represents a path from an
     * entity type down to a selected field (for projection or condition).
     * @param type A type contains this field    
     * @return <code>true</code> if field's path has contained its parent
     * {@link org.talend.mdm.commmon.metadata.ComplexTypeMetadata type}
     */
    private static boolean pathContainsParentType(List<FieldMetadata> path, ComplexTypeMetadata type) {
        for (FieldMetadata fieldMetadata : path) {
            if (type.equals(fieldMetadata.getContainingType())) {
                return true;
            }
        }
        return false;
    }

    /**
     * <p>
     * Generate an alias to the <code>field</code> starting from <code>type</code>. This code ensures all paths to
     * <code>field</code> are covered (this field might be present several times inside the MDM entity scope).
     * </p>
     * 
     * @param type A type in the query.
     * @param field A field to include in current Hibernate criteria.
     * @return A set of aliases that represents the <code>field</code>.
     */
    private Set<String> getAliases(ComplexTypeMetadata type, Field field) {
        FieldMetadata fieldMetadata = field.getFieldMetadata();
        boolean fieldContainerInstantiable = fieldMetadata.getContainingType().getEntity().isInstantiable();
        String previousAlias;
        if (fieldContainerInstantiable) {
            previousAlias = fieldMetadata.getEntityTypeName();
        } else {
            previousAlias = type.getName();
        }
        Set<List<FieldMetadata>> paths;
        if (pathContainsInheritance(field.getPath())
                || (!fieldContainerInstantiable && !pathContainsParentType(field.getPath(), type))) {
            paths = StorageMetadataUtils.paths(type, fieldMetadata);
        } else {
            paths = Collections.singleton(field.getPath());
        }
        Set<String> aliases = new HashSet<>(paths.size());
        for (List<FieldMetadata> path : paths) {
            JoinType joinType = JoinType.INNER_JOIN;
            Iterator<FieldMetadata> iterator = path.iterator();
            ReferenceFieldMetadata previousRefFieldMetadata = null;
            String aliasPathKey = ""; //$NON-NLS-1$
            while (iterator.hasNext()) {
                FieldMetadata next = iterator.next();
                if (next instanceof ReferenceFieldMetadata) {
                    aliasPathKey = getReferenceFieldJoinPath(previousRefFieldMetadata, next, aliasPathKey);
                    String alias = pathToAlias.get(aliasPathKey);
                    if (alias == null) {
                        alias = createNewAlias();
                        // Remembers aliases created for the next field in path (to prevent same alias name creation for
                        // field - in case of join, for example -)
                        pathToAlias.put(aliasPathKey, alias);
                        // TMDM-4866: Do a left join in case FK is not mandatory (only if there's one path).
                        // TMDM-7636: As soon as a left join is selected all remaining join should remain left outer.
                        if (next.isMandatory() && paths.size() == 1 && joinType != JoinType.LEFT_OUTER_JOIN) {
                            if (storage != null && storage instanceof HibernateStorage
                                    && StorageType.STAGING == storage.getType()) {
                                joinType = JoinType.LEFT_OUTER_JOIN;
                            } else {
                                joinType = JoinType.INNER_JOIN;
                            }
                        } else {
                            joinType = JoinType.LEFT_OUTER_JOIN;
                        }
                        criteria.createAlias(previousAlias + '.' + next.getName(), alias, joinType);
                    }
                    previousAlias = alias;
                    previousRefFieldMetadata = (ReferenceFieldMetadata) next;
                }
            }
            aliases.add(previousAlias);
            previousAlias = type.getName();

        }
        return aliases;
    }

    @Override
    public StorageResults visit(Count count) {
        projectionList.add(Projections.rowCount());
        return null;
    }

    @Override
    public StorageResults visit(Select select) {
        createCriteria(select);
        // Paging
        Paging paging = select.getPaging();
        paging.accept(this);
        return createResults(select);
    }

    @SuppressWarnings("rawtypes")
    protected StorageResults createResults(Select select) {
        Paging paging = select.getPaging();
        int pageSize = paging.getLimit();
        boolean hasPaging = pageSize < Integer.MAX_VALUE;
        // Results
        if (!hasPaging) {
            if (storage instanceof HibernateStorage) {
                RDBMSDataSource dataSource = (RDBMSDataSource) storage.getDataSource();
                if (dataSource.getDialectName() == RDBMSDataSource.DataSourceDialect.DB2) {
                    // TMDM-7701: DB2 doesn't like use of SCROLL_INSENSITIVE for projections including a CLOB.
                    if (select.isProjection()) {
                        return createResults(criteria.scroll(ScrollMode.FORWARD_ONLY), true);
                    } else {
                        return createResults(criteria.scroll(ScrollMode.SCROLL_INSENSITIVE), false);
                    }
                } else {
                    return createResults(criteria.scroll(ScrollMode.SCROLL_INSENSITIVE), select.isProjection());
                }
            }
            return createResults(criteria.scroll(ScrollMode.SCROLL_INSENSITIVE), select.isProjection());
        } else {
            List list = criteria.list();
            return createResults(list, select.isProjection());
        }
    }

    protected Criteria createCriteria(Select select) {
        List<ComplexTypeMetadata> selectedTypes = select.getTypes();
        if (selectedTypes.isEmpty()) {
            throw new IllegalArgumentException("Select clause is expected to select at least one entity type."); //$NON-NLS-1$
        }
        mainType = selectedTypes.get(0);
        String mainClassName = ClassCreator.getClassName(mainType.getName());
        criteria = session.createCriteria(mainClassName, mainType.getName());
        if (!select.forUpdate()) {
            criteria.setReadOnly(true); // We are reading data, turns on ready only mode.
        }
        // Handle JOIN (if any)
        List<Join> joins = select.getJoins();
        for (Join join : joins) {
            join.accept(this);
        }
        // If select is not a projection, selecting root type is enough, otherwise add projection for selected fields.
        if (select.isProjection()) {
            boolean toDistinct = true;
            projectionList = Projections.projectionList();
            {
                List<TypedExpression> queryFields = select.getSelectedFields();
                boolean isCountQuery = false;
                boolean hasGroupSize = false;
                for (Expression selectedField : queryFields) {
                    if (selectedField instanceof GroupSize) {
                        hasGroupSize = true;
                    }
                    selectedField.accept(this);
                    if (selectedField instanceof Alias) {
                        Alias alias = (Alias) selectedField;
                        if (alias.getTypedExpression() instanceof Count) {
                            isCountQuery = true;
                        }
                        if (alias.getTypedExpression() instanceof Distinct) {
                            toDistinct = false;
                        }
                    }
                }
                // TMDM-9502/TMDM-10395, If selected fields including "GroupSize", besides GROUP BY "x_talend_task_id"
                // NOT ORACLE DB, should GROUP BY "All Key Fields"
                // ORACLE DB, need to GROUP BY "All Selected Fields"
                if (hasGroupSize) {
                    projectionList = optimizeProjectionList(mainType, projectionList);
                }
                if (isCountQuery && queryFields.size() > 1) {
                    Projection projection = projectionList.getProjection(projectionList.getLength() - 1);
                    projectionList = Projections.projectionList();
                    projectionList.add(projection);
                    TypedExpression countTypedExpression = selectedFields.get(queryFields.size() - 1);
                    selectedFields.clear();
                    selectedFields.add(countTypedExpression);
                }
            }
            // for SELECT DISTINCT, ORDER BY expressions must appear in select list. Or it will throw exception in H2, postgres...
            for (OrderBy current : select.getOrderBy()) {
                if ((current.getExpression() instanceof Field
                        && !select.getSelectedFields().contains(current.getExpression()))
                        || current.getExpression() instanceof Type || current.getExpression() instanceof Alias) {
                    toDistinct = false;
                    break;
                }
            }
            if (select.getOrderBy().size() > 0 && toDistinct) {
                criteria.setProjection(Projections.distinct(projectionList));
            } else {
                criteria.setProjection(projectionList);
            }
        } else {
            // TMDM-5388: Hibernate sometimes returns duplicate results (like for User stored in System storage), this
            // line avoids this situation.
            criteria.setResultTransformer(DistinctRootEntityResultTransformer.INSTANCE);
        }
        // Make projection read only in case code tries to modify it later (see code that handles condition).
        projectionList = ReadOnlyProjectionList.makeReadOnly(projectionList);
        // Handle condition (if there's any condition to handle).
        Condition condition = select.getCondition();
        if (condition != null) {
            condition.accept(this);
        }
        // Order by
        for (OrderBy current : select.getOrderBy()) {
            if (current.getExpression() instanceof Count) {
                int limit = select.getPaging().getLimit();
                if (limit > 0) {
                    RDBMSDataSource dataSource = (RDBMSDataSource) storage.getDataSource();
                    if (dataSource.getDialectName() == RDBMSDataSource.DataSourceDialect.DB2
                            || dataSource.getDialectName() == RDBMSDataSource.DataSourceDialect.SQL_SERVER) {
                        LOGGER.error("The query is not supported by DB2 and SQLSERVER Database."); //$NON-NLS-1$
                        throw new UnsupportedQueryException(
                                "The query is not supported by DB2 and SQLSERVER Database."); //$NON-NLS-1$
                    }
                }
            }
            current.accept(this);
        }
        return criteria;
    }

    private ProjectionList optimizeProjectionList(ComplexTypeMetadata mainType, ProjectionList oldProjectionList) {
        ProjectionList newProjectionList = null;
        RDBMSDataSource dataSource = (RDBMSDataSource) storage.getDataSource();
        if (dataSource.getDialectName() != RDBMSDataSource.DataSourceDialect.ORACLE_10G) {
            newProjectionList = oldProjectionList;
            for (FieldMetadata keyField : mainType.getKeyFields()) {
                newProjectionList.add(Projections.groupProperty(keyField.getName()));
            }
        } else { // ORACLE need to GROUP BY all selected fields
            newProjectionList = Projections.projectionList();
            Set<String> groupBys = new LinkedHashSet<String>();// GROUP BY fields
            ProjectionList extraProjectionList = Projections.projectionList();
            for (int i = 0; i < oldProjectionList.getLength(); i++) {
                String propertyName = null;
                Projection oldProjection = oldProjectionList.getProjection(i);
                if (oldProjection instanceof SQLProjection) { // Group Size
                    newProjectionList.add(oldProjection);
                } else if (oldProjection instanceof PropertyProjection) {// normal fields
                    propertyName = ((PropertyProjection) oldProjection).getPropertyName();
                    newProjectionList.add(Projections.groupProperty(propertyName));
                } else if (oldProjection instanceof AggregateProjection) {// Max, Min
                    propertyName = ((AggregateProjection) oldProjection).getPropertyName();
                    newProjectionList.add(oldProjection);
                    extraProjectionList.add(Projections.groupProperty(propertyName));
                }
                if (propertyName != null) {
                    groupBys.add(propertyName);
                }
            }
            // Add key fields to GROUP BY
            for (FieldMetadata keyField : mainType.getKeyFields()) {
                String keyFieldName = mainType.getName() + '.' + keyField.getName();
                if (!groupBys.contains(keyFieldName)) {
                    extraProjectionList.add(Projections.groupProperty(keyFieldName));
                }
            }
            for (int i = 0; i < extraProjectionList.getLength(); i++) {
                newProjectionList.add(extraProjectionList.getProjection(i));
            }
        }
        return newProjectionList;
    }

    @Override
    public StorageResults visit(FullText fullText) {
        // Ignore full text queries (if any).
        return null;
    }

    @Override
    public StorageResults visit(FieldFullText fullText) {
        // Ignore full text queries (if any).
        return null;
    }

    @Override
    public StorageResults visit(Paging paging) {
        if (paging.getLimit() < Integer.MAX_VALUE) {
            criteria.setFirstResult(paging.getStart());
            criteria.setFetchSize(JDBC_FETCH_SIZE);
            criteria.setMaxResults(paging.getLimit());
        }
        return null;
    }

    @Override
    public StorageResults visit(OrderBy orderBy) {
        TypedExpression orderByExpression = orderBy.getExpression();
        CriterionFieldCondition fieldCondition = new CriterionFieldCondition();
        FieldCondition condition = orderByExpression.accept(fieldCondition);
        if (orderByExpression instanceof Field) {
            Field field = (Field) orderByExpression;
            FieldMetadata userFieldMetadata = field.getFieldMetadata();
            ComplexTypeMetadata containingType = getContainingType(userFieldMetadata);
            Set<String> aliases = getAliases(containingType, field);
            condition.criterionFieldNames = new ArrayList<>(aliases.size());
            for (String alias : aliases) {
                addCondition(condition, alias, userFieldMetadata);
            }
        }
        if (orderByExpression instanceof Count) {
            Count count = (Count) orderByExpression;
            String propertyName = count.getExpression().accept(fieldCondition).criterionFieldNames.get(0);
            ProjectionList list = projectionList;
            if (projectionList instanceof ReadOnlyProjectionList) {
                list = ((ReadOnlyProjectionList) projectionList).inner();
            }
            list.add(Projections.groupProperty(propertyName));
            String alias = "x_talend_countField" + countAggregateIndex++; //$NON-NLS-1$
            list.add(Projections.count(propertyName).as(alias));
            switch (orderBy.getDirection()) {
            case ASC:
                criteria.addOrder(Order.asc(alias));
                break;
            case DESC:
                criteria.addOrder(Order.desc(alias));
                break;
            }
        }
        if (condition != null) {
            for (String fieldName : condition.criterionFieldNames) {
                OrderBy.Direction direction = orderBy.getDirection();
                switch (direction) {
                case ASC:
                    criteria.addOrder(Order.asc(fieldName));
                    break;
                case DESC:
                    criteria.addOrder(Order.desc(fieldName));
                    break;
                }
            }
        }
        return null;
    }

    @Override
    public StorageResults visit(BinaryLogicOperator condition) {
        criteria.add(condition.accept(criterionVisitor));
        return null;
    }

    @Override
    public StorageResults visit(UnaryLogicOperator condition) {
        criteria.add(condition.accept(criterionVisitor));
        return null;
    }

    @Override
    public StorageResults visit(Isa isa) {
        criteria.add(isa.accept(criterionVisitor));
        return null;
    }

    @Override
    public StorageResults visit(Compare condition) {
        Criterion criterion = condition.accept(criterionVisitor);
        criteria.add(criterion);
        return null;
    }

    @Override
    public StorageResults visit(Max max) {
        FieldCondition fieldCondition = max.getExpression().accept(criterionFieldCondition);
        for (String criterionFieldName : fieldCondition.criterionFieldNames) {
            projectionList.add(Projections.max(criterionFieldName));
        }
        return null;
    }

    @Override
    public StorageResults visit(Min min) {
        FieldCondition fieldCondition = min.getExpression().accept(criterionFieldCondition);
        for (String criterionFieldName : fieldCondition.criterionFieldNames) {
            projectionList.add(Projections.min(criterionFieldName));
        }
        return null;
    }

    @Override
    public StorageResults visit(IsNull isNull) {
        Criterion criterion = isNull.accept(criterionVisitor);
        criteria.add(criterion);
        return null;
    }

    @Override
    public StorageResults visit(IsEmpty isEmpty) {
        Criterion criterion = isEmpty.accept(criterionVisitor);
        criteria.add(criterion);
        return null;
    }

    @Override
    public StorageResults visit(NotIsEmpty notIsEmpty) {
        Criterion criterion = notIsEmpty.accept(criterionVisitor);
        criteria.add(criterion);
        return null;
    }

    @Override
    public StorageResults visit(NotIsNull notIsNull) {
        Criterion criterion = notIsNull.accept(criterionVisitor);
        criteria.add(criterion);
        return null;
    }

    @Override
    public StorageResults visit(Range range) {
        Criterion criterion = range.accept(criterionVisitor);
        if (criterion != null) {
            criteria.add(criterion);
        }
        return null;
    }

    private class CriterionAdapter extends VisitorAdapter<Criterion> {

        private final CriterionFieldCondition visitor = new CriterionFieldCondition();

        private final RDBMSDataSource datasource;

        private CriterionAdapter(RDBMSDataSource datasource) {
            this.datasource = datasource;
        }

        @Override
        public Criterion visit(Condition condition) {
            if (condition instanceof ConstantCondition) {
                ConstantCondition constantCondition = ((ConstantCondition) condition);
                if (constantCondition.value()) {
                    return TRUE_CRITERION;
                } else {
                    return FALSE_CRITERION;
                }
            }
            return super.visit(condition);
        }

        @Override
        public Criterion visit(UnaryLogicOperator condition) {
            Predicate predicate = condition.getPredicate();
            Criterion conditionCriterion = condition.getCondition().accept(this);

            if (predicate == Predicate.NOT) {
                return not(conditionCriterion);
            } else {
                throw new NotImplementedException("No support for predicate '" + predicate + "'"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        @Override
        public Criterion visit(Isa isa) {
            FieldCondition fieldCondition = isa.getExpression().accept(visitor);
            if (fieldCondition == null) {
                return TRUE_CRITERION;
            }
            if (fieldCondition.criterionFieldNames.isEmpty()) {
                // Case #1: doing a simple instance type check on main selected type.
                return Restrictions.eq("class", storageClassLoader.getClassFromType(isa.getType())); //$NON-NLS-1$
            } else {
                // Case #2: doing a instance type check on a field reachable from main selected type.
                // First, need to join with all tables to get to the table that stores the type
                List<FieldMetadata> path = StorageMetadataUtils.path(mainType, fieldCondition.fieldMetadata);
                if (path.isEmpty()) {
                    throw new IllegalStateException("Expected field '" + fieldCondition.fieldMetadata.getName() //$NON-NLS-1$
                            + "' to be reachable from '" + mainType.getName() + "'."); //$NON-NLS-1$ //$NON-NLS-2$
                }
                // Generate the joins
                Set<String> aliases = getAliases(mainType, fieldCondition.field);
                generateJoinPath(aliases, JoinType.INNER_JOIN, path);
                // Find the criteria that does the join to the table to check (only way to get the SQL alias for table).
                Criteria foundSubCriteria = findCriteria(criteria, aliases);
                String name = storageClassLoader.getClassFromType(isa.getType()).getName();
                return new FieldTypeCriterion(foundSubCriteria, name);
            }
        }

        @Override
        public Criterion visit(IsNull isNull) {
            TypedExpression field = isNull.getField();
            FieldCondition fieldCondition = field.accept(visitor);
            if (fieldCondition == null) {
                return TRUE_CRITERION;
            }
            if (fieldCondition.isMany) {
                throw new UnsupportedOperationException("Does not support 'is null' operation on collections."); //$NON-NLS-1$
            }
            if (fieldCondition.criterionFieldNames.isEmpty()) {
                throw new IllegalStateException("No field name for 'is null' condition on " + field); //$NON-NLS-1$
            } else {
                // Criterion affect multiple fields
                Criterion current = null;
                for (String criterionFieldName : fieldCondition.criterionFieldNames) {
                    Criterion criterion;
                    if (field instanceof Field) {
                        // TMDM-7700: Fix incorrect alias for isNull condition on FK (pick the FK's containing type
                        // iso. the referenced type).
                        FieldMetadata fieldMetadata = ((Field) field).getFieldMetadata();
                        if (fieldMetadata.getContainingType().isInstantiable()) {
                            String typeName = fieldMetadata.getEntityTypeName();
                            criterion = Restrictions.isNull(typeName + '.' + fieldMetadata.getName());
                        } else {
                            criterion = Restrictions.isNull(criterionFieldName);
                        }
                    } else {
                        criterion = Restrictions.isNull(criterionFieldName);
                    }
                    if (current == null) {
                        current = criterion;
                    } else {
                        current = Restrictions.and(current, criterion);
                    }
                }
                return current;
            }
        }

        @Override
        public Criterion visit(IsEmpty isEmpty) {
            FieldCondition fieldCondition = isEmpty.getField().accept(visitor);
            if (fieldCondition == null) {
                return TRUE_CRITERION;
            }
            if (fieldCondition.isMany) {
                if (fieldCondition.criterionFieldNames.isEmpty()) {
                    throw new IllegalStateException(
                            "No field name for 'is empty' condition on " + isEmpty.getField()); //$NON-NLS-1$
                } else {
                    // Criterion affect multiple fields
                    Criterion current = null;
                    for (String criterionFieldName : fieldCondition.criterionFieldNames) {
                        if (current == null) {
                            current = Restrictions.isEmpty(criterionFieldName);
                        } else {
                            current = Restrictions.and(current, Restrictions.isEmpty(criterionFieldName));
                        }
                    }
                    return current;
                }
            } else {
                if (fieldCondition.criterionFieldNames.isEmpty()) {
                    throw new IllegalStateException(
                            "No field name for 'is empty' condition on " + isEmpty.getField()); //$NON-NLS-1$
                } else {
                    // Criterion affect multiple fields
                    Criterion current = null;
                    for (String criterionFieldName : fieldCondition.criterionFieldNames) {
                        if (current == null) {
                            current = Restrictions.eq(criterionFieldName, StringUtils.EMPTY);
                        } else {
                            current = Restrictions.and(current,
                                    Restrictions.eq(criterionFieldName, StringUtils.EMPTY));
                        }
                    }
                    return current;
                }
            }
        }

        @Override
        public Criterion visit(NotIsEmpty notIsEmpty) {
            FieldCondition fieldCondition = notIsEmpty.getField().accept(visitor);
            if (fieldCondition == null) {
                return TRUE_CRITERION;
            }
            if (fieldCondition.isMany) {
                if (fieldCondition.criterionFieldNames.isEmpty()) {
                    throw new IllegalStateException(
                            "No field name for 'not is empty' condition on " + notIsEmpty.getField()); //$NON-NLS-1$
                } else {
                    // Criterion affect multiple fields
                    Criterion current = null;
                    for (String criterionFieldName : fieldCondition.criterionFieldNames) {
                        if (current == null) {
                            current = Restrictions.isNotEmpty(criterionFieldName);
                        } else {
                            current = Restrictions.and(current, Restrictions.isNotEmpty(criterionFieldName));
                        }
                    }
                    return current;
                }
            } else {
                if (fieldCondition.criterionFieldNames.isEmpty()) {
                    throw new IllegalStateException(
                            "No field name for 'not is empty' condition on " + notIsEmpty.getField()); //$NON-NLS-1$
                } else {
                    // Criterion affect multiple fields
                    Criterion current = null;
                    for (String criterionFieldName : fieldCondition.criterionFieldNames) {
                        if (current == null) {
                            current = Restrictions.not(Restrictions.eq(criterionFieldName, StringUtils.EMPTY));
                        } else {
                            current = Restrictions.and(current,
                                    Restrictions.not(Restrictions.eq(criterionFieldName, StringUtils.EMPTY)));
                        }
                    }
                    return current;
                }
            }
        }

        @Override
        public Criterion visit(GroupSize groupSize) {
            return Restrictions.sqlRestriction("count()"); //$NON-NLS-1$
        }

        @Override
        public Criterion visit(NotIsNull notIsNull) {
            FieldCondition fieldCondition = notIsNull.getField().accept(visitor);
            if (fieldCondition == null) {
                return TRUE_CRITERION;
            }
            if (fieldCondition.isMany) {
                throw new UnsupportedOperationException("Does not support 'not is null' operation on collections."); //$NON-NLS-1$
            }
            if (fieldCondition.criterionFieldNames.isEmpty()) {
                throw new IllegalStateException(
                        "No field name for 'not is null' condition on " + notIsNull.getField()); //$NON-NLS-1$
            } else {
                // Criterion affect multiple fields
                Criterion current = null;
                for (String criterionFieldName : fieldCondition.criterionFieldNames) {
                    if (current == null) {
                        current = Restrictions.isNotNull(criterionFieldName);
                    } else {
                        current = Restrictions.and(current, Restrictions.isNotNull(criterionFieldName));
                    }
                }
                return current;
            }
        }

        @Override
        public Criterion visit(BinaryLogicOperator condition) {
            Predicate predicate = condition.getPredicate();
            Criterion left = condition.getLeft().accept(this);
            Criterion right = condition.getRight().accept(this);
            if (predicate == Predicate.AND) {
                return and(left, right);
            } else if (predicate == Predicate.OR) {
                return or(left, right);
            } else {
                throw new NotImplementedException("No support for predicate '" + predicate + "'"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        @Override
        public Criterion visit(Range range) {
            FieldCondition condition = range.getExpression().accept(new CriterionFieldCondition());
            TypedExpression expression = range.getExpression();
            Object start;
            Object end;
            String startValue = String.valueOf(range.getStart().accept(VALUE_ADAPTER));
            String endValue = String.valueOf(range.getEnd().accept(VALUE_ADAPTER));
            if (expression instanceof Field) {
                Field field = (Field) expression;
                FieldMetadata fieldMetadata = field.getFieldMetadata();
                start = StorageMetadataUtils.convert(startValue, fieldMetadata);
                end = StorageMetadataUtils.convert(endValue, fieldMetadata);
            } else {
                start = StorageMetadataUtils.convert(startValue, expression.getTypeName());
                end = StorageMetadataUtils.convert(endValue, expression.getTypeName());
            }
            if (condition != null) {
                if (condition.criterionFieldNames.isEmpty()) {
                    throw new IllegalStateException(
                            "No field name for 'range' condition on " + range.getExpression()); //$NON-NLS-1$
                } else {
                    // Criterion affect multiple fields
                    Criterion current = null;
                    for (String criterionFieldName : condition.criterionFieldNames) {
                        if (current == null) {
                            current = Restrictions.between(criterionFieldName, start, end);
                        } else {
                            current = Restrictions.and(current,
                                    Restrictions.between(criterionFieldName, start, end));
                        }
                    }
                    return current;
                }
            } else {
                return null;
            }
        }

        @SuppressWarnings("rawtypes")
        @Override
        public Criterion visit(Compare condition) {
            FieldCondition leftFieldCondition = condition.getLeft().accept(criterionFieldCondition);
            FieldCondition rightFieldCondition = condition.getRight().accept(criterionFieldCondition);
            Predicate predicate = condition.getPredicate();
            if (!leftFieldCondition.isProperty) {
                if (leftFieldCondition.isComputedProperty) {
                    // TODO Assuming that main table alias is "this_" is quite a bold statement.
                    String mainTableAlias = "this_"; //$NON-NLS-1$
                    String mainTableName = resolver.get(mainType);
                    Object value = condition.getRight().accept(VALUE_ADAPTER);
                    String comparator;
                    if (predicate == Predicate.EQUALS) {
                        comparator = "="; //$NON-NLS-1$
                    } else if (predicate == Predicate.GREATER_THAN) {
                        comparator = ">"; //$NON-NLS-1$
                    } else if (predicate == Predicate.GREATER_THAN_OR_EQUALS) {
                        comparator = ">="; //$NON-NLS-1$
                    } else if (predicate == Predicate.LOWER_THAN) {
                        comparator = "<"; //$NON-NLS-1$
                    } else if (predicate == Predicate.LOWER_THAN_OR_EQUALS) {
                        comparator = "<="; //$NON-NLS-1$
                    } else {
                        throw new IllegalArgumentException(
                                "Predicate '" + predicate + "' is not supported on group_size value."); //$NON-NLS-1$ //$NON-NLS-2$
                    }
                    String sqlConditionBuilder = "("; //$NON-NLS-1$
                    sqlConditionBuilder += "select count(1) from"; //$NON-NLS-1$
                    sqlConditionBuilder += ' ' + mainTableName + ' ';
                    sqlConditionBuilder += "where " + Storage.METADATA_TASK_ID + " = " + mainTableAlias + "." //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
                            + Storage.METADATA_TASK_ID;
                    sqlConditionBuilder += ')';
                    sqlConditionBuilder += ' ' + comparator + ' ' + value;
                    return Restrictions.sqlRestriction(sqlConditionBuilder);
                }
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Query on '" + leftFieldCondition //$NON-NLS-1$
                            + "' is not a user set property. Ignore this condition."); //$NON-NLS-1$
                }
                return TRUE_CRITERION;
            }
            if (condition.getLeft() instanceof Field) {
                Field leftField = (Field) condition.getLeft();
                FieldMetadata fieldMetadata = leftField.getFieldMetadata();
                Set<String> aliases = Collections.singleton(fieldMetadata.getContainingType().getName());
                // TODO Ugly code path to fix once test coverage is ok.
                if (leftFieldCondition.position < 0 && (!mainType.equals(fieldMetadata.getContainingType())
                        || fieldMetadata instanceof ReferenceFieldMetadata)) {
                    leftField.accept(StandardQueryHandler.this);
                    aliases = getAliases(mainType, leftField);
                    if (!fieldMetadata.isMany()) {
                        leftFieldCondition.criterionFieldNames.clear();
                        for (String alias : aliases) {
                            if (fieldMetadata instanceof ReferenceFieldMetadata) {
                                // ignored CompoundFieldMetadata
                                if (!(((ReferenceFieldMetadata) fieldMetadata)
                                        .getReferencedField() instanceof CompoundFieldMetadata)) {
                                    leftFieldCondition.criterionFieldNames
                                            .add(alias + '.' + ((ReferenceFieldMetadata) fieldMetadata)
                                                    .getReferencedField().getName());
                                }
                            } else {
                                leftFieldCondition.criterionFieldNames.add(alias + '.' + fieldMetadata.getName());
                            }
                        }
                    }
                }
                if (leftFieldCondition.isMany || rightFieldCondition.isMany) {
                    // This is what could be done with Hibernate 4 for searches that includes conditions on collections:
                    // criteria = criteria.createCriteria(leftFieldCondition.criterionFieldNames);
                    // This is what is done on Hibernate 3.5.6
                    if (criteria instanceof CriteriaImpl) {
                        Iterator iterator = ((CriteriaImpl) criteria).iterateSubcriteria();
                        Criteria typeCheckCriteria = criteria;
                        while (iterator.hasNext()) {
                            Criteria subCriteria = (Criteria) iterator.next();
                            if (aliases.contains(subCriteria.getAlias())) {
                                typeCheckCriteria = subCriteria;
                                break;
                            }
                        }
                        if (leftFieldCondition.position >= 0) {
                            return new ManyFieldCriterion(datasource, typeCheckCriteria, resolver, fieldMetadata,
                                    condition.getRight().accept(VALUE_ADAPTER), leftFieldCondition.position);
                        } else {
                            return new ManyFieldCriterion(datasource, typeCheckCriteria, resolver, fieldMetadata,
                                    condition.getRight().accept(VALUE_ADAPTER));
                        }
                    } else {
                        throw new IllegalStateException(
                                "Expected a criteria instance of " + CriteriaImpl.class.getName() + "."); //$NON-NLS-1$ //$NON-NLS-2$
                    }
                }
            }
            if (!rightFieldCondition.isProperty) { // "Standard" comparison between a field and a constant value.
                Object compareValue = condition.getRight().accept(VALUE_ADAPTER);
                if (condition.getLeft() instanceof Field) {
                    Field leftField = (Field) condition.getLeft();
                    FieldMetadata fieldMetadata = leftField.getFieldMetadata();
                    if (!fieldMetadata.getType().equals(fieldMetadata.getType())) {
                        compareValue = StorageMetadataUtils.convert(String.valueOf(compareValue), fieldMetadata);
                    }
                }
                if (compareValue instanceof Boolean && predicate == Predicate.EQUALS) {
                    if (!(Boolean) compareValue) {
                        // Special case for boolean: when looking for 'false' value, consider null values as 'false'
                        // too.
                        Criterion current = null;
                        for (String criterionFieldName : leftFieldCondition.criterionFieldNames) {
                            if (current == null) {
                                current = or(eq(criterionFieldName, compareValue), isNull(criterionFieldName));
                            } else {
                                current = or(current,
                                        or(eq(criterionFieldName, compareValue), isNull(criterionFieldName)));
                            }
                        }
                        return current;
                    }
                }
                if (predicate == Predicate.EQUALS) {
                    if (compareValue instanceof Object[]) {
                        Field leftField = (Field) condition.getLeft();
                        FieldMetadata fieldMetadata = leftField.getFieldMetadata();
                        Criterion current = null;
                        if (fieldMetadata instanceof ReferenceFieldMetadata) {
                            FieldMetadata referencedField = ((ReferenceFieldMetadata) fieldMetadata)
                                    .getReferencedField();
                            if (!(referencedField instanceof CompoundFieldMetadata)) {
                                throw new IllegalArgumentException("Expected field '" + referencedField //$NON-NLS-1$
                                        + "' to be a composite key."); //$NON-NLS-1$
                            }
                            Set<String> aliases = getAliases(mainType, leftField);
                            current = null;
                            for (String alias : aliases) {
                                FieldMetadata[] fields = ((CompoundFieldMetadata) referencedField).getFields();
                                Object[] keyValues = (Object[]) compareValue;
                                Criterion[] keyValueCriteria = new Criterion[keyValues.length];
                                int i = 0;
                                for (FieldMetadata keyField : fields) {
                                    Object keyValue = StorageMetadataUtils.convert(
                                            StorageMetadataUtils.toString(keyValues[i], keyField), keyField);
                                    keyValueCriteria[i] = eq(alias + '.' + keyField.getName(), keyValue);
                                    i++;
                                }
                                Criterion newCriterion = makeAnd(keyValueCriteria);
                                if (current == null) {
                                    current = newCriterion;
                                } else {
                                    current = or(newCriterion, current);
                                }
                            }
                        } else {
                            Set<String> aliases = getAliases(mainType, leftField);
                            if (aliases.isEmpty()) {
                                throw new IllegalStateException(
                                        "No alias found for field '" + fieldMetadata.getName() + "'."); //$NON-NLS-1$ //$NON-NLS-2$
                            }
                            for (String alias : aliases) {
                                current = Restrictions.in(alias + '.' + fieldMetadata.getName(),
                                        (Object[]) compareValue);
                            }
                        }
                        if (current == null) {
                            throw new IllegalStateException(
                                    "No condition was generated for '" + fieldMetadata.getName() + "'."); //$NON-NLS-1$ //$NON-NLS-2$
                        }
                        return current;
                    } else {
                        Criterion current = null;
                        for (String fieldName : leftFieldCondition.criterionFieldNames) {
                            Criterion newCriterion = eq(fieldName,
                                    applyDatabaseType(leftFieldCondition, compareValue));
                            if (current == null) {
                                current = newCriterion;
                            } else {
                                current = or(newCriterion, current);
                            }
                        }
                        return current;
                    }
                } else if (predicate == Predicate.CONTAINS) {
                    String value = String.valueOf(compareValue);
                    if (value.length() > 2 && value.startsWith("'") && value.endsWith("'")) { //$NON-NLS-1$//$NON-NLS-2$
                        value = value.substring(1, value.length() - 1);
                    }
                    if (!value.isEmpty()) {
                        if (value.charAt(0) != '%') {
                            value = '%' + value;
                        }
                        if (value.charAt(value.length() - 1) != '%') {
                            value += '%';
                        }
                    } else {
                        value = "%"; //$NON-NLS-1$
                    }
                    Object databaseValue = applyDatabaseType(leftFieldCondition, value); // Converts to CLOB if needed
                    if (datasource.isCaseSensitiveSearch() || !(databaseValue instanceof String)) { // Can't use ilike
                                                                                                    // on CLOBs
                        Criterion current = null;
                        for (String fieldName : leftFieldCondition.criterionFieldNames) {
                            Criterion newCriterion = like(fieldName, databaseValue);
                            if (current == null) {
                                current = newCriterion;
                            } else {
                                current = or(newCriterion, current);
                            }
                        }
                        return current;
                    } else {
                        Criterion current = null;
                        for (String fieldName : leftFieldCondition.criterionFieldNames) {
                            Criterion newCriterion = ilike(fieldName, databaseValue);
                            if (current == null) {
                                current = newCriterion;
                            } else {
                                current = or(newCriterion, current);
                            }
                        }
                        return current;
                    }
                } else if (predicate == Predicate.STARTS_WITH) {
                    Object value = applyDatabaseType(leftFieldCondition, compareValue + "%"); //$NON-NLS-1$
                    if (datasource.isCaseSensitiveSearch() || !(value instanceof String)) {
                        Criterion current = null;
                        for (String fieldName : leftFieldCondition.criterionFieldNames) {
                            Criterion newCriterion = like(fieldName, value);
                            if (current == null) {
                                current = newCriterion;
                            } else {
                                current = or(newCriterion, current);
                            }
                        }
                        return current;
                    } else {
                        Criterion current = null;
                        for (String fieldName : leftFieldCondition.criterionFieldNames) {
                            Criterion newCriterion = ilike(fieldName, value);
                            if (current == null) {
                                current = newCriterion;
                            } else {
                                current = or(newCriterion, current);
                            }
                        }
                        return current;
                    }
                } else if (predicate == Predicate.GREATER_THAN) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = gt(fieldName, compareValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.LOWER_THAN) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = lt(fieldName, compareValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.GREATER_THAN_OR_EQUALS) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = ge(fieldName, compareValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.LOWER_THAN_OR_EQUALS) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = le(fieldName, compareValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else {
                    throw new NotImplementedException("No support for predicate '" + predicate.getClass() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
                }
            } else { // Since we expect left part to be a field, this 'else' means we're comparing 2 fields
                if (rightFieldCondition.criterionFieldNames.size() > 1) {
                    throw new UnsupportedOperationException("Can't compare to multiple right fields (was " //$NON-NLS-1$
                            + rightFieldCondition.criterionFieldNames.size() + ")."); //$NON-NLS-1$
                }
                String rightValue = rightFieldCondition.criterionFieldNames.get(0);
                if (predicate == Predicate.EQUALS) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = Restrictions.eqProperty(fieldName, rightValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.GREATER_THAN) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = Restrictions.gtProperty(fieldName, rightValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.LOWER_THAN) {
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = Restrictions.ltProperty(fieldName, rightValue);
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.GREATER_THAN_OR_EQUALS) {
                    // No GTE for properties, do it "manually"
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = or(Restrictions.gtProperty(fieldName, rightValue),
                                Restrictions.eqProperty(fieldName, rightValue));
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else if (predicate == Predicate.LOWER_THAN_OR_EQUALS) {
                    // No LTE for properties, do it "manually"
                    Criterion current = null;
                    for (String fieldName : leftFieldCondition.criterionFieldNames) {
                        Criterion newCriterion = or(Restrictions.ltProperty(fieldName, rightValue),
                                Restrictions.eqProperty(fieldName, rightValue));
                        if (current == null) {
                            current = newCriterion;
                        } else {
                            current = or(newCriterion, current);
                        }
                    }
                    return current;
                } else {
                    throw new NotImplementedException("No support for predicate '" + predicate.getClass() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
                }
            }
        }
    }

    private Object applyDatabaseType(FieldCondition field, Object value) {
        if (field.fieldMetadata != null
                && TypeMapping.SQL_TYPE_CLOB.equals(field.fieldMetadata.getType().getData(TypeMapping.SQL_TYPE))) {
            return Hibernate.getLobCreator(session).createClob(String.valueOf(value));
        }
        return value;
    }

    @SuppressWarnings("rawtypes")
    public static Criteria findCriteria(Criteria mainCriteria, Set<String> aliases) {
        if (aliases.contains(mainCriteria.getAlias())) {
            return mainCriteria;
        }
        if (mainCriteria instanceof CriteriaImpl) {
            Criteria foundSubCriteria = null;
            Iterator iterator = ((CriteriaImpl) mainCriteria).iterateSubcriteria();
            while (iterator.hasNext()) {
                Criteria subCriteria = (Criteria) iterator.next();
                if (aliases.contains(subCriteria.getAlias())) {
                    foundSubCriteria = subCriteria;
                    break;
                }
            }
            if (foundSubCriteria == null) {
                throw new IllegalStateException("Could not find criteria for type check."); //$NON-NLS-1$
            }
            return foundSubCriteria;
        } else {
            throw new IllegalStateException(
                    "Expected a criteria instance of " + CriteriaImpl.class.getName() + "."); //$NON-NLS-1$ //$NON-NLS-2$
        }
    }

    private static Criterion makeAnd(Criterion... criterions) {
        if (criterions.length == 1) {
            return criterions[0];
        }
        Criterion current = TRUE_CRITERION;
        for (Criterion cri : criterions) {
            current = Restrictions.and(current, cri);
        }
        return current;
    }

    private void addCondition(FieldCondition condition, String alias, FieldMetadata fieldMetadata) {
        if (fieldMetadata instanceof CompoundFieldMetadata) {
            FieldMetadata[] fields = ((CompoundFieldMetadata) fieldMetadata).getFields();
            for (FieldMetadata subFieldMetadata : fields) {
                condition.criterionFieldNames.add(alias + '.' + subFieldMetadata.getName());
            }
        } else if (fieldMetadata instanceof ReferenceFieldMetadata
                && mainType.equals(fieldMetadata.getContainingType())) {
            condition.criterionFieldNames.add(getFieldName(fieldMetadata, true));
        } else {
            condition.criterionFieldNames.add(alias + '.' + fieldMetadata.getName());
        }
    }

    private void addCondition(FieldCondition condition, FieldMetadata fieldMetadata) {
        if (fieldMetadata instanceof CompoundFieldMetadata) {
            FieldMetadata[] fields = ((CompoundFieldMetadata) fieldMetadata).getFields();
            for (FieldMetadata subFieldMetadata : fields) {
                condition.criterionFieldNames.add(getFieldName(subFieldMetadata, true));
            }
        } else {
            condition.criterionFieldNames.add(getFieldName(fieldMetadata, true));
        }
    }

    private class CriterionFieldCondition extends VisitorAdapter<FieldCondition> {

        private FieldCondition createInternalCondition(String fieldName) {
            FieldCondition condition = new FieldCondition();
            condition.criterionFieldNames.add(fieldName);
            condition.isMany = false;
            condition.isProperty = true;
            return condition;
        }

        private FieldCondition createConstantCondition() {
            FieldCondition condition = new FieldCondition();
            condition.isProperty = false;
            condition.isMany = false;
            condition.criterionFieldNames.clear();
            return condition;
        }

        @Override
        public FieldCondition visit(Count count) {
            FieldCondition fieldCondition = new FieldCondition();
            fieldCondition.isProperty = false;
            fieldCondition.isComputedProperty = true;
            return fieldCondition;
        }

        @Override
        public FieldCondition visit(Timestamp timestamp) {
            String databaseTimestamp = mappings.getMappingFromDatabase(mainType).getDatabaseTimestamp();
            if (databaseTimestamp != null) {
                return createInternalCondition(databaseTimestamp);
            } else {
                return null;
            }
        }

        @Override
        public FieldCondition visit(TaskId taskId) {
            String taskIdField = mappings.getMappingFromDatabase(mainType).getDatabaseTaskId();
            if (taskIdField != null) {
                return createInternalCondition(Storage.METADATA_TASK_ID);
            } else {
                return null;
            }
        }

        @Override
        public FieldCondition visit(GroupSize groupSize) {
            FieldCondition fieldCondition = new FieldCondition();
            fieldCondition.isProperty = false;
            fieldCondition.isComputedProperty = true;
            return fieldCondition;
        }

        @Override
        public FieldCondition visit(StagingStatus stagingStatus) {
            return createInternalCondition(Storage.METADATA_STAGING_STATUS);
        }

        @Override
        public FieldCondition visit(StagingError stagingError) {
            return createInternalCondition(Storage.METADATA_STAGING_ERROR);
        }

        @Override
        public FieldCondition visit(StagingSource stagingSource) {
            return createInternalCondition(Storage.METADATA_STAGING_SOURCE);
        }

        @Override
        public FieldCondition visit(StagingBlockKey stagingBlockKey) {
            return createInternalCondition(Storage.METADATA_STAGING_BLOCK_KEY);
        }

        @Override
        public FieldCondition visit(Expression expression) {
            if (expression instanceof ComplexTypeExpression) {
                return createConstantCondition();
            } else {
                return super.visit(expression);
            }
        }

        @Override
        public FieldCondition visit(Alias alias) {
            return alias.getTypedExpression().accept(this);
        }

        @Override
        public FieldCondition visit(Field field) {
            FieldCondition condition = new FieldCondition();
            condition.isMany = field.getFieldMetadata().isMany();
            // Use line below to allow searches on collection fields (but Hibernate 4 should be used).
            // condition.criterionFieldNames = field.getFieldMetadata().isMany() ? "elements" : getFieldName(field,
            // StandardQueryHandler.this.mappingMetadataRepository);
            Set<String> aliases = getAliases(mainType, field);
            if (aliases.size() > 0) {
                for (String alias : aliases) {
                    List<FieldMetadata> path = field.getPath();
                    if (path.size() > 1) {
                        // For path with more than 1 element, the alias for the criterion is the *containing* one(s).
                        String containerAlias = pathToAlias.get(path.get(path.size() - 2).getPath());
                        addCondition(condition, containerAlias, field.getFieldMetadata());
                    } else {
                        // For path with size 1, code did not generate an alias for field and returned containing alias.
                        addCondition(condition, alias, field.getFieldMetadata());
                    }
                }
            } else {
                addCondition(condition, field.getFieldMetadata());
            }
            condition.fieldMetadata = field.getFieldMetadata();
            condition.field = field;
            condition.isProperty = true;
            return condition;
        }

        @Override
        public FieldCondition visit(IndexedField indexedField) {
            FieldCondition condition = new FieldCondition();
            condition.isMany = indexedField.getFieldMetadata().isMany();
            // Use line below to allow searches on collection fields (but Hibernate 4 should be used).
            // condition.criterionFieldNames = field.getFieldMetadata().isMany() ? "elements" : getFieldName(field,
            // StandardQueryHandler.this.mappingMetadataRepository);
            condition.criterionFieldNames.add(getFieldName(indexedField));
            condition.isProperty = true;
            condition.position = indexedField.getPosition();
            return condition;
        }

        @Override
        public FieldCondition visit(Id id) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(ConstantCollection collection) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(StringConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(IntegerConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(DateConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(DateTimeConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(BooleanConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(BigDecimalConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(TimeConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(ShortConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(ByteConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(LongConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(DoubleConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(FloatConstant constant) {
            return createConstantCondition();
        }

        @Override
        public FieldCondition visit(ComplexTypeExpression expression) {
            FieldCondition fieldCondition = new FieldCondition();
            fieldCondition.criterionFieldNames.clear();
            fieldCondition.isMany = false;
            fieldCondition.isProperty = true;
            return fieldCondition;
        }

        @Override
        public FieldCondition visit(Type type) {
            FieldCondition fieldCondition = new FieldCondition();
            Field field = type.getField();
            FieldMetadata fieldMetadata = field.getFieldMetadata();
            Set<String> aliases = getAliases(fieldMetadata.getContainingType(), field);
            for (String alias : aliases) {
                fieldCondition.criterionFieldNames.add(alias + ".class"); //$NON-NLS-1$
            }
            fieldCondition.isMany = false;
            fieldCondition.isProperty = true;
            return fieldCondition;
        }
    }
}