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

org.hibernate.search.engine.impl.WorkPlan 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.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.hibernate.search.exception.AssertionFailure;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.backend.LuceneWork;
import org.hibernate.search.backend.PurgeAllLuceneWork;
import org.hibernate.search.backend.spi.Work;
import org.hibernate.search.backend.spi.WorkType;
import org.hibernate.search.bridge.spi.ConversionContext;
import org.hibernate.search.bridge.util.impl.ContextualExceptionBridgeHelper;
import org.hibernate.search.engine.integration.impl.SearchFactoryImplementor;
import org.hibernate.search.engine.spi.AbstractDocumentBuilder;
import org.hibernate.search.engine.spi.DepthValidator;
import org.hibernate.search.engine.spi.DocumentBuilderContainedEntity;
import org.hibernate.search.engine.spi.EntityIndexBinding;
import org.hibernate.search.indexes.interceptor.EntityIndexingInterceptor;
import org.hibernate.search.indexes.interceptor.IndexingOverride;
import org.hibernate.search.spi.InstanceInitializer;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * Represents the set of changes going to be applied to the index for the entities. A stream of Work is feed as input, a
 * list of LuceneWork is output, and in the process we try to reduce the number of output operations to the minimum
 * needed to reach the same final state.
 *
 * @author Sanne Grinovero
 * @author Hardy Ferentschik
 * @since 3.3
 */
@SuppressWarnings( { "rawtypes", "unchecked" })
public class WorkPlan {

	private static final Log log = LoggerFactory.make();

	private final HashMap, PerClassWork> byClass = new HashMap, PerClassWork>();

	private final SearchFactoryImplementor searchFactoryImplementor;

	private final InstanceInitializer instanceInitializer;

	/**
	 * most work is split in two, some other might cancel one or more existing works,
	 * we don't track the number accurately as that's not needed.
	 */
	private int approximateWorkQueueSize = 0;

	public WorkPlan(SearchFactoryImplementor searchFactoryImplementor) {
		this.searchFactoryImplementor = searchFactoryImplementor;
		this.instanceInitializer = searchFactoryImplementor.getInstanceInitializer();
	}

	/**
	 * Adds a work to be performed as part of the final plan.
	 *
	 * @param work The work instance to add to the work plan
	 */
	public void addWork(Work work) {
		approximateWorkQueueSize++;
		Class entityClass = instanceInitializer.getClassFromWork( work );
		PerClassWork classWork = getClassWork( entityClass );
		classWork.addWork( work );
	}

	/**
	 * Removes all scheduled work
	 */
	public void clear() {
		byClass.clear();
		approximateWorkQueueSize = 0;
	}

	/**
	 * Returns an approximation of the amount of work in the queue.
	 * This is meant for resource control for auto flushing of large pending batches.
	 *
	 * @return the approximation
	 * @see org.hibernate.search.cfg.Environment#QUEUEINGPROCESSOR_BATCHSIZE
	 */
	public int size() {
		return approximateWorkQueueSize;
	}

	/**
	 * @param entityClass The entity class for which to retrieve the work
	 *
	 * @return returns (and creates if needed) the {@code PerClassWork} from the {@link #byClass} map.
	 */
	private PerClassWork getClassWork(Class entityClass) {
		PerClassWork classWork = byClass.get( entityClass );
		if ( classWork == null ) {
			classWork = new PerClassWork( entityClass );
			byClass.put( entityClass, classWork );
		}
		return classWork;
	}

	/**
	 * Makes sure that all additional work needed because of containedIn
	 * is added to the work plan.
	 */
	public void processContainedInAndPrepareExecution() {
		PerClassWork[] worksFromEvents = new PerClassWork[byClass.size()];
		worksFromEvents = byClass.values().toArray( worksFromEvents );

		// We need to iterate on a "frozen snapshot" of the byClass values
		// because of HSEARCH-647. This method is not recursive, invoked
		// only after the current unit of work is complete, and all additional
		// work we add through recursion is already complete, so we don't need
		// to process again new classes we add during the process.
		for ( PerClassWork perClassWork : worksFromEvents ) {
			perClassWork.processContainedInAndPrepareExecution();
		}
	}

	/**
	 * Used for recursive processing of containedIn
	 *
	 * @param value the entity to be processed
	 */
	public  void recurseContainedIn(T value, DepthValidator depth) {
		Class entityClass = instanceInitializer.getClass( value );
		PerClassWork classWork = getClassWork( entityClass );
		classWork.recurseContainedIn( value, depth );
	}

	/**
	 * @return returns the current plan converted as a list of {@code LuceneWork}
	 */
	public List getPlannedLuceneWork() {
		List luceneQueue = new ArrayList();
		for ( PerClassWork perClassWork : byClass.values() ) {
			perClassWork.enqueueLuceneWork( luceneQueue );
		}
		return luceneQueue;
	}

	/**
	 * {@code PerClassWork} organizes work per entity type.
	 */
	class PerClassWork {

		/**
		 * We further organize work per entity identifier so that we can cancel or adapt work being done
		 * on the same entities.
		 * This map uses as key what we originally received as {@link Work#getId()} if the type
		 * is annotated with @ProvidedId, otherwise it uses the value pointed to by
		 * {@link org.hibernate.search.annotations.DocumentId} or as last attempt {@code javax.persistence.Id}.
		 */
		private final Map entityById = new HashMap();

		/**
		 * When a PurgeAll operation is send on the type, we can remove all previously scheduled work
		 * and remember that the first operation on the index is going to be a purge all.
		 */
		private boolean purgeAll = false;

		/**
		 * The type of all classes being managed
		 */
		private final Class entityClass;

		/**
		 * The DocumentBuilder relative to the type being managed
		 */
		private final AbstractDocumentBuilder documentBuilder;

		/**
		 * The entity {@link #entityClass} does not have its own index, but is only used in contained scenarios
		 */
		private final boolean containedInOnly;

		/**
		 * @param clazz The type of entities being managed by this instance
		 */
		PerClassWork(Class clazz) {
			this.entityClass = clazz;
			this.documentBuilder = getEntityBuilder( searchFactoryImplementor, clazz );
			this.containedInOnly = documentBuilder instanceof DocumentBuilderContainedEntity;
		}

		/**
		 * Adds a work to the current plan. The entityClass of the work must be of the
		 * type managed by this.
		 *
		 * @param work the {@code Work} instance to add to the plan
		 */
		public void addWork(Work work) {
			if ( work.getType() == WorkType.PURGE_ALL ) {
				entityById.clear();
				purgeAll = true;
			}
			else {
				Serializable id = extractProperId( work );
				PerEntityWork entityWork = entityById.get( id );
				if ( entityWork == null ) {
					entityWork = new PerEntityWork( work );
					entityById.put( id, entityWork );
				}
				entityWork.addWork( work );
			}
		}

		/**
		 * We need to make a difference on which value is used as identifier
		 * according to use case and mapping options
		 *
		 * @param work The work instance from which to extract the id
		 *
		 * @return the appropriate id to use for this work
		 */
		private Serializable extractProperId(Work work) {
			// see HSEARCH-662
			if ( containedInOnly ) {
				return work.getId();
			}

			Object entity = work.getEntity();
			// 1) entity is null for purge operation, which requires to trust the work id
			// 2) types mapped as provided id require to use the work id
			// 3) when Hibernate identifier rollback is used && this identifier is our same id source, we need to get the value from work id
			if ( entity == null
					|| documentBuilder.requiresProvidedId()
					|| ( work.isIdentifierWasRolledBack() && documentBuilder.isIdMatchingJpaId() ) ) {
				return work.getId();
			}
			else {
				return documentBuilder.getId( entity );
			}
		}

		/**
		 * Enqueues all work needed to be performed according to current state into
		 * the LuceneWork queue.
		 *
		 * @param luceneQueue work will be appended to this list
		 */
		public void enqueueLuceneWork(List luceneQueue) {
			final Set> entityInstances = entityById.entrySet();
			ConversionContext conversionContext = new ContextualExceptionBridgeHelper();
			if ( purgeAll ) {
				luceneQueue.add( new PurgeAllLuceneWork( entityClass ) );
			}
			for ( Entry entry : entityInstances ) {
				Serializable indexingId = entry.getKey();
				PerEntityWork perEntityWork = entry.getValue();
				perEntityWork.enqueueLuceneWork( entityClass, indexingId, documentBuilder, luceneQueue, conversionContext );
			}
		}

		/**
		 * Starts processing the {@code ContainedIn} annotation for all instances stored in
		 * {@link #entityById}.
		 *
		 * This processing must be performed when no more work is being collected by the event
		 * system. The processing might recursively add more work to the plan.
		 */
		public void processContainedInAndPrepareExecution() {
			Entry[] entityInstancesFrozenView = new Entry[entityById.size()];
			entityInstancesFrozenView = entityById.entrySet().toArray( entityInstancesFrozenView );
			for ( Entry entry : entityInstancesFrozenView ) {
				PerEntityWork perEntityWork = entry.getValue();
				perEntityWork.processContainedIn( documentBuilder, WorkPlan.this );
			}
		}

		/**
		 * Method to continue the recursion for ContainedIn processing, as started by {@link #processContainedInAndPrepareExecution()}
		 * Additional work that needs to be processed will be added to this same WorkPlan.
		 *
		 * @param value the instance to be processed
		 */
		void recurseContainedIn(Object value, DepthValidator depth) {
			if ( documentBuilder.requiresProvidedId() ) {
				log.containedInPointsToProvidedId( instanceInitializer.getClass( value ) );
			}
			else {
				Serializable extractedId = documentBuilder.getId( value );
				if ( extractedId != null ) {
					PerEntityWork entityWork = entityById.get( extractedId );
					if ( entityWork == null ) {
						EntityIndexingInterceptor entityInterceptor = getEntityInterceptor();
						IndexingOverride operation;
						if ( entityInterceptor != null ) {
							operation = entityInterceptor.onUpdate( value );
						}
						else {
							operation = IndexingOverride.APPLY_DEFAULT;
						}
						//TODO there is a small duplication with some of TransactionalWorker.interceptWork
						//     but what would be a proper factored solution?
						switch ( operation ) {
							//we are planning an update by default
							case UPDATE:
							case APPLY_DEFAULT:
								entityWork = new PerEntityWork( value );
								entityById.put( extractedId, entityWork );
								break;
							case SKIP:
								log.forceSkipIndexOperationViaInterception( entityClass, WorkType.UPDATE );
								break;
							case REMOVE:
								log.forceRemoveOnIndexOperationViaInterception( entityClass, WorkType.UPDATE );
								Work work = new Work(value, extractedId, WorkType.DELETE);
								entityWork = new PerEntityWork( work );
								entityById.put( extractedId, entityWork );
								break;
							default:
								throw new AssertionFailure( "Unknown action type: " + operation );
						}
						// recursion starts
						documentBuilder.appendContainedInWorkForInstance( value, WorkPlan.this, depth );
					}
					// else nothing to do as it's being processed already
				}
				else {
					// this branch for @ContainedIn recursive work of non-indexed entities
					// as they don't have an indexingId
					documentBuilder.appendContainedInWorkForInstance( value, WorkPlan.this, depth );
				}
			}
		}

		private EntityIndexingInterceptor getEntityInterceptor() {
			EntityIndexBinding indexBindingForEntity = searchFactoryImplementor.getIndexBinding(
					entityClass
			);
			return indexBindingForEntity != null ? indexBindingForEntity.getEntityIndexingInterceptor() : null;
		}
	}

	/**
	 * Keeps track of what needs to be done Lucene wise for each entity.
	 * Each entity might need to be deleted from the index, added to the index,
	 * or both; in this case delete will be performed first.
	 */
	private static class PerEntityWork {

		private Object entity;

		/**
		 * When true, the Lucene Document representing this entity will be deleted
		 * from the index.
		 */
		private boolean delete = false;

		/**
		 * When true, the entity will be converted to a Lucene Document and added
		 * to the index.
		 */
		private boolean add = false;

		/**
		 * Needed to stop recursion for processing ContainedIn
		 * of already processed instances.
		 */
		private boolean containedInProcessed = false;

		/**
		 * Constructor to force an update of the entity even without
		 * having a specific Work instance for it.
		 *
		 * @param entity the instance which needs to be updated in the index
		 */
		private PerEntityWork(Object entity) {
			// for updates only
			this.entity = entity;
			this.delete = true;
			this.add = true;
			this.containedInProcessed = true;
		}

		/**
		 * Prepares the initial state of planned changes according
		 * to the type of work being fired.
		 *
		 * @param work the work instance
		 */
		private PerEntityWork(Work work) {
			entity = work.getEntity();
			WorkType type = work.getType();
			// sets the initial state:
			switch ( type ) {
				case ADD:
					add = true;
					break;
				case DELETE:
				case PURGE:
					delete = true;
					break;
				case COLLECTION:
				case UPDATE:
					delete = true;
					add = true;
					break;
				case INDEX:
					add = true;
					delete = true;
					break;
				case PURGE_ALL:
					// not breaking intentionally: PURGE_ALL should not reach this
					// class
				default:
					throw new SearchException( "unexpected state:" + type );
			}
		}

		/**
		 * Has different effects depending on the new type of work needed
		 * and the previous scheduled work.
		 * This way we never store more than a plan for each entity and order
		 * of final execution is irrelevant, what matters is the order in which the
		 * work is added to the plan.
		 *
		 * @param work the work instance to add
		 */
		public void addWork(Work work) {
			entity = work.getEntity();
			WorkType type = work.getType();
			switch ( type ) {
				case INDEX:
				case UPDATE:
					if ( add && !delete ) {
						// noop: the entity was newly created in this same unit of work
						// so it needs to be added no need to delete
					}
					else {
						add = true;
						delete = true;
					}
					break;
				case ADD: // Is the only operation which doesn't imply a delete-before-add
					add = true;
					// leave delete flag as-is
					break;
				case DELETE:
				case PURGE:
					if ( add && !delete ) {
						// the entity was was newly created in this same unit of
						// work so works counter each other
						add = false;
					}
					else {
						add = false;
						delete = true;
					}
					break;
				case COLLECTION:
					if ( !add && !delete ) {
						add = true;
						delete = true;
					}
					// nothing to do, as something else was done
					break;
				case PURGE_ALL:
				default:
					throw new SearchException( "unexpected state:" + type );
			}
		}

		/**
		 * Adds the needed LuceneWork to the queue for this entity instance
		 *
		 * @param entityClass the type
		 * @param indexingId identifier of the instance
		 * @param entityBuilder the DocumentBuilder for this type
		 * @param luceneQueue the queue collecting all changes
		 */
		public void enqueueLuceneWork(Class entityClass, Serializable indexingId, AbstractDocumentBuilder entityBuilder,
				List luceneQueue, ConversionContext conversionContext) {
			if ( add || delete ) {
				entityBuilder.addWorkToQueue( entityClass, entity, indexingId, delete, add, luceneQueue, conversionContext );
			}
		}

		/**
		 * Works via recursion passing the WorkPlan over, so that additional work can be planned
		 * according to the needs of ContainedIn processing.
		 *
		 * @param entityBuilder the DocumentBuilder for this type
		 * @param workplan the current WorkPlan, used for recursion
		 *
		 * @see org.hibernate.search.annotations.ContainedIn
		 */
		public void processContainedIn(AbstractDocumentBuilder entityBuilder, WorkPlan workplan) {
			if ( entity != null && !containedInProcessed ) {
				containedInProcessed = true;
				if ( add || delete ) {
					entityBuilder.appendContainedInWorkForInstance( entity, workplan, null );
				}
			}
		}
	}

	/**
	 * Get and cache the DocumentBuilder for this type. Being this a perClassWork
	 * we can fetch it once.
	 *
	 * @param searchFactoryImplementor the search factory (implementor)
	 * @param entityClass the entity type for which to retrieve the document builder
	 *
	 * @return the DocumentBuilder for this type
	 */
	private static AbstractDocumentBuilder getEntityBuilder(SearchFactoryImplementor searchFactoryImplementor, Class entityClass) {
		EntityIndexBinding entityIndexBinding = searchFactoryImplementor.getIndexBinding( entityClass );
		if ( entityIndexBinding == null ) {
			DocumentBuilderContainedEntity entityBuilder = searchFactoryImplementor.getDocumentBuilderContainedEntity(
					entityClass
			);
			if ( entityBuilder == null ) {
				// should never happen but better be safe than sorry
				throw new SearchException(
						"Unable to perform work. Entity Class is not @Indexed nor hosts @ContainedIn: " + entityClass
				);
			}
			else {
				return entityBuilder;
			}
		}
		else {
			return entityIndexBinding.getDocumentBuilder();
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy