/*
* JBoss, Home of Professional Open Source
* Copyright ${year}, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.cache.search;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.hibernate.HibernateException;
import org.hibernate.search.FullTextFilter;
import org.hibernate.search.SearchException;
import org.hibernate.search.engine.DocumentBuilder;
import org.hibernate.search.engine.DocumentExtractor;
import org.hibernate.search.engine.FilterDef;
import org.hibernate.search.engine.SearchFactoryImplementor;
import org.hibernate.search.filter.ChainedFilter;
import org.hibernate.search.filter.FilterKey;
import org.hibernate.search.query.FullTextFilterImpl;
import org.hibernate.search.store.DirectoryProvider;
import org.hibernate.transform.ResultTransformer;
import org.jboss.cache.Cache;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Implementation class of the CacheQuery interface.
* <p/>
*
* @author Navin Surtani (<a href="mailto:nsurtani@redhat.com">nsurtani@redhat.com</a>)
*/
public class CacheQueryImpl implements CacheQuery
{
// private Cache cache; - Removed on 11/07/2008, cache is assigned but never used. Hence removed.
private Class[] classes;
private Sort sort;
private Filter filter;
private Map<String, FullTextFilterImpl> filterDefinitions;
private SearchFactoryImplementor searchFactory;
private Integer firstResult;
private Integer resultSize;
private Integer maxResults;
private static final Log log = LogFactory.getLog(CacheQueryImpl.class);
private Set<Class> classesAndSubclasses;
private boolean needClassFilterClause;
private Query luceneQuery;
private String[] indexProjection;
private ResultTransformer resultTransformer;
CacheEntityLoader entityLoader;
public CacheQueryImpl(Query luceneQuery, SearchFactoryImplementor searchFactory, Cache cache)
{
this.luceneQuery = luceneQuery;
// this.cache = cache;
entityLoader = new CacheEntityLoader(cache);
this.searchFactory = searchFactory;
}
public CacheQueryImpl(Query luceneQuery, SearchFactoryImplementor searchFactory, Cache cache, Class... classes)
{
this(luceneQuery, searchFactory, cache);
this.classes = classes;
}
/**
* Takes in a lucene filter and sets it to the filter field in the class.
*
* @param f - lucene filter
*/
public void setFilter(Filter f)
{
filter = f;
}
/**
* @return The result size of the query.
*/
public int getResultSize()
{
if (resultSize == null)
{
// get result size without object initialization
IndexSearcher searcher = buildSearcher(searchFactory);
if (searcher == null)
{
resultSize = 0;
}
else
{
Hits hits;
try
{
hits = getHits(searcher);
resultSize = hits.length();
}
catch (IOException e)
{
throw new HibernateException("Unable to query Lucene index", e);
}
finally
{
//searcher cannot be null
try
{
IndexSearcherCloser.closeSearcher(searcher, searchFactory.getReaderProvider());
//searchFactoryImplementor.getReaderProvider().closeReader( searcher.getIndexReader() );
}
catch (SearchException e)
{
log.warn("Unable to properly close searcher during lucene query: " + e);
}
}
}
}
return this.resultSize;
}
public void setSort(Sort s)
{
sort = s;
}
/**
* Enable a given filter by its name.
*
* @param name of filter.
* @return a FullTextFilter object.
*/
public FullTextFilter enableFullTextFilter(String name)
{
if (filterDefinitions == null)
{
filterDefinitions = new HashMap<String, FullTextFilterImpl>();
}
FullTextFilterImpl filterDefinition = filterDefinitions.get(name);
if (filterDefinition != null) return filterDefinition;
filterDefinition = new FullTextFilterImpl();
filterDefinition.setName(name);
FilterDef filterDef = searchFactory.getFilterDefinition(name);
if (filterDef == null)
{
throw new SearchException("Unkown @FullTextFilter: " + name);
}
filterDefinitions.put(name, filterDefinition);
return filterDefinition;
}
/**
* Disable a given filter by its name.
*
* @param name of filter.
*/
public void disableFullTextFilter(String name)
{
filterDefinitions.remove(name);
}
/**
* Sets the the result of the given integer value to the first result.
*
* @param firstResult index to be set.
* @throws IllegalArgumentException if the index given is less than zero.
*/
public void setFirstResult(int firstResult)
{
if (firstResult < 0)
{
throw new IllegalArgumentException("'first' pagination parameter less than 0");
}
this.firstResult = firstResult;
//TODO How do we deal with this if the parameter is too high.
}
public QueryResultIterator iterator() throws HibernateException
{
return iterator (1);
}
public QueryResultIterator iterator(int fetchSize) throws HibernateException
{
List<CacheEntityId> ids = null;
IndexSearcher searcher = buildSearcher(searchFactory);
if (searcher == null)
{
throw new NullPointerException("IndexSearcher instance is null.");
}
try
{
Hits hits = getHits(searcher);
int first = first();
int max = max(first, hits);
int size = max - first + 1 < 0 ? 0 : max - first + 1;
ids = new ArrayList<CacheEntityId>(size);
DocumentExtractor extractor = new DocumentExtractor(luceneQuery, searcher, searchFactory, indexProjection);
for (int index = first; index <= max; index++)
{
String documentId = (String) extractor.extract(hits, index).id;
CacheEntityId id = new CacheEntityId(documentId);
ids.add(id);
}
}
catch (IOException e)
{
throw new HibernateException("Unable to query Lucene index", e);
}
finally
{
IndexSearcherCloser.closeSearcher(searcher, searchFactory.getReaderProvider());
}
return new QueryResultIteratorImpl(ids, entityLoader, fetchSize);
}
public QueryResultIterator lazyIterator()
{
return lazyIterator(1);
}
public QueryResultIterator lazyIterator(int fetchSize)
{
IndexSearcher searcher = buildSearcher(searchFactory);
try
{
Hits hits = getHits(searcher);
int first = first();
int max = max(first, hits);
DocumentExtractor extractor = new DocumentExtractor(luceneQuery, searcher, searchFactory, indexProjection);
return new LazyQueryResultIterator(extractor, entityLoader, hits, searcher, searchFactory, first, max, fetchSize);
}
catch (IOException e)
{
try
{
IndexSearcherCloser.closeSearcher(searcher, searchFactory.getReaderProvider());
}
catch (SearchException ee)
{
//we have the initial issue already
}
throw new HibernateException("Unable to query Lucene index", e);
}
}
public List<Object> list() throws HibernateException
{
IndexSearcher searcher = buildSearcher(searchFactory);
if (searcher == null) return new ArrayList(0);
Hits hits;
try
{
hits = getHits(searcher);
if (log.isTraceEnabled()) log.trace("Number of hits are " + hits.length());
int first = first();
int max = max(first, hits);
int size = max - first + 1 < 0 ? 0 : max - first + 1;
List<CacheEntityId> ids = new ArrayList<CacheEntityId>(size);
DocumentExtractor extractor = new DocumentExtractor(luceneQuery, searcher, searchFactory, indexProjection);
for (int index = first; index <= max; index++)
{
String documentId = (String) extractor.extract(hits, index).id;
CacheEntityId id = new CacheEntityId(documentId);
ids.add(id);
}
List<Object> list = entityLoader.load(ids);
if (resultTransformer == null)
{
return list;
}
else
{
return resultTransformer.transformList(list);
}
}
catch (IOException e)
{
throw new HibernateException("Unable to query Lucene index", e);
}
finally
{
IndexSearcherCloser.closeSearcher(searcher, searchFactory.getReaderProvider());
}
}
private int max(int first, Hits hits)
{
return maxResults == null ?
hits.length() - 1 :
maxResults + first < hits.length() ?
first + maxResults - 1 :
hits.length() - 1;
}
private int first()
{
return firstResult != null ?
firstResult :
0;
}
public void setMaxResults(int maxResults)
{
if (maxResults < 0)
{
throw new IllegalArgumentException("'max' pagination parameter less than 0");
}
this.maxResults = maxResults;
}
private IndexSearcher buildSearcher(SearchFactoryImplementor searchFactoryImplementor)
{
Map<Class, DocumentBuilder<Object>> builders = searchFactoryImplementor.getDocumentBuilders();
List<DirectoryProvider> directories = new ArrayList<DirectoryProvider>();
Similarity searcherSimilarity = null;
if (classes == null || classes.length == 0)
{
//no class means all classes
for (DocumentBuilder builder : builders.values())
{
searcherSimilarity = checkSimilarity(searcherSimilarity, builder);
final DirectoryProvider[] directoryProviders = builder.getDirectoryProviderSelectionStrategy().getDirectoryProvidersForAllShards();
populateDirectories(directories, directoryProviders);
}
classesAndSubclasses = null;
}
else
{
Set<Class> involvedClasses = new HashSet<Class>(classes.length);
Collections.addAll(involvedClasses, classes);
for (Class clazz : classes)
{
DocumentBuilder builder = builders.get(clazz);
if (builder != null) involvedClasses.addAll(builder.getMappedSubclasses());
}
for (Class clazz : involvedClasses)
{
DocumentBuilder builder = builders.get(clazz);
if (builder == null)
{
throw new HibernateException("Not a mapped entity (don't forget to add @Indexed): " + clazz);
}
final DirectoryProvider[] directoryProviders = builder.getDirectoryProviderSelectionStrategy().getDirectoryProvidersForAllShards();
searcherSimilarity = checkSimilarity(searcherSimilarity, builder);
populateDirectories(directories, directoryProviders);
}
classesAndSubclasses = involvedClasses;
}
//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 (DirectoryProvider dp : directories)
{
final Set<Class> classesInDirectoryProvider = searchFactoryImplementor.getClassesInDirectoryProvider(dp);
// if a DP contains only one class, we know for sure it's part of classesAndSubclasses
if (classesInDirectoryProvider.size() > 1)
{
//risk of needClassFilterClause
for (Class clazz : classesInDirectoryProvider)
{
if (!classesAndSubclasses.contains(clazz))
{
this.needClassFilterClause = true;
break;
}
}
}
if (this.needClassFilterClause) break;
}
}
//set up the searcher
final DirectoryProvider[] directoryProviders = directories.toArray(new DirectoryProvider[directories.size()]);
IndexSearcher is = new IndexSearcher(searchFactoryImplementor.getReaderProvider().openReader(directoryProviders));
is.setSimilarity(searcherSimilarity);
return is;
}
private Similarity checkSimilarity(Similarity similarity, DocumentBuilder builder)
{
if (similarity == null)
{
similarity = builder.getSimilarity();
}
else if (!similarity.getClass().equals(builder.getSimilarity().getClass()))
{
throw new HibernateException("Cannot perform search on two entities with differing Similarity implementations (" + similarity.getClass().getName() + " & " + builder.getSimilarity().getClass().getName() + ")");
}
return similarity;
}
private void populateDirectories(List<DirectoryProvider> directories, DirectoryProvider[] directoryProviders)
{
for (DirectoryProvider provider : directoryProviders)
{
if (!directories.contains(provider))
{
directories.add(provider);
}
}
}
private Hits getHits(Searcher searcher) throws IOException
{
Hits hits;
org.apache.lucene.search.Query query = filterQueryByClasses(luceneQuery);
buildFilters();
hits = searcher.search(query, filter, sort);
setResultSize(hits);
return hits;
}
private void setResultSize(Hits hits)
{
resultSize = hits.length();
}
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 DocumentBuilder.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 void buildFilters()
{
if (filterDefinitions != null && filterDefinitions.size() > 0)
{
ChainedFilter chainedFilter = new ChainedFilter();
for (FullTextFilterImpl filterDefinition : filterDefinitions.values())
{
FilterDef def = searchFactory.getFilterDefinition(filterDefinition.getName());
Class implClass = def.getImpl();
Object instance;
try
{
instance = implClass.newInstance();
}
catch (Exception e)
{
throw new SearchException("Unable to create @FullTextFilterDef: " + def.getImpl(), e);
}
for (Map.Entry<String, Object> entry : filterDefinition.getParameters().entrySet())
{
def.invoke(entry.getKey(), instance, entry.getValue());
}
if (def.isCache() && def.getKeyMethod() == null && filterDefinition.getParameters().size() > 0)
{
throw new SearchException("Filter with parameters and no @Key method: " + filterDefinition.getName());
}
FilterKey key = null;
if (def.isCache())
{
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());
}
Filter filter = def.isCache() ?
searchFactory.getFilterCachingStrategy().getCachedFilter(key) :
null;
if (filter == null)
{
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());
}
catch (InvocationTargetException e)
{
throw new SearchException("Unable to access @Factory method: "
+ def.getImpl().getName() + "." + def.getFactoryMethod().getName());
}
catch (ClassCastException e)
{
throw new SearchException("@Key method does not return a org.apache.lucene.search.Filter class: "
+ def.getImpl().getName() + "." + def.getFactoryMethod().getName());
}
}
else
{
try
{
filter = (Filter) instance;
}
catch (ClassCastException e)
{
throw new SearchException("@Key method does not return a org.apache.lucene.search.Filter class: "
+ def.getImpl().getName() + "." + def.getFactoryMethod().getName());
}
}
if (def.isCache())
{
searchFactory.getFilterCachingStrategy().addCachedFilter(key, filter);
}
}
chainedFilter.addFilter(filter);
}
if (filter != null) chainedFilter.addFilter(filter);
filter = chainedFilter;
}
}
}