All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hibernate.search.engine.spi.AbstractDocumentBuilder Maven / Gradle / Ivy

There is a newer version: 5.11.12.Final
Show newest version
/*
 * 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 .
 */
package org.hibernate.search.engine.spi;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.annotations.common.reflection.XClass;
import org.hibernate.annotations.common.reflection.XMember;
import org.hibernate.search.backend.LuceneWork;
import org.hibernate.search.bridge.spi.ConversionContext;
import org.hibernate.search.engine.BoostStrategy;
import org.hibernate.search.engine.impl.DefaultBoostStrategy;
import org.hibernate.search.engine.impl.WorkPlan;
import org.hibernate.search.engine.metadata.impl.ContainedInMetadata;
import org.hibernate.search.engine.metadata.impl.EmbeddedTypeMetadata;
import org.hibernate.search.engine.metadata.impl.PropertyMetadata;
import org.hibernate.search.engine.metadata.impl.TypeMetadata;
import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.spi.InstanceInitializer;
import org.hibernate.search.util.impl.ReflectionHelper;
import org.hibernate.search.util.impl.ScopedAnalyzerReference;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * Abstract base class for the document builders.
 *
 * @author Hardy Ferentschik
 * @author Davide D'Alto
 * @author Sanne Grinovero
 */
public abstract class AbstractDocumentBuilder {
	private static final Log log = LoggerFactory.make();

	private final XClass beanXClass;
	private final Class beanClass;
	private final TypeMetadata typeMetadata;
	private final InstanceInitializer instanceInitializer;

	private boolean isRoot;
	private Set> mappedSubclasses = new HashSet>();

	protected EntityState entityState;

	/**
	 * Constructor.
	 *
	 * @param xClass The class for which to build a document builder
	 * @param typeMetadata metadata for the specified class
	 * @param reflectionManager Reflection manager to use for processing the annotations
	 * @param optimizationBlackList keeps track of types on which we need to disable collection events optimizations
	 * @param instanceInitializer a {@link org.hibernate.search.spi.InstanceInitializer} object.
	 */
	public AbstractDocumentBuilder(XClass xClass,
			TypeMetadata typeMetadata,
			ReflectionManager reflectionManager,
			Set optimizationBlackList,
			InstanceInitializer instanceInitializer) {
		if ( xClass == null ) {
			throw new AssertionFailure( "Unable to build a DocumentBuilderContainedEntity with a null class" );
		}

		this.instanceInitializer = instanceInitializer;
		this.entityState = EntityState.CONTAINED_IN_ONLY;
		this.beanXClass = xClass;
		this.beanClass = reflectionManager.toClass( xClass );
		this.typeMetadata = typeMetadata;

		optimizationBlackList.addAll( typeMetadata.getOptimizationBlackList() );
	}

	public abstract void addWorkToQueue(
			String tenantIdentifier,
			Class entityClass,
			Object entity, Serializable id,
			boolean delete,
			boolean add,
			List queue,
			ConversionContext contextualBridge);

	/**
	 * In case of an indexed entity, return the value of it's identifier: what is marked as @Id or @DocumentId;
	 * in case the entity uses @ProvidedId, it's illegal to call this method.
	 *
	 * @param entity the instance for which to retrieve the id
	 *
	 * @return the value, or null if it's not an indexed entity
	 *
	 * @throws IllegalStateException when used with a @ProvidedId annotated entity
	 */
	public abstract Serializable getId(Object entity);

	public TypeMetadata getTypeMetadata() {
		return typeMetadata;
	}

	public boolean isRoot() {
		return isRoot;
	}

	public Class getBeanClass() {
		return beanClass;
	}

	public XClass getBeanXClass() {
		return beanXClass;
	}

	public TypeMetadata getMetadata() {
		return typeMetadata;
	}

	public ScopedAnalyzerReference getAnalyzerReference() {
		return typeMetadata.getDefaultAnalyzerReference();
	}

	public EntityState getEntityState() {
		return entityState;
	}

	public Set> getMappedSubclasses() {
		return mappedSubclasses;
	}

	public void postInitialize(Set> indexedClasses) {
		//we initialize only once because we no longer have a reference to the reflectionManager
		//in theory
		Class plainClass = beanClass;
		if ( entityState == EntityState.NON_INDEXABLE ) {
			throw new AssertionFailure( "A non indexed entity is post processed" );
		}
		Set> tempMappedSubclasses = new HashSet>();
		//together with the caller this creates a o(2), but I think it's still faster than create the up hierarchy for each class
		for ( Class currentClass : indexedClasses ) {
			if ( plainClass != currentClass && plainClass.isAssignableFrom( currentClass ) ) {
				tempMappedSubclasses.add( currentClass );
			}
		}
		this.mappedSubclasses = Collections.unmodifiableSet( tempMappedSubclasses );
		Class superClass = plainClass.getSuperclass();
		this.isRoot = true;
		while ( superClass != null ) {
			if ( indexedClasses.contains( superClass ) ) {
				this.isRoot = false;
				break;
			}
			superClass = superClass.getSuperclass();
		}
	}

	/**
	 * If we have a work instance we have to check whether the instance to be indexed is contained in any other indexed entities.
	 *
	 * @see #appendContainedInWorkForInstance(Object, WorkPlan, ContainedInRecursionContext, String)
	 * @param instance the instance to be indexed
	 * @param workPlan the current work plan
	 * @param currentRecursionContext the current {@link org.hibernate.search.engine.spi.ContainedInRecursionContext} object used to check the graph traversal
	 */
	public void appendContainedInWorkForInstance(Object instance, WorkPlan workPlan, ContainedInRecursionContext currentRecursionContext) {
		appendContainedInWorkForInstance( instance, workPlan, currentRecursionContext, null );
	}

	/**
	 * If we have a work instance we have to check whether the instance to be indexed is contained in any other indexed entities for a tenant.
	 *
	 * @param instance the instance to be indexed
	 * @param workPlan the current work plan
	 * @param currentRecursionContext the current {@link org.hibernate.search.engine.spi.ContainedInRecursionContext} object used to check the graph traversal
	 * @param tenantIdentifier the identifier of the tenant or null, if there isn't one
	 * @see #appendContainedInWorkForInstance(Object, WorkPlan, ContainedInRecursionContext)
	 */
	public void appendContainedInWorkForInstance(Object instance, WorkPlan workPlan, ContainedInRecursionContext currentRecursionContext, String tenantIdentifier) {
		for ( ContainedInMetadata containedInMetadata : typeMetadata.getContainedInMetadata() ) {
			XMember member = containedInMetadata.getContainedInMember();
			Object unproxiedInstance = instanceInitializer.unproxy( instance );

			ContainedInRecursionContext recursionContext = updateContainedInRecursionContext( unproxiedInstance, containedInMetadata, currentRecursionContext );

			if ( recursionContext.isTerminal() ) {
				continue;
			}

			Object value = ReflectionHelper.getMemberValue( unproxiedInstance, member );

			if ( value == null ) {
				continue;
			}

			if ( member.isArray() ) {
				Object[] array = (Object[]) value;
				for ( Object arrayValue : array ) {
					processSingleContainedInInstance( workPlan, arrayValue, recursionContext, tenantIdentifier );
				}
			}
			else if ( member.isCollection() ) {
				Collection collection = null;
				try {
					collection = getActualCollection( member, value );
					collection.size(); //load it
				}
				catch (Exception e) {
					if ( e.getClass().getName().contains( "org.hibernate.LazyInitializationException" ) ) {
						/* A deleted entity not having its collection initialized
						 * leads to a LIE because the collection is no longer attached to the session
						 *
						 * But that's ok as the collection update event has been processed before
						 * or the fk would have been cleared and thus triggering the cleaning
						 */
						collection = null;
					}
				}
				if ( collection != null ) {
					for ( Object collectionValue : collection ) {
						processSingleContainedInInstance( workPlan, collectionValue, recursionContext, tenantIdentifier );
					}
				}
			}
			else {
				processSingleContainedInInstance( workPlan, value, recursionContext, tenantIdentifier );
			}
		}
	}

	protected InstanceInitializer getInstanceInitializer() {
		return instanceInitializer;
	}

	private ContainedInRecursionContext updateContainedInRecursionContext(Object containedInstance, ContainedInMetadata containedInMetadata,
			ContainedInRecursionContext containedContext) {
		int maxDepth;
		int depth;

		// Handle @IndexedEmbedded.depth-induced limits

		Integer metadataMaxDepth = containedInMetadata.getMaxDepth();
		if ( containedInstance != null && metadataMaxDepth != null ) {
			maxDepth = metadataMaxDepth;
		}
		else {
			maxDepth = containedContext != null ? containedContext.getMaxDepth() : Integer.MAX_VALUE;
		}

		depth = containedContext != null ? containedContext.getDepth() : 0;
		if ( depth < Integer.MAX_VALUE ) { // Avoid integer overflow
			++depth;
		}

		/*
		 * Handle @IndexedEmbedded.includePaths-induced limits If the context for the contained element has a
		 * comprehensive set of included paths, and if the @IndexedEmbedded matching the @ContainedIn we're currently
		 * processing also has a comprehensive set of embedded paths, *then* we can compute the resulting set of
		 * embedded fields (which is the intersection of those two sets). If this resulting set is empty, we can safely
		 * stop the @ContainedIn processing: any changed field wouldn't be included in the Lucene document for
		 * "containerInstance" anyway.
		 */

		Set comprehensivePaths;
		Set metadataIncludePaths = containedInMetadata.getIncludePaths();

		/*
		 * See @IndexedEmbedded.depth: it should be considered as zero if it has its default value and if includePaths
		 * contains elements
		 */
		if ( metadataIncludePaths != null && !metadataIncludePaths.isEmpty()
				&& metadataMaxDepth != null && metadataMaxDepth.equals( Integer.MAX_VALUE ) ) {
			String metadataPrefix = containedInMetadata.getPrefix();

			/*
			 * If the contained context Filter by contained context's included paths if they are comprehensive This
			 * allows to detect when a @ContainedIn is irrelevant because the matching @IndexedEmbedded would not
			 * capture any property.
			 */
			Set containedComprehensivePaths =
					containedContext != null ? containedContext.getComprehensivePaths() : null;

			comprehensivePaths = new HashSet<>();
			for ( String includedPath : metadataIncludePaths ) {
				/*
				 * If the contained context has a comprehensive list of included paths, use it to filter out our own
				 * list
				 */
				if ( containedComprehensivePaths == null || containedComprehensivePaths.contains( includedPath ) ) {
					comprehensivePaths.add( metadataPrefix + includedPath );
				}
			}
		}
		else {
			comprehensivePaths = null;
		}

		return new ContainedInRecursionContext( maxDepth, depth, comprehensivePaths );
	}

	@Override
	public String toString() {
		return "DocumentBuilder for {" + beanClass.getName() + "}";
	}

	/**
	 * A {@code XMember } instance treats a map as a collection as well in which case the map values are returned as
	 * collection.
	 *
	 * @param member The member instance
	 * @param value The value
	 *
	 * @return The {@code value} cast to collection or in case of {@code value} being a map the map values as collection.
	 */
	@SuppressWarnings("unchecked")
	private  Collection getActualCollection(XMember member, Object value) {
		Collection collection;
		if ( Map.class.equals( member.getCollectionClass() ) ) {
			collection = ( (Map) value ).values();
		}
		else {
			collection = (Collection) value;
		}
		return collection;
	}

	private  void processSingleContainedInInstance(WorkPlan workplan, T value, ContainedInRecursionContext depth, String tenantId) {
		workplan.recurseContainedIn( value, depth, tenantId );
	}

	/**
	 * Hibernate entities might be dirty (their state has changed), but none of these changes would affect
	 * the index state. This method will return {@code true} if any of changed entity properties identified
	 * by their names ({@code dirtyPropertyNames}) will effect the index state.
	 *
	 * @param dirtyPropertyNames array of property names for the changed entity properties, {@code null} in case the
	 * changed properties cannot be specified.
	 *
	 * @return {@code true} if the entity changes will effect the index state, {@code false} otherwise
	 *
	 * @since 3.4
	 */
	public boolean isDirty(String[] dirtyPropertyNames) {
		if ( dirtyPropertyNames == null || dirtyPropertyNames.length == 0 ) {
			return true; // it appears some collection work has no oldState -> reindex
		}
		if ( !stateInspectionOptimizationsEnabled() ) {
			return true;
		}

		for ( String dirtyPropertyName : dirtyPropertyNames ) {
			PropertyMetadata propertyMetadata = typeMetadata.getPropertyMetadataForProperty( dirtyPropertyName );
			if ( propertyMetadata != null ) {
				// if there is a property metadata it means that there is at least one @Field.
				// Fields are either indexed or stored, so we need to re-index
				return true;
			}

			// consider IndexedEmbedded:
			for ( EmbeddedTypeMetadata embeddedTypeMetadata : typeMetadata.getEmbeddedTypeMetadata() ) {
				String name = embeddedTypeMetadata.getEmbeddedFieldName();
				if ( name.equals( dirtyPropertyName ) ) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * To be removed, see org.hibernate.search.engine.DocumentBuilderIndexedEntity.isIdMatchingJpaId()
	 *
	 * @return true if a providedId needs to be provided for indexing
	 */
	public boolean requiresProvidedId() {
		return false;
	}

	/**
	 * To be removed, see org.hibernate.search.engine.DocumentBuilderIndexedEntity.isIdMatchingJpaId()
	 *
	 * @return true if @DocumentId and @Id are found on the same property
	 */
	public boolean isIdMatchingJpaId() {
		return true;
	}

	/**
	 * Returns {@code true} if the collection event is going to affect the index state. In this case
	 * the indexing event can be ignored. {@code false} otherwise.
	 *
	 * @param collectionRoleName a {@link java.lang.String} object.
	 *
	 * @return {@code true} if an update to the collection identified by the given role name effects the index
	 * state, {@code false} otherwise.
	 */
	public boolean collectionChangeRequiresIndexUpdate(String collectionRoleName) {
		if ( collectionRoleName == null ) {
			// collection name will only be non null for PostCollectionUpdateEvents
			return true;
		}

		// don't check stateInspectionOptimizationsEnabled() as it might ignore depth limit:
		// it will disable optimization even if we have class bridges, but we're too deep
		// to be reachable. The evaluation of stateInspectionOptimizationsEnabled() was
		// actually stored in stateInspectionOptimizationsEnabled, but limiting to depth recursion.
		if ( !typeMetadata.areStateInspectionOptimizationsEnabled() ) {
			// if optimizations are not enabled we need to re-index
			return true;
		}

		return this.typeMetadata.containsCollectionRole( collectionRoleName );
	}

	/**
	 * Verifies entity level preconditions to know if it's safe to skip index updates based
	 * on specific field or collection updates.
	 *
	 * @return true if it seems safe to apply such optimizations
	 */
	boolean stateInspectionOptimizationsEnabled() {
		if ( !typeMetadata.areStateInspectionOptimizationsEnabled() ) {
			return false;
		}
		if ( typeMetadata.areClassBridgesUsed() ) {
			log.tracef(
					"State inspection optimization disabled as entity %s uses class bridges",
					this.beanClass.getName()
			);
			return false; // can't know what a class bridge is going to look at -> reindex // TODO nice new feature to have?
		}
		BoostStrategy boostStrategy = typeMetadata.getDynamicBoost();
		if ( boostStrategy != null && !( boostStrategy instanceof DefaultBoostStrategy ) ) {
			log.tracef(
					"State inspection optimization disabled as DynamicBoost is enabled on entity %s",
					this.beanClass.getName()
			);
			return false; // as with class bridge: might be affected by any field // TODO nice new feature to have?
		}
		return true;
	}

	/**
	 * Makes sure isCollectionRoleExcluded will always return false, so that
	 * collection update events are always processed.
	 *
	 * @see #collectionChangeRequiresIndexUpdate(String)
	 */
	public void forceStateInspectionOptimizationsDisabled() {
		typeMetadata.disableStateInspectionOptimizations();
	}

	/**
	 * Closes any resource
	 */
	public void close() {
		typeMetadata.getDefaultAnalyzerReference().close();
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy