/*
* 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.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.hibernate.Criteria;
import org.hibernate.annotations.common.reflection.XMember;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.search.cfg.spi.IdUniquenessResolver;
import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator;
import org.hibernate.search.engine.service.spi.ServiceManager;
import org.hibernate.search.engine.spi.DocumentBuilderIndexedEntity;
import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.query.engine.spi.EntityInfo;
import org.hibernate.search.query.engine.spi.TimeoutManager;
import org.hibernate.search.spi.InstanceInitializer;
import org.hibernate.search.util.impl.ReflectionHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;
/**
* Initialize object using one or several criteria queries.
*
* @author Emmanuel Bernard
* @author Gunnar Morling
* @author Guillaume Smet
*/
public class CriteriaObjectInitializer implements ObjectInitializer {
private static final Log log = LoggerFactory.make();
private static final int MAX_IN_CLAUSE = 500;
public static final CriteriaObjectInitializer INSTANCE = new CriteriaObjectInitializer();
private CriteriaObjectInitializer() {
// use INSTANCE instead of constructor
}
@Override
public void initializeObjects(List<EntityInfo> entityInfos,
LinkedHashMap<EntityInfoLoadKey, Object> idToObjectMap,
ObjectInitializationContext objectInitializationContext) {
// Do not call isTimeOut here as the caller might be the last biggie on the list.
final int maxResults = entityInfos.size();
if ( log.isTraceEnabled() ) {
log.tracef( "Load %d objects using criteria queries", (Integer) maxResults );
}
if ( maxResults == 0 ) {
return;
}
List<Criteria> criterias = buildUpCriteria( entityInfos, objectInitializationContext );
for ( Criteria criteria : criterias ) {
setCriteriaTimeout( criteria, objectInitializationContext.getTimeoutManager() );
@SuppressWarnings("unchecked")
List<Object> queryResultList = criteria.list();
InstanceInitializer instanceInitializer = objectInitializationContext.getExtendedSearchIntegrator()
.getInstanceInitializer();
for ( Object o : queryResultList ) {
Class<?> loadedType = instanceInitializer.getClass( o );
Object unproxiedObject = instanceInitializer.unproxy( o );
DocumentBuilderIndexedEntity documentBuilder = getDocumentBuilder(
loadedType,
objectInitializationContext.getExtendedSearchIntegrator()
);
if ( documentBuilder == null ) {
// the query result can contain entities which are not indexed. This can for example happen if
// the targeted entity type is a superclass with indexed and un-indexed sub classes
// entities which don't have a document builder can be ignored (HF)
continue;
}
XMember idProperty = documentBuilder.getIdGetter();
Object id = ReflectionHelper.getMemberValue( unproxiedObject, idProperty );
EntityInfoLoadKey key = new EntityInfoLoadKey( loadedType, id );
Object previousValue = idToObjectMap.put( key, unproxiedObject );
if ( previousValue == null ) {
throw new AssertionFailure( "An entity got loaded even though it was not part of the EntityInfo list" );
}
}
}
}
private void setCriteriaTimeout(Criteria criteria, TimeoutManager timeoutManager) {
// not best effort so fail fast
if ( timeoutManager.getType() != TimeoutManager.Type.LIMIT ) {
Long timeLeftInSecond = timeoutManager.getTimeoutLeftInSeconds();
if ( timeLeftInSecond != null ) {
if ( timeLeftInSecond == 0 ) {
timeoutManager.reactOnQueryTimeoutExceptionWhileExtracting( null );
}
criteria.setTimeout( timeLeftInSecond.intValue() );
}
}
}
/**
* Returns a list with one or more {@link Criteria} objects for loading the given entity infos. The returned list
* will contain one criteria object for each id space used by the given infos. A single criteria will be returned in
* case all the entity infos originate from the same id space.
*/
private List<Criteria> buildUpCriteria(List<EntityInfo> entityInfos, ObjectInitializationContext objectInitializationContext) {
Map<Class<?>, EntityInfoIdSpace> infosByIdSpace = groupInfosByIdSpace( entityInfos, objectInitializationContext );
// all entities from same id space -> single criteria
if ( infosByIdSpace.size() == 1 ) {
EntityInfoIdSpace idSpace = infosByIdSpace.values().iterator().next();
// no explicitly user specified criteria query, define one
Criteria criteria = objectInitializationContext.getCriteria();
if ( criteria == null ) {
criteria = createCriteria( idSpace, objectInitializationContext );
}
criteria.add( getIdListCriterion( idSpace.getEntityInfos(), objectInitializationContext ) );
return Collections.singletonList( criteria );
}
// entities originate from different id spaces -> criteria per space
else {
// Cannot use external criteria for fetching entities from different spaces
if ( objectInitializationContext.getCriteria() != null ) {
log.givenCriteriaObjectCannotBeApplied();
}
List<Criteria> criterias = new ArrayList<>( infosByIdSpace.size() );
for ( Entry<Class<?>, EntityInfoIdSpace> infosOfIdSpace : infosByIdSpace.entrySet() ) {
EntityInfoIdSpace idSpace = infosOfIdSpace.getValue();
Criteria criteria = createCriteria( idSpace, objectInitializationContext );
criteria.add( getIdListCriterion( idSpace.getEntityInfos(), objectInitializationContext ) );
criterias.add( criteria );
}
return criterias;
}
}
private Criteria createCriteria(EntityInfoIdSpace idSpace, ObjectInitializationContext objectInitializationContext) {
// Legacy Hibernate Criteria is constructed directly to avoid it logging a warning
// which is meant to suggest that end users need to move away from the legacy Criteria usage..
// We can't avoid the Criteria now w/o breacking backwards compatibility
// (Specifically, without removing "org.hibernate.search.FullTextQuery.setCriteriaQuery(Criteria)" )
return new CriteriaImpl( idSpace.getMostSpecificEntityType().getName(), (SharedSessionContractImplementor) objectInitializationContext.getSession() );
}
/**
* Returns a {@link Criterion} for fetching all the given entity infos. If needed, this criterion will contain a
* {@link Disjunction} for fetching the infos in chunks of {@link CriteriaObjectInitializer#MAX_IN_CLAUSE} elements.
*/
private Criterion getIdListCriterion(List<EntityInfo> entityInfos, ObjectInitializationContext objectInitializationContext) {
DocumentBuilderIndexedEntity documentBuilder = getDocumentBuilder(
entityInfos.iterator().next().getClazz(),
objectInitializationContext.getExtendedSearchIntegrator()
);
String idName = documentBuilder.getIdPropertyName();
Disjunction disjunction = Restrictions.disjunction();
int maxResults = entityInfos.size();
int loop = maxResults / MAX_IN_CLAUSE;
boolean exact = maxResults % MAX_IN_CLAUSE == 0;
if ( !exact ) {
loop++;
}
for ( int index = 0; index < loop; index++ ) {
int max = Math.min( index * MAX_IN_CLAUSE + MAX_IN_CLAUSE, maxResults );
List<Serializable> ids = new ArrayList<>( max - index * MAX_IN_CLAUSE );
for ( int entityInfoIndex = index * MAX_IN_CLAUSE; entityInfoIndex < max; entityInfoIndex++ ) {
ids.add( entityInfos.get( entityInfoIndex ).getId() );
}
disjunction.add( Restrictions.in( idName, ids ) );
}
return disjunction;
}
/**
* Groups the given entity infos by id spaces. An id space is a set of entity types which share the same id
* property, e.g. defined in a common super-entity.
*
* @return The given entity infos, keyed by the root entity type of id spaces
*/
private Map<Class<?>, EntityInfoIdSpace> groupInfosByIdSpace(List<EntityInfo> entityInfos, ObjectInitializationContext objectInitializationContext) {
ServiceManager serviceManager = objectInitializationContext.getExtendedSearchIntegrator().getServiceManager();
IdUniquenessResolver resolver = serviceManager.requestService( IdUniquenessResolver.class );
SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) objectInitializationContext.getSession().getSessionFactory();
try {
Map<Class<?>, EntityInfoIdSpace> idSpaces = new HashMap<>();
for ( EntityInfo entityInfo : entityInfos ) {
addToIdSpace( idSpaces, entityInfo, resolver, sessionFactory );
}
return idSpaces;
}
finally {
serviceManager.releaseService( IdUniquenessResolver.class );
}
}
private Class<?> getRootEntityType(SessionFactoryImplementor sessionFactory, Class<?> entityType) {
String entityName = sessionFactory.getClassMetadata( entityType ).getEntityName();
String rootEntityName = sessionFactory.getEntityPersister( entityName ).getRootEntityName();
return sessionFactory.getEntityPersister( rootEntityName ).getMappedClass();
}
private void addToIdSpace(Map<Class<?>, EntityInfoIdSpace> idSpaces, EntityInfo entityInfo, IdUniquenessResolver resolver, SessionFactoryImplementor sessionFactory) {
// add to existing id space if possible
for ( Entry<Class<?>, EntityInfoIdSpace> idSpace : idSpaces.entrySet() ) {
if ( resolver.areIdsUniqueForClasses( entityInfo.getClazz(), idSpace.getKey() ) ) {
idSpace.getValue().add( entityInfo );
return;
}
}
// otherwise create a new id space, using the root entity as key
Class<?> rootType = getRootEntityType( sessionFactory, entityInfo.getClazz() );
EntityInfoIdSpace idSpace = new EntityInfoIdSpace( rootType, entityInfo );
idSpaces.put( getRootEntityType( sessionFactory, entityInfo.getClazz() ), idSpace );
}
private DocumentBuilderIndexedEntity getDocumentBuilder(Class<?> entityType, ExtendedSearchIntegrator extendedIntegrator) {
Set<Class<?>> indexedEntities = extendedIntegrator.getIndexedTypesPolymorphic( new Class<?>[] { entityType } );
if ( indexedEntities.size() > 0 ) {
return extendedIntegrator.getIndexBinding(
indexedEntities.iterator().next()
).getDocumentBuilder();
}
else {
return null;
}
}
/**
* Container used to store all the {@code EntityInfo}s for entities which share the same id space (typically all the
* subtypes of the same root type).
*
* Determines the most specific entity type we can use to build a Criteria to get the entities associated with these
* {@code EntityInfo}s.
*/
private static class EntityInfoIdSpace {
private final Class<?> rootType;
private Class<?> mostSpecificEntityType;
private List<EntityInfo> entityInfos = new ArrayList<>();
private EntityInfoIdSpace(Class<?> rootType, EntityInfo entityInfo) {
this.rootType = rootType;
this.entityInfos.add( entityInfo );
this.mostSpecificEntityType = entityInfo.getClazz();
}
private void add(EntityInfo entityInfo) {
entityInfos.add( entityInfo );
mostSpecificEntityType = getMostSpecificCommonSuperClass( mostSpecificEntityType, entityInfo.getClazz() );
}
private Class<?> getMostSpecificCommonSuperClass(Class<?> class1, Class<?> class2) {
if ( rootType.equals( class1 ) || rootType.equals( class2 ) ) {
return rootType;
}
Class<?> superClass = class1;
while ( !superClass.isAssignableFrom( class2 ) ) {
superClass = superClass.getSuperclass();
}
return superClass;
}
private List<EntityInfo> getEntityInfos() {
return entityInfos;
}
/**
* Returns the most specific possible type to build the criteria with. In case of a hierarchy using a join inheritance,
* it makes a huge difference if we are targeting only one subtype as we will avoid the joins on all the subtypes
* of the root entity type.
*
* @return the most specific entity type we can use for the Criteria
*/
private Class<?> getMostSpecificEntityType() {
return mostSpecificEntityType;
}
}
}