org.hibernate.search.query.engine.impl.HSQueryImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.hibernate.search.query.engine.impl.HSQueryImpl.java

Source

/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * JBoss, Home of Professional Open Source
 * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @authors tag. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */
package org.hibernate.search.query.engine.impl;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Similarity;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;

import org.hibernate.annotations.common.AssertionFailure;
import org.hibernate.search.FullTextFilter;
import org.hibernate.search.SearchException;
import org.hibernate.search.annotations.FieldCacheType;
import org.hibernate.search.engine.DocumentBuilder;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.engine.spi.EntityIndexBinder;
import org.hibernate.search.engine.impl.FilterDef;
import org.hibernate.search.engine.spi.SearchFactoryImplementor;
import org.hibernate.search.filter.StandardFilterKey;
import org.hibernate.search.filter.impl.ChainedFilter;
import org.hibernate.search.filter.FilterKey;
import org.hibernate.search.filter.FullTextFilterImplementor;
import org.hibernate.search.filter.ShardSensitiveOnlyFilter;
import org.hibernate.search.filter.impl.CachingWrapperFilter;
import org.hibernate.search.filter.impl.FullTextFilterImpl;
import org.hibernate.search.indexes.spi.IndexManager;
import org.hibernate.search.query.collector.impl.FieldCacheCollectorFactory;
import org.hibernate.search.query.engine.QueryTimeoutException;
import org.hibernate.search.query.engine.spi.DocumentExtractor;
import org.hibernate.search.query.engine.spi.EntityInfo;
import org.hibernate.search.query.engine.spi.HSQuery;
import org.hibernate.search.query.engine.spi.TimeoutExceptionFactory;
import org.hibernate.search.query.engine.spi.TimeoutManager;
import org.hibernate.search.reader.impl.MultiReaderFactory;
import org.hibernate.search.store.IndexShardingStrategy;

import static org.hibernate.search.util.impl.CollectionHelper.newHashMap;
import static org.hibernate.search.util.impl.FilterCacheModeTypeHelper.cacheInstance;
import static org.hibernate.search.util.impl.FilterCacheModeTypeHelper.cacheResults;

/**
 * @author Emmanuel Bernard <emmanuel@hibernate.org>
 * @author Hardy Ferentschik <hardy@hibernate.org>
 */
public class HSQueryImpl implements HSQuery, Serializable {

    private static final FullTextFilterImplementor[] EMPTY_FULL_TEXT_FILTER_IMPLEMENTOR = new FullTextFilterImplementor[0];

    private transient SearchFactoryImplementor searchFactoryImplementor;
    private Query luceneQuery;
    private List<Class<?>> targetedEntities;
    private transient TimeoutManagerImpl timeoutManager;
    private Set<Class<?>> indexedTargetedEntities;
    private boolean allowFieldSelectionInProjection = true;
    /**
     * The  map of currently active/enabled filters.
     */
    private final Map<String, FullTextFilterImpl> filterDefinitions = newHashMap();
    private Filter filter;
    private Filter userFilter;
    private Sort sort;
    private String[] projectedFields;
    private int firstResult;
    private Integer maxResults;
    private transient Set<Class<?>> classesAndSubclasses;
    //optimization: if we can avoid the filter clause (we can most of the time) do it as it has a significant perf impact
    private boolean needClassFilterClause;
    private Set<String> idFieldNames;
    private transient TimeoutExceptionFactory timeoutExceptionFactory = QueryTimeoutException.DEFAULT_TIMEOUT_EXCEPTION_FACTORY;
    private boolean useFieldCacheOnClassTypes = false;
    private transient FacetManagerImpl facetManager;

    /**
     * The number of results for this query. This field gets populated once {@link #queryResultSize}, {@link #queryEntityInfos}
     * or {@link #queryDocumentExtractor} is called.
     */
    private Integer resultSize;

    public HSQueryImpl(SearchFactoryImplementor searchFactoryImplementor) {
        this.searchFactoryImplementor = searchFactoryImplementor;
    }

    public void afterDeserialise(SearchFactoryImplementor searchFactoryImplementor) {
        this.searchFactoryImplementor = searchFactoryImplementor;
    }

    public HSQuery luceneQuery(Query query) {
        clearCachedResults();
        this.luceneQuery = query;
        return this;
    }

    public HSQuery targetedEntities(List<Class<?>> classes) {
        clearCachedResults();
        this.targetedEntities = classes == null ? new ArrayList<Class<?>>(0) : new ArrayList<Class<?>>(classes);
        final Class[] classesAsArray = targetedEntities.toArray(new Class[targetedEntities.size()]);
        this.indexedTargetedEntities = searchFactoryImplementor.getIndexedTypesPolymorphic(classesAsArray);
        if (targetedEntities != null && targetedEntities.size() > 0 && indexedTargetedEntities.size() == 0) {
            String msg = "None of the specified entity types or any of their subclasses are indexed.";
            throw new IllegalArgumentException(msg);
        }
        return this;
    }

    public HSQuery sort(Sort sort) {
        this.sort = sort;
        return this;
    }

    public HSQuery filter(Filter filter) {
        clearCachedResults();
        this.userFilter = filter;
        return this;
    }

    public HSQuery timeoutExceptionFactory(TimeoutExceptionFactory exceptionFactory) {
        this.timeoutExceptionFactory = exceptionFactory;
        return this;
    }

    public HSQuery projection(String... fields) {
        if (fields == null || fields.length == 0) {
            this.projectedFields = null;
        } else {
            this.projectedFields = fields;
        }
        return this;
    }

    public HSQuery firstResult(int firstResult) {
        if (firstResult < 0) {
            throw new IllegalArgumentException("'first' pagination parameter less than 0");
        }
        this.firstResult = firstResult;
        return this;
    }

    public HSQuery maxResults(Integer maxResults) {
        if (maxResults != null && maxResults < 0) {
            throw new IllegalArgumentException("'max' pagination parameter less than 0");
        }
        this.maxResults = maxResults;
        return this;
    }

    /**
     * List of targeted entities as described by the user
     */
    public List<Class<?>> getTargetedEntities() {
        return targetedEntities;
    }

    /**
     * Set of indexed entities corresponding to the class hierarchy of the targeted entities
     */
    public Set<Class<?>> getIndexedTargetedEntities() {
        return indexedTargetedEntities;
    }

    public String[] getProjectedFields() {
        return projectedFields;
    }

    private TimeoutManagerImpl getTimeoutManagerImpl() {
        if (timeoutManager == null) {
            if (luceneQuery == null) {
                throw new AssertionFailure("Requesting TimeoutManager before setting luceneQuery()");
            }
            timeoutManager = new TimeoutManagerImpl(luceneQuery, timeoutExceptionFactory);
        }
        return timeoutManager;
    }

    public TimeoutManager getTimeoutManager() {
        return getTimeoutManagerImpl();
    }

    public FacetManagerImpl getFacetManager() {
        if (facetManager == null) {
            facetManager = new FacetManagerImpl(this);
        }
        return facetManager;
    }

    public Query getLuceneQuery() {
        return luceneQuery;
    }

    public List<EntityInfo> queryEntityInfos() {
        IndexSearcherWithPayload searcher = buildSearcher();
        if (searcher == null) {
            return Collections.emptyList();
        }
        try {
            QueryHits queryHits = getQueryHits(searcher, calculateTopDocsRetrievalSize());
            int first = getFirstResultIndex();
            int max = max(first, queryHits.getTotalHits());

            int size = max - first + 1 < 0 ? 0 : max - first + 1;
            List<EntityInfo> infos = new ArrayList<EntityInfo>(size);
            DocumentExtractor extractor = buildDocumentExtractor(searcher, queryHits, first, max);
            for (int index = first; index <= max; index++) {
                infos.add(extractor.extract(index));
                //TODO should we measure on each extractor?
                if (index % 10 == 0) {
                    getTimeoutManager().isTimedOut();
                }
            }
            return infos;
        } catch (IOException e) {
            throw new SearchException("Unable to query Lucene index", e);
        } finally {
            closeSearcher(searcher);
        }
    }

    private DocumentExtractor buildDocumentExtractor(IndexSearcherWithPayload searcher, QueryHits queryHits,
            int first, int max) {
        return new DocumentExtractorImpl(queryHits, searchFactoryImplementor, projectedFields, idFieldNames,
                allowFieldSelectionInProjection, searcher, luceneQuery, first, max, classesAndSubclasses);
    }

    /**
     * DocumentExtractor returns a traverser over the full-text results (EntityInfo)
     * This operation is lazy bound:
     * - the query is executed
     * - results are not retrieved until actually requested
     *
     * DocumentExtractor objects *must* be closed when the results are no longer traversed.
     */
    public DocumentExtractor queryDocumentExtractor() {
        //keep the searcher open until the resultset is closed
        //find the directories
        IndexSearcherWithPayload openSearcher = buildSearcher();
        //FIXME: handle null searcher
        try {
            QueryHits queryHits = getQueryHits(openSearcher, calculateTopDocsRetrievalSize());
            int first = getFirstResultIndex();
            int max = max(first, queryHits.getTotalHits());
            return buildDocumentExtractor(openSearcher, queryHits, first, max);
        } catch (IOException e) {
            closeSearcher(openSearcher);
            throw new SearchException("Unable to query Lucene index", e);
        }
    }

    public int queryResultSize() {
        if (resultSize == null) {
            //the timeoutManager does not need to be stopped nor reset as a start does indeed reset
            getTimeoutManager().start();
            //get result size without object initialization
            IndexSearcherWithPayload searcher = buildSearcher(searchFactoryImplementor, false);
            if (searcher == null) {
                resultSize = 0;
            } else {
                try {
                    QueryHits queryHits = getQueryHits(searcher, 0);
                    resultSize = queryHits.getTotalHits();
                } catch (IOException e) {
                    throw new SearchException("Unable to query Lucene index", e);
                } finally {
                    closeSearcher(searcher);
                }
            }
        }
        return this.resultSize;
    }

    public Explanation explain(int documentId) {
        //don't use TimeoutManager here as explain is a dev tool when things are weird... or slow :)
        Explanation explanation = null;
        IndexSearcherWithPayload searcher = buildSearcher(searchFactoryImplementor, true);
        if (searcher == null) {
            throw new SearchException(
                    "Unable to build explanation for document id:" + documentId + ". no index found");
        }
        try {
            org.apache.lucene.search.Query filteredQuery = filterQueryByClasses(luceneQuery);
            buildFilters();
            explanation = searcher.getSearcher().explain(filteredQuery, documentId);
        } catch (IOException e) {
            throw new SearchException("Unable to query Lucene index and build explanation", e);
        } finally {
            closeSearcher(searcher);
        }
        return explanation;
    }

    public FullTextFilter enableFullTextFilter(String name) {
        clearCachedResults();
        FullTextFilterImpl filterDefinition = filterDefinitions.get(name);
        if (filterDefinition != null) {
            return filterDefinition;
        }

        filterDefinition = new FullTextFilterImpl();
        filterDefinition.setName(name);
        FilterDef filterDef = searchFactoryImplementor.getFilterDefinition(name);
        if (filterDef == null) {
            throw new SearchException("Unknown @FullTextFilter: " + name);
        }
        filterDefinitions.put(name, filterDefinition);
        return filterDefinition;
    }

    public void disableFullTextFilter(String name) {
        clearCachedResults();
        filterDefinitions.remove(name);
    }

    private void closeSearcher(IndexSearcherWithPayload searcherWithPayload) {
        if (searcherWithPayload == null) {
            return;
        }
        searcherWithPayload.closeSearcher(luceneQuery, searchFactoryImplementor);
    }

    /**
     * This class caches some of the query results and we need to reset the state in case something in the query
     * changes (eg a new filter is set).
     */
    void clearCachedResults() {
        resultSize = null;
    }

    /**
     * Execute the lucene search and return the matching hits.
     *
     * @param searcher The index searcher.
     * @param n Number of documents to retrieve
     *
     * @return An instance of <code>QueryHits</code> wrapping the Lucene query and the matching documents.
     *
     * @throws IOException in case there is an error executing the lucene search.
     */
    private QueryHits getQueryHits(IndexSearcherWithPayload searcher, Integer n) throws IOException {
        org.apache.lucene.search.Query filteredQuery = filterQueryByClasses(luceneQuery);
        buildFilters();
        QueryHits queryHits;

        boolean stats = searchFactoryImplementor.getStatistics().isStatisticsEnabled();
        long startTime = 0;
        if (stats) {
            startTime = System.nanoTime();
        }

        if (n == null) { // try to make sure that we get the right amount of top docs
            queryHits = new QueryHits(searcher, filteredQuery, filter, sort, getTimeoutManagerImpl(),
                    facetManager.getFacetRequests(), useFieldCacheOnTypes(),
                    getAppropriateIdFieldCollectorFactory());
        } else if (0 == n) {
            queryHits = new QueryHits(searcher, filteredQuery, filter, null, 0, getTimeoutManagerImpl(), null,
                    false, null);
        } else {
            queryHits = new QueryHits(searcher, filteredQuery, filter, sort, n, getTimeoutManagerImpl(),
                    facetManager.getFacetRequests(), useFieldCacheOnTypes(),
                    getAppropriateIdFieldCollectorFactory());
        }
        resultSize = queryHits.getTotalHits();

        if (stats) {
            searchFactoryImplementor.getStatisticsImplementor().searchExecuted(filteredQuery.toString(),
                    System.nanoTime() - startTime);
        }
        facetManager.setFacetResults(queryHits.getFacets());
        return queryHits;
    }

    /**
     * @return Calculates the number of <code>TopDocs</code> which should be retrieved as part of the query. If Hibernate's
     *         pagination parameters are set returned value is <code>first + maxResults</code>. Otherwise <code>null</code> is
     *         returned.
     */
    private Integer calculateTopDocsRetrievalSize() {
        if (maxResults == null) {
            return null;
        } else {
            long tmpMaxResult = (long) getFirstResultIndex() + maxResults;
            if (tmpMaxResult >= Integer.MAX_VALUE) {
                // don't return just Integer.MAX_VALUE due to a bug in Lucene - see HSEARCH-330
                return Integer.MAX_VALUE - 1;
            }
            if (tmpMaxResult == 0) {
                return 1; // Lucene enforces that at least one top doc will be retrieved. See also HSEARCH-604
            } else {
                return (int) tmpMaxResult;
            }
        }
    }

    private int getFirstResultIndex() {
        return firstResult;
    }

    private IndexSearcherWithPayload buildSearcher() {
        return buildSearcher(searchFactoryImplementor, null);
    }

    /**
     * Build the index searcher for this fulltext query.
     *
     * @param searchFactoryImplementor the search factory.
     * @param forceScoring if true, force SCORE computation, if false, force not to compute score, if null used best choice
     *
     * @return the <code>IndexSearcher</code> for this query (can be <code>null</code>.
     *         TODO change classesAndSubclasses by side effect, which is a mismatch with the Searcher return, fix that.
     */
    private IndexSearcherWithPayload buildSearcher(SearchFactoryImplementor searchFactoryImplementor,
            Boolean forceScoring) {
        Map<Class<?>, EntityIndexBinder<?>> builders = searchFactoryImplementor.getIndexBindingForEntity();
        List<IndexManager> targetedIndexes = new ArrayList<IndexManager>();
        Set<String> idFieldNames = new HashSet<String>();

        Similarity searcherSimilarity = null;
        //TODO check if caching this work for the last n list of indexedTargetedEntities makes a perf boost
        if (indexedTargetedEntities.size() == 0) {
            // empty indexedTargetedEntities array means search over all indexed entities,
            // but we have to make sure there is at least one
            if (builders.isEmpty()) {
                throw new SearchException(
                        "There are no mapped entities. Don't forget to add @Indexed to at least one class.");
            }

            for (EntityIndexBinder indexBinder : builders.values()) {
                DocumentBuilderIndexedEntity<?> builder = indexBinder.getDocumentBuilder();
                searcherSimilarity = checkSimilarity(searcherSimilarity, builder);
                if (builder.getIdKeywordName() != null) {
                    idFieldNames.add(builder.getIdKeywordName());
                    allowFieldSelectionInProjection = allowFieldSelectionInProjection
                            && builder.allowFieldSelectionInProjection();
                }
                useFieldCacheOnClassTypes = useFieldCacheOnClassTypes
                        || builder.getFieldCacheOption().contains(FieldCacheType.CLASS);
                populateIndexManagers(targetedIndexes, indexBinder.getSelectionStrategy());
            }
            classesAndSubclasses = null;
        } else {
            Set<Class<?>> involvedClasses = new HashSet<Class<?>>(indexedTargetedEntities.size());
            involvedClasses.addAll(indexedTargetedEntities);
            for (Class<?> clazz : indexedTargetedEntities) {
                EntityIndexBinder<?> indexBinder = builders.get(clazz);
                if (indexBinder != null) {
                    DocumentBuilderIndexedEntity<?> builder = indexBinder.getDocumentBuilder();
                    involvedClasses.addAll(builder.getMappedSubclasses());
                }
            }

            for (Class clazz : involvedClasses) {
                EntityIndexBinder indexBinder = builders.get(clazz);
                //TODO should we rather choose a polymorphic path and allow non mapped entities
                if (indexBinder == null) {
                    throw new SearchException("Not a mapped entity (don't forget to add @Indexed): " + clazz);
                }
                DocumentBuilderIndexedEntity<?> builder = indexBinder.getDocumentBuilder();
                if (builder.getIdKeywordName() != null) {
                    idFieldNames.add(builder.getIdKeywordName());
                    allowFieldSelectionInProjection = allowFieldSelectionInProjection
                            && builder.allowFieldSelectionInProjection();
                }
                searcherSimilarity = checkSimilarity(searcherSimilarity, builder);
                useFieldCacheOnClassTypes = useFieldCacheOnClassTypes
                        || builder.getFieldCacheOption().contains(FieldCacheType.CLASS);
                populateIndexManagers(targetedIndexes, indexBinder.getSelectionStrategy());
            }
            this.classesAndSubclasses = involvedClasses;
        }
        this.idFieldNames = idFieldNames;

        //compute optimization needClassFilterClause
        //if at least one DP contains one class that is not part of the targeted classesAndSubclasses we can't optimize
        if (classesAndSubclasses != null) {
            for (IndexManager indexManager : targetedIndexes) {
                final Set<Class<?>> classesInIndexManager = indexManager.getContainedTypes();
                // if an IndexManager contains only one class, we know for sure it's part of classesAndSubclasses
                if (classesInIndexManager.size() > 1) {
                    //risk of needClassFilterClause
                    for (Class clazz : classesInIndexManager) {
                        if (!classesAndSubclasses.contains(clazz)) {
                            this.needClassFilterClause = true;
                            break;
                        }
                    }
                }
                if (this.needClassFilterClause) {
                    break;
                }
            }
        } else {
            Map<Class<?>, EntityIndexBinder<?>> documentBuildersIndexedEntities = searchFactoryImplementor
                    .getIndexBindingForEntity();
            this.classesAndSubclasses = documentBuildersIndexedEntities.keySet();
        }

        //set up the searcher
        final IndexManager[] indexManagers = targetedIndexes.toArray(new IndexManager[targetedIndexes.size()]);
        IndexSearcher is = new IndexSearcher(MultiReaderFactory.openReader(indexManagers));
        is.setSimilarity(searcherSimilarity);

        //handle the sort and projection
        final String[] projection = this.projectedFields;
        if (Boolean.TRUE.equals(forceScoring)) {
            return new IndexSearcherWithPayload(is, true, true);
        } else if (Boolean.FALSE.equals(forceScoring)) {
            return new IndexSearcherWithPayload(is, false, false);
        } else if (this.sort != null && projection != null) {
            boolean activate = false;
            for (String field : projection) {
                if (SCORE.equals(field)) {
                    activate = true;
                    break;
                }
            }
            if (activate) {
                return new IndexSearcherWithPayload(is, true, false);
            }
        }
        //default
        return new IndexSearcherWithPayload(is, false, false);
    }

    private Similarity checkSimilarity(Similarity similarity, DocumentBuilderIndexedEntity builder) {
        if (similarity == null) {
            similarity = builder.getSimilarity();
        } else if (!similarity.getClass().equals(builder.getSimilarity().getClass())) {
            throw new SearchException(
                    "Cannot perform search on two entities with differing Similarity implementations ("
                            + similarity.getClass().getName() + " & " + builder.getSimilarity().getClass().getName()
                            + ")");
        }

        return similarity;
    }

    private void populateIndexManagers(List<IndexManager> indexManagersTarget,
            final IndexShardingStrategy indexShardingStrategy) {
        final IndexManager[] indexManagersForQuery;
        if (filterDefinitions != null && !filterDefinitions.isEmpty()) {
            indexManagersForQuery = indexShardingStrategy.getIndexManagersForQuery(
                    filterDefinitions.values().toArray(new FullTextFilterImplementor[filterDefinitions.size()]));
        } else {
            //no filter get all shards
            indexManagersForQuery = indexShardingStrategy
                    .getIndexManagersForQuery(EMPTY_FULL_TEXT_FILTER_IMPLEMENTOR);
        }

        for (IndexManager indexManager : indexManagersForQuery) {
            if (!indexManagersTarget.contains(indexManager)) {
                indexManagersTarget.add(indexManager);
            }
        }
    }

    private void buildFilters() {
        ChainedFilter chainedFilter = new ChainedFilter();
        if (!filterDefinitions.isEmpty()) {
            for (FullTextFilterImpl fullTextFilter : filterDefinitions.values()) {
                Filter filter = buildLuceneFilter(fullTextFilter);
                if (filter != null) {
                    chainedFilter.addFilter(filter);
                }
            }
        }

        if (userFilter != null) {
            chainedFilter.addFilter(userFilter);
        }

        if (getFacetManager().getFacetFilter() != null) {
            chainedFilter.addFilter(facetManager.getFacetFilter());
        }

        if (chainedFilter.isEmpty()) {
            filter = null;
        } else {
            filter = chainedFilter;
        }
    }

    /**
     * Builds a Lucene filter using the given <code>FullTextFilter</code>.
     *
     * @param fullTextFilter the Hibernate specific <code>FullTextFilter</code> used to create the
     * Lucene <code>Filter</code>.
     *
     * @return the Lucene filter mapped to the filter definition
     */
    private Filter buildLuceneFilter(FullTextFilterImpl fullTextFilter) {

        /*
         * FilterKey implementations and Filter(Factory) do not have to be threadsafe wrt their parameter injection
         * as FilterCachingStrategy ensure a memory barrier between concurrent thread calls
         */
        FilterDef def = searchFactoryImplementor.getFilterDefinition(fullTextFilter.getName());
        //def can never be null, ti's guarded by enableFullTextFilter(String)

        if (isPreQueryFilterOnly(def)) {
            return null;
        }

        Object instance = createFilterInstance(fullTextFilter, def);
        FilterKey key = createFilterKey(def, instance);

        // try to get the filter out of the cache
        Filter filter = cacheInstance(def.getCacheMode())
                ? searchFactoryImplementor.getFilterCachingStrategy().getCachedFilter(key)
                : null;

        if (filter == null) {
            filter = createFilter(def, instance);

            // add filter to cache if we have to
            if (cacheInstance(def.getCacheMode())) {
                searchFactoryImplementor.getFilterCachingStrategy().addCachedFilter(key, filter);
            }
        }
        return filter;
    }

    private boolean isPreQueryFilterOnly(FilterDef def) {
        return def.getImpl().equals(ShardSensitiveOnlyFilter.class);
    }

    private Filter createFilter(FilterDef def, Object instance) {
        Filter filter;
        if (def.getFactoryMethod() != null) {
            try {
                filter = (Filter) def.getFactoryMethod().invoke(instance);
            } catch (IllegalAccessException e) {
                throw new SearchException("Unable to access @Factory method: " + def.getImpl().getName() + "."
                        + def.getFactoryMethod().getName(), e);
            } catch (InvocationTargetException e) {
                throw new SearchException("Unable to access @Factory method: " + def.getImpl().getName() + "."
                        + def.getFactoryMethod().getName(), e);
            } catch (ClassCastException e) {
                throw new SearchException("@Key method does not return a org.apache.lucene.search.Filter class: "
                        + def.getImpl().getName() + "." + def.getFactoryMethod().getName(), e);
            }
        } else {
            try {
                filter = (Filter) instance;
            } catch (ClassCastException e) {
                throw new SearchException(
                        "Filter implementation does not implement the Filter interface: " + def.getImpl().getName()
                                + ". " + (def.getFactoryMethod() != null ? def.getFactoryMethod().getName() : ""),
                        e);
            }
        }

        filter = addCachingWrapperFilter(filter, def);
        return filter;
    }

    /**
     * Decides whether to wrap the given filter around a <code>CachingWrapperFilter<code>.
     *
     * @param filter the filter which maybe gets wrapped.
     * @param def The filter definition used to decide whether wrapping should occur or not.
     *
     * @return The original filter or wrapped filter depending on the information extracted from
     *         <code>def</code>.
     */
    private Filter addCachingWrapperFilter(Filter filter, FilterDef def) {
        if (cacheResults(def.getCacheMode())) {
            int cachingWrapperFilterSize = searchFactoryImplementor.getFilterCacheBitResultsSize();
            filter = new CachingWrapperFilter(filter, cachingWrapperFilterSize);
        }

        return filter;
    }

    private FilterKey createFilterKey(FilterDef def, Object instance) {
        FilterKey key = null;
        if (!cacheInstance(def.getCacheMode())) {
            return key; // if the filter is not cached there is no key!
        }

        if (def.getKeyMethod() == null) {
            key = new FilterKey() {
                public int hashCode() {
                    return getImpl().hashCode();
                }

                public boolean equals(Object obj) {
                    if (!(obj instanceof FilterKey)) {
                        return false;
                    }
                    FilterKey that = (FilterKey) obj;
                    return this.getImpl().equals(that.getImpl());
                }
            };
        } else {
            try {
                key = (FilterKey) def.getKeyMethod().invoke(instance);
            } catch (IllegalAccessException e) {
                throw new SearchException("Unable to access @Key method: " + def.getImpl().getName() + "."
                        + def.getKeyMethod().getName());
            } catch (InvocationTargetException e) {
                throw new SearchException("Unable to access @Key method: " + def.getImpl().getName() + "."
                        + def.getKeyMethod().getName());
            } catch (ClassCastException e) {
                throw new SearchException("@Key method does not return FilterKey: " + def.getImpl().getName() + "."
                        + def.getKeyMethod().getName());
            }
        }
        key.setImpl(def.getImpl());

        //Make sure Filters are isolated by filter def name
        StandardFilterKey wrapperKey = new StandardFilterKey();
        wrapperKey.addParameter(def.getName());
        wrapperKey.addParameter(key);
        return wrapperKey;
    }

    private Object createFilterInstance(FullTextFilterImpl fullTextFilter, FilterDef def) {
        Object instance;
        try {
            instance = def.getImpl().newInstance();
        } catch (InstantiationException e) {
            throw new SearchException("Unable to create @FullTextFilterDef: " + def.getImpl(), e);
        } catch (IllegalAccessException e) {
            throw new SearchException("Unable to create @FullTextFilterDef: " + def.getImpl(), e);
        }
        for (Map.Entry<String, Object> entry : fullTextFilter.getParameters().entrySet()) {
            def.invoke(entry.getKey(), instance, entry.getValue());
        }
        if (cacheInstance(def.getCacheMode()) && def.getKeyMethod() == null
                && fullTextFilter.getParameters().size() > 0) {
            throw new SearchException("Filter with parameters and no @Key method: " + fullTextFilter.getName());
        }
        return instance;
    }

    private org.apache.lucene.search.Query filterQueryByClasses(org.apache.lucene.search.Query luceneQuery) {
        if (!needClassFilterClause) {
            return luceneQuery;
        } else {
            //A query filter is more practical than a manual class filtering post query (esp on scrollable resultsets)
            //it also probably minimise the memory footprint
            BooleanQuery classFilter = new BooleanQuery();
            //annihilate the scoring impact of DocumentBuilderIndexedEntity.CLASS_FIELDNAME
            classFilter.setBoost(0);
            for (Class clazz : classesAndSubclasses) {
                Term t = new Term(DocumentBuilder.CLASS_FIELDNAME, clazz.getName());
                TermQuery termQuery = new TermQuery(t);
                classFilter.add(termQuery, BooleanClause.Occur.SHOULD);
            }
            BooleanQuery filteredQuery = new BooleanQuery();
            filteredQuery.add(luceneQuery, BooleanClause.Occur.MUST);
            filteredQuery.add(classFilter, BooleanClause.Occur.MUST);
            return filteredQuery;
        }
    }

    private int max(int first, int totalHits) {
        if (maxResults == null) {
            return totalHits - 1;
        } else {
            return maxResults + first < totalHits ? first + maxResults - 1 : totalHits - 1;
        }
    }

    public SearchFactoryImplementor getSearchFactoryImplementor() {
        return searchFactoryImplementor;
    }

    private boolean useFieldCacheOnTypes() {
        if (classesAndSubclasses.size() <= 1) {
            // force it to false, as we won't need classes at all
            return false;
        }
        return useFieldCacheOnClassTypes;
    }

    /**
     * @return The FieldCacheCollectorFactory to use for this query, or null to not use FieldCaches
     */
    private FieldCacheCollectorFactory getAppropriateIdFieldCollectorFactory() {
        Map<Class<?>, EntityIndexBinder<?>> builders = searchFactoryImplementor.getIndexBindingForEntity();
        Set<FieldCacheCollectorFactory> allCollectors = new HashSet<FieldCacheCollectorFactory>();
        // we need all documentBuilder to agree on type, fieldName, and enabling the option:
        FieldCacheCollectorFactory anyImplementation = null;
        for (Class<?> clazz : classesAndSubclasses) {
            EntityIndexBinder<?> docBuilder = builders.get(clazz);
            FieldCacheCollectorFactory fieldCacheCollectionFactory = docBuilder.getIdFieldCacheCollectionFactory();
            if (fieldCacheCollectionFactory == null) {
                // some implementation disable it, so we won't use it
                return null;
            }
            anyImplementation = fieldCacheCollectionFactory;
            allCollectors.add(fieldCacheCollectionFactory);
        }
        if (allCollectors.size() != 1) {
            // some implementations have different requirements
            return null;
        } else {
            // they are all the same, return any:
            return anyImplementation;
        }
    }
}