/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.query.hibernate.impl; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.persistence.FlushModeType; import javax.persistence.LockModeType; import javax.persistence.Parameter; import javax.persistence.TemporalType; import org.apache.lucene.search.Explanation; import org.apache.lucene.search.Filter; import org.apache.lucene.search.Sort; import org.hibernate.Criteria; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.ScrollMode; import org.hibernate.Session; import org.hibernate.TypeMismatchException; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.hql.internal.QueryExecutionRequestException; import org.hibernate.query.ParameterMetadata; import org.hibernate.query.internal.AbstractProducedQuery; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.query.spi.ScrollableResultsImplementor; import org.hibernate.search.FullTextQuery; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.filter.FullTextFilter; import org.hibernate.search.hcore.util.impl.ContextHelper; import org.hibernate.search.query.DatabaseRetrievalMethod; import org.hibernate.search.query.ObjectLookupMethod; import org.hibernate.search.query.engine.spi.DocumentExtractor; import org.hibernate.search.query.engine.spi.EntityInfo; import org.hibernate.search.query.engine.spi.FacetManager; 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.spatial.Coordinates; import org.hibernate.search.spatial.impl.Point; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.util.logging.impl.LoggerFactory; import org.hibernate.transform.ResultTransformer; import org.hibernate.type.Type; /** * Implementation of {@link org.hibernate.search.FullTextQuery}. * * @author Emmanuel Bernard * @author Hardy Ferentschik */ @SuppressWarnings("rawtypes") // We extend the raw version of AbstractProducedQuery on purpose, see HSEARCH-2564 public class FullTextQueryImpl extends AbstractProducedQuery implements FullTextQuery { private static final Log log = LoggerFactory.make(); private ObjectLookupMethod objectLookupMethod; private DatabaseRetrievalMethod databaseRetrievalMethod; private Criteria criteria; private ResultTransformer resultTransformer; private int fetchSize = 1; private final HSQuery hSearchQuery; private final SessionImplementor session; private Integer firstResult; private Integer maxResults; //initialized at 0 since we don't expect to use hints at this stage private final Map<String, Object> hints = new HashMap<String, Object>( 0 ); /** * Constructs a <code>FullTextQueryImpl</code> instance. * * @param hSearchQuery The query, with the {@link HSQuery#targetedEntities(List) targeted entities} already set if necessary. * @param session Access to the Hibernate session. * @param parameterMetadata Additional query metadata. */ public FullTextQueryImpl(HSQuery hSearchQuery, SessionImplementor session, ParameterMetadata parameterMetadata) { //TODO handle flushMode super( session, parameterMetadata ); this.session = session; ExtendedSearchIntegrator extendedIntegrator = getExtendedSearchIntegrator(); this.objectLookupMethod = extendedIntegrator.getDefaultObjectLookupMethod(); this.databaseRetrievalMethod = extendedIntegrator.getDefaultDatabaseRetrievalMethod(); this.hSearchQuery = hSearchQuery; this.hSearchQuery .timeoutExceptionFactory( new FullTextQueryTimeoutExceptionFactory() ) .tenantIdentifier( session.getTenantIdentifier() ); } @Override public FullTextQueryImpl setSort(Sort sort) { hSearchQuery.sort( sort ); return this; } @Override @Deprecated public FullTextQueryImpl setFilter(Filter filter) { hSearchQuery.filter( filter ); return this; } @Override public List getResultList() { return list(); } /** * Return an iterator on the results. * Retrieve the object one by one (initialize it during the next() operation) */ @Override public Iterator iterate() { //implement an iterator which keep the id/class for each hit and get the object on demand //cause I can't keep the searcher and hence the hit opened. I don't have any hook to know when the //user stops using it //scrollable is better in this area hSearchQuery.getTimeoutManager().start(); final List<EntityInfo> entityInfos = hSearchQuery.queryEntityInfos(); //stop timeout manager, the iterator pace is in the user's hands hSearchQuery.getTimeoutManager().stop(); //TODO is this no-loader optimization really needed? final Iterator<Object> iterator; if ( entityInfos.size() == 0 ) { iterator = new IteratorImpl( entityInfos, noLoader ); return iterator; } else { Loader loader = getLoader(); iterator = new IteratorImpl( entityInfos, loader ); } hSearchQuery.getTimeoutManager().stop(); return iterator; } /** * Decide which object loader to use depending on the targeted entities. If there is only a single entity targeted * a <code>QueryLoader</code> can be used which will only execute a single query to load the entities. If more than * one entity is targeted a <code>MultiClassesQueryLoader</code> must be used. We also have to consider whether * projections or <code>Criteria</code> are used. * * @return The loader instance to use to load the results of the query. */ private Loader getLoader() { ObjectLoaderBuilder loaderBuilder = new ObjectLoaderBuilder() .criteria( criteria ) .targetedEntities( hSearchQuery.getTargetedEntities() ) .indexedTargetedEntities( hSearchQuery.getIndexedTargetedEntities() ) .session( session ) .searchFactory( hSearchQuery.getExtendedSearchIntegrator() ) .timeoutManager( hSearchQuery.getTimeoutManager() ) .lookupMethod( objectLookupMethod ) .retrievalMethod( databaseRetrievalMethod ); if ( hSearchQuery.getProjectedFields() != null ) { return getProjectionLoader( loaderBuilder ); } else { return loaderBuilder.buildLoader(); } } private Loader getProjectionLoader(ObjectLoaderBuilder loaderBuilder) { ProjectionLoader loader = new ProjectionLoader(); loader.init( (Session) session, hSearchQuery.getExtendedSearchIntegrator(), resultTransformer, loaderBuilder, hSearchQuery.getProjectedFields(), hSearchQuery.getTimeoutManager(), hSearchQuery.hasThisProjection() ); return loader; } @Override public ScrollableResultsImpl scroll() { //keep the searcher open until the resultset is closed hSearchQuery.getTimeoutManager().start(); final DocumentExtractor documentExtractor = hSearchQuery.queryDocumentExtractor(); //stop timeout manager, the iterator pace is in the user's hands hSearchQuery.getTimeoutManager().stop(); Loader loader = getLoader(); return new ScrollableResultsImpl( fetchSize, documentExtractor, loader, this.session, hSearchQuery.hasThisProjection() ); } @Override public ScrollableResultsImplementor scroll(ScrollMode scrollMode) { //TODO think about this scrollmode return scroll(); } @Override public List list() { // Reproduce the behavior of AbstractProducedQuery.list() regarding exceptions try { return doHibernateSearchList(); } catch (QueryExecutionRequestException he) { throw new IllegalStateException( he ); } catch (TypeMismatchException e) { throw new IllegalArgumentException( e ); } catch (HibernateException he) { throw getExceptionConverter().convert( he ); } } protected List doHibernateSearchList() { hSearchQuery.getTimeoutManager().start(); final List<EntityInfo> entityInfos = hSearchQuery.queryEntityInfos(); Loader loader = getLoader(); List list = loader.load( entityInfos ); //no need to timeoutManager.isTimedOut from this point, we don't do anything intensive if ( resultTransformer == null || loader instanceof ProjectionLoader ) { //stay consistent with transformTuple which can only be executed during a projection //nothing to do } else { list = resultTransformer.transformList( list ); } hSearchQuery.getTimeoutManager().stop(); return list; } @Override public Explanation explain(int documentId) { return hSearchQuery.explain( documentId ); } @Override public int getResultSize() { try { return doGetResultSize(); } catch (HibernateException he) { throw getExceptionConverter().convert( he ); } } public int doGetResultSize() { if ( getLoader().isSizeSafe() ) { return hSearchQuery.queryResultSize(); } else { throw log.cannotGetResultSizeWithCriteriaAndRestriction( criteria.toString() ); } } @Override public FullTextQueryImpl setCriteriaQuery(Criteria criteria) { this.criteria = criteria; return this; } @Override public FullTextQueryImpl setProjection(String... fields) { hSearchQuery.projection( fields ); return this; } @Override public FullTextQueryImpl setSpatialParameters(Coordinates center, String fieldName) { hSearchQuery.setSpatialParameters( center, fieldName ); return this; } @Override public FullTextQueryImpl setSpatialParameters(double latitude, double longitude, String fieldName) { setSpatialParameters( Point.fromDegrees( latitude, longitude ), fieldName ); return this; } @Override public FullTextQuery setMaxResults(int maxResults) { if ( maxResults < 0 ) { throw new IllegalArgumentException( "Negative (" + maxResults + ") parameter passed in to setMaxResults" ); } hSearchQuery.maxResults( maxResults ); this.maxResults = maxResults; return this; } @Override public int getMaxResults() { return maxResults == null || maxResults == -1 ? Integer.MAX_VALUE : maxResults; } @Override public FullTextQuery setFirstResult(int firstResult) { if ( firstResult < 0 ) { throw new IllegalArgumentException( "Negative (" + firstResult + ") parameter passed in to setFirstResult" ); } hSearchQuery.firstResult( firstResult ); this.firstResult = firstResult; return this; } @Override public int getFirstResult() { return firstResult == null ? 0 : firstResult; } @Override public FullTextQuery setHint(String hintName, Object value) { hints.put( hintName, value ); if ( "javax.persistence.query.timeout".equals( hintName ) ) { if ( value == null ) { //nothing } else if ( value instanceof String ) { setTimeout( Long.parseLong( (String) value ), TimeUnit.MILLISECONDS ); } else if ( value instanceof Number ) { setTimeout( ( (Number) value ).longValue(), TimeUnit.MILLISECONDS ); } } return this; } @Override public Map<String, Object> getHints() { return hints; } @Override // No generics, see unwrap() (same issue) public FullTextQueryImpl setParameter(Parameter tParameter, Object t) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override // No generics, see unwrap() (same issue) public FullTextQueryImpl setParameter(Parameter calendarParameter, Calendar calendar, TemporalType temporalType) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override // No generics, see unwrap() (same issue) public FullTextQueryImpl setParameter(Parameter dateParameter, Date date, TemporalType temporalType) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public FullTextQueryImpl setParameter(String name, Object value) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public FullTextQueryImpl setParameter(String name, Date value, TemporalType temporalType) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public FullTextQueryImpl setParameter(String name, Calendar value, TemporalType temporalType) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public FullTextQueryImpl setParameter(int position, Object value) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public FullTextQueryImpl setParameter(int position, Date value, TemporalType temporalType) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override @SuppressWarnings("unchecked") public Set<Parameter<?>> getParameters() { return Collections.EMPTY_SET; } @Override public FullTextQueryImpl setParameter(int position, Calendar value, TemporalType temporalType) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public Parameter<?> getParameter(String name) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public Parameter<?> getParameter(int position) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override // No generics, see unwrap() (same issue) public Parameter getParameter(String name, Class type) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override // No generics, see unwrap() (same issue) public Parameter getParameter(int position, Class type) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override // No generics, see unwrap() (same issue) public boolean isBound(Parameter param) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override // No generics, see unwrap() (same issue) public Object getParameterValue(Parameter param) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public Object getParameterValue(String name) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public Object getParameterValue(int position) { throw new UnsupportedOperationException( "parameters not supported in fullText queries" ); } @Override public FullTextQueryImpl setFlushMode(FlushModeType flushModeType) { return (FullTextQueryImpl) super.setFlushMode( flushModeType ); } @Override public FullTextQueryImpl setFetchSize(int fetchSize) { super.setFetchSize( fetchSize ); if ( fetchSize <= 0 ) { throw new IllegalArgumentException( "'fetch size' parameter less than or equals to 0" ); } this.fetchSize = fetchSize; return this; } @Override public QueryImplementor setLockOptions(LockOptions lockOptions) { throw new UnsupportedOperationException( "Lock options are not implemented in Hibernate Search queries" ); } @Override public FullTextQueryImpl setResultTransformer(ResultTransformer transformer) { super.setResultTransformer( transformer ); this.resultTransformer = transformer; return this; } /* * Implementation note: this method is defined as generic in the interface, * but we must implement it without generics (otherwise it won't compile). * * The actual reason is a bit hard to explain: basically we implement * javax.persistence.Query as a raw type at some point, and our superclass * (also extended as a raw type) also implements this interface, but as a non-raw type. * This seems to confuse the compiler, which thinks there are two different methods. */ @Override @SuppressWarnings("unchecked") public Object unwrap(Class type) { //I've purposely decided not to return the underlying Hibernate FullTextQuery //as I see this as an implementation detail that should not be exposed. if ( type == org.apache.lucene.search.Query.class ) { return hSearchQuery.getLuceneQuery(); } throw new IllegalArgumentException( "Cannot unwrap " + type.getName() ); } @Override public FullTextQueryImpl setLockMode(LockModeType lockModeType) { throw new UnsupportedOperationException( "lock modes not supported in fullText queries" ); } @Override public LockModeType getLockMode() { throw new UnsupportedOperationException( "lock modes not supported in fullText queries" ); } @Override public LockOptions getLockOptions() { throw new UnsupportedOperationException( "Lock options are not implemented in Hibernate Search queries" ); } @Override public int executeUpdate() { throw new UnsupportedOperationException( "executeUpdate is not supported in Hibernate Search queries" ); } @Override public QueryImplementor setLockMode(String alias, LockMode lockMode) { throw new UnsupportedOperationException( "Lock options are not implemented in Hibernate Search queries" ); } protected Map getLockModes() { throw new UnsupportedOperationException( "Lock options are not implemented in Hibernate Search queries" ); } @Override public FullTextFilter enableFullTextFilter(String name) { return hSearchQuery.enableFullTextFilter( name ); } @Override public void disableFullTextFilter(String name) { hSearchQuery.disableFullTextFilter( name ); } @Override public FacetManager getFacetManager() { return hSearchQuery.getFacetManager(); } @Override public FullTextQueryImpl setTimeout(int timeout) { return setTimeout( timeout, TimeUnit.SECONDS ); } @Override public FullTextQueryImpl setTimeout(long timeout, TimeUnit timeUnit) { super.setTimeout( (int) timeUnit.toSeconds( timeout ) ); hSearchQuery.getTimeoutManager().setTimeout( timeout, timeUnit ); hSearchQuery.getTimeoutManager().raiseExceptionOnTimeout(); return this; } @Override public FullTextQueryImpl limitExecutionTimeTo(long timeout, TimeUnit timeUnit) { hSearchQuery.getTimeoutManager().setTimeout( timeout, timeUnit ); hSearchQuery.getTimeoutManager().limitFetchingOnTimeout(); return this; } @Override public boolean hasPartialResults() { return hSearchQuery.getTimeoutManager().hasPartialResults(); } @Override public FullTextQueryImpl initializeObjectsWith(ObjectLookupMethod lookupMethod, DatabaseRetrievalMethod retrievalMethod) { this.objectLookupMethod = lookupMethod; this.databaseRetrievalMethod = retrievalMethod; return this; } private ExtendedSearchIntegrator getExtendedSearchIntegrator() { return ContextHelper.getSearchIntegratorBySessionImplementor( session ); } @Override public String getQueryString() { return hSearchQuery.getQueryString(); } @Override protected boolean isNativeQuery() { return false; } @Override public Type[] getReturnTypes() { throw new UnsupportedOperationException( "getReturnTypes() is not implemented in Hibernate Search queries" ); } @Override public String[] getReturnAliases() { throw new UnsupportedOperationException( "getReturnAliases() is not implemented in Hibernate Search queries" ); } @Override public FullTextQueryImpl setEntity(int position, Object val) { throw new UnsupportedOperationException( "setEntity(int,Object) is not implemented in Hibernate Search queries" ); } @Override public FullTextQueryImpl setEntity(String name, Object val) { throw new UnsupportedOperationException( "setEntity(String,Object) is not implemented in Hibernate Search queries" ); } @Override public String toString() { return "FullTextQueryImpl(" + getQueryString() + ")"; } private static final Loader noLoader = new Loader() { @Override public void init(Session session, ExtendedSearchIntegrator extendedIntegrator, ObjectInitializer objectInitializer, TimeoutManager timeoutManager) { } @Override public Object load(EntityInfo entityInfo) { throw new UnsupportedOperationException( "noLoader should not be used" ); } @Override public Object loadWithoutTiming(EntityInfo entityInfo) { throw new UnsupportedOperationException( "noLoader should not be used" ); } @Override public List load(List<EntityInfo> entityInfos) { throw new UnsupportedOperationException( "noLoader should not be used" ); } @Override public boolean isSizeSafe() { return false; } }; private class FullTextQueryTimeoutExceptionFactory implements TimeoutExceptionFactory { @Override public RuntimeException createTimeoutException(String message, String queryDescription) { return new javax.persistence.QueryTimeoutException( message, null, FullTextQueryImpl.this ); } }; }