/* * 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.event.impl; import java.io.Serializable; import java.util.Map; import java.util.concurrent.CompletableFuture; import javax.transaction.Status; import javax.transaction.Synchronization; import org.hibernate.Session; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.event.spi.AbstractCollectionEvent; import org.hibernate.event.spi.AbstractEvent; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.FlushEvent; import org.hibernate.event.spi.FlushEventListener; import org.hibernate.event.spi.PostCollectionRecreateEvent; import org.hibernate.event.spi.PostCollectionRecreateEventListener; import org.hibernate.event.spi.PostCollectionRemoveEvent; import org.hibernate.event.spi.PostCollectionRemoveEventListener; import org.hibernate.event.spi.PostCollectionUpdateEvent; import org.hibernate.event.spi.PostCollectionUpdateEventListener; import org.hibernate.event.spi.PostDeleteEvent; import org.hibernate.event.spi.PostDeleteEventListener; import org.hibernate.event.spi.PostInsertEvent; import org.hibernate.event.spi.PostInsertEventListener; import org.hibernate.event.spi.PostUpdateEvent; import org.hibernate.event.spi.PostUpdateEventListener; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.search.backend.impl.EventSourceTransactionContext; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.engine.integration.impl.ExtendedSearchIntegrator; import org.hibernate.search.engine.spi.AbstractDocumentBuilder; import org.hibernate.search.engine.spi.EntityIndexBinding; import org.hibernate.search.spi.IndexingMode; import org.hibernate.search.util.impl.Maps; import org.hibernate.search.util.logging.impl.Log; import org.hibernate.search.util.logging.impl.LoggerFactory; /** * Hibernate ORM event listener called by various ORM life cycle events. This listener must be registered in order * to enable automatic index updates. * * @author Gavin King * @author Emmanuel Bernard * @author Mattias Arbin * @author Sanne Grinovero * @author Hardy Ferentschik */ @SuppressWarnings("serial") public final class FullTextIndexEventListener implements PostDeleteEventListener, PostInsertEventListener, PostUpdateEventListener, PostCollectionRecreateEventListener, PostCollectionRemoveEventListener, PostCollectionUpdateEventListener, FlushEventListener { private static final Log log = LoggerFactory.make(); private volatile EventsIntegratorState state = new NonInitializedIntegratorState(); //only used by the FullTextIndexEventListener instance playing in the FlushEventListener role. // make sure the Synchronization doesn't contain references to Session, otherwise we'll leak memory. private final Map<Session, Synchronization> flushSynch = Maps.createIdentityWeakKeyConcurrentMap( 64, 32 ); @Override public void onPostDelete(PostDeleteEvent event) { if ( state.eventsDisabled() ) { return; } final Object entity = event.getEntity(); if ( getDocumentBuilder( entity ) != null ) { // FIXME The engine currently needs to know about details such as identifierRollbackEnabled // but we should not move the responsibility to figure out the proper id to the engine boolean identifierRollbackEnabled = event.getSession() .getFactory() .getSettings() .isIdentifierRollbackEnabled(); processWork( tenantIdentifier( event ), entity, event.getId(), WorkType.DELETE, event, identifierRollbackEnabled ); } } @Override public void onPostInsert(PostInsertEvent event) { if ( state.eventsDisabled() ) { return; } final Object entity = event.getEntity(); if ( getDocumentBuilder( entity ) != null ) { Serializable id = event.getId(); processWork( tenantIdentifier( event ), entity, id, WorkType.ADD, event, false ); } } private String tenantIdentifier(AbstractEvent event) { EventSource session = event.getSession(); return session.getTenantIdentifier(); } @Override public void onPostUpdate(PostUpdateEvent event) { if ( state.eventsDisabled() ) { return; } final Object entity = event.getEntity(); final AbstractDocumentBuilder docBuilder = getDocumentBuilder( entity ); if ( docBuilder != null && ( state.skipDirtyChecks() || docBuilder.isDirty( getDirtyPropertyNames( event ) ) ) ) { Serializable id = event.getId(); processWork( tenantIdentifier( event ), entity, id, WorkType.UPDATE, event, false ); } } @Override public void onPostRecreateCollection(PostCollectionRecreateEvent event) { processCollectionEvent( event ); } @Override public void onPostRemoveCollection(PostCollectionRemoveEvent event) { processCollectionEvent( event ); } @Override public void onPostUpdateCollection(PostCollectionUpdateEvent event) { processCollectionEvent( event ); } /** * Make sure the indexes are updated right after the hibernate flush, * avoiding object loading during a flush. Not needed during transactions. */ @Override public void onFlush(FlushEvent event) { if ( state.eventsDisabled() ) { return; } Session session = event.getSession(); Synchronization synchronization = flushSynch.get( session ); if ( synchronization != null ) { //first cleanup flushSynch.remove( session ); log.debug( "flush event causing index update out of transaction" ); synchronization.beforeCompletion(); synchronization.afterCompletion( Status.STATUS_COMMITTED ); } } public ExtendedSearchIntegrator getExtendedSearchFactoryIntegrator() { return state.getExtendedSearchIntegrator(); } public String[] getDirtyPropertyNames(PostUpdateEvent event) { EntityPersister persister = event.getPersister(); final int[] dirtyProperties = event.getDirtyProperties(); if ( dirtyProperties != null && dirtyProperties.length > 0 ) { String[] propertyNames = persister.getPropertyNames(); int length = dirtyProperties.length; String[] dirtyPropertyNames = new String[length]; for ( int i = 0; i < length; i++ ) { dirtyPropertyNames[i] = propertyNames[dirtyProperties[i]]; } return dirtyPropertyNames; } else { return null; } } /** * Initialize method called by Hibernate Core when the SessionFactory starts. * @param extendedIntegratorFuture a completable future that will eventually hold the {@link ExtendedSearchIntegrator} */ public void initialize(CompletableFuture<ExtendedSearchIntegrator> extendedIntegratorFuture) { this.state = new InitializingIntegratorState( extendedIntegratorFuture.thenApply( this::doInitialize ) ); } private ExtendedSearchIntegrator doInitialize(ExtendedSearchIntegrator extendedIntegrator) { final boolean disabled = eventsDisabled( extendedIntegrator ); final boolean skipDirtyChecks = skipDirtyChecks( extendedIntegrator ); log.debug( "Hibernate Search event listeners " + ( disabled ? "deactivated" : "activated" ) ); if ( ! disabled ) { log.debug( "Hibernate Search dirty checks " + ( skipDirtyChecks ? "disabled" : "enabled" ) ); } OptimalEventsIntegratorState newState = new OptimalEventsIntegratorState( disabled, skipDirtyChecks, extendedIntegrator ); this.state = newState; // discard the suboptimal EventsIntegratorState instances return extendedIntegrator; } /** * Adds a synchronization to be performed in the onFlush method; * should only be used as workaround for the case a flush is happening * out of transaction. * Warning: if the synchronization contains a hard reference * to the Session proper cleanup is not guaranteed and memory leaks * will happen. * * @param eventSource should be the Session doing the flush * @param synchronization the synchronisation instance */ public void addSynchronization(EventSource eventSource, Synchronization synchronization) { this.flushSynch.put( eventSource, synchronization ); } protected void processWork(String tenantIdentifier, Object entity, Serializable id, WorkType workType, AbstractEvent event, boolean identifierRollbackEnabled) { Work work = new Work( tenantIdentifier, entity, id, workType, identifierRollbackEnabled ); final EventSourceTransactionContext transactionContext = new EventSourceTransactionContext( event.getSession() ); getExtendedSearchFactoryIntegrator().getWorker().performWork( work, transactionContext ); } protected void processCollectionEvent(AbstractCollectionEvent event) { if ( state.eventsDisabled() ) { return; } Object entity = event.getAffectedOwnerOrNull(); if ( entity == null ) { //Hibernate cannot determine every single time the owner especially in case detached objects are involved // or property-ref is used //Should log really but we don't know if we're interested in this collection for indexing return; } PersistentCollection persistentCollection = event.getCollection(); final String collectionRole; if ( persistentCollection != null ) { if ( !persistentCollection.wasInitialized() ) { // non-initialized collections will still trigger events, but we want to skip them // as they won't contain new values affecting the index state return; } collectionRole = persistentCollection.getRole(); } else { collectionRole = null; } AbstractDocumentBuilder documentBuilder = getDocumentBuilder( entity ); if ( documentBuilder != null && documentBuilder.collectionChangeRequiresIndexUpdate( collectionRole ) ) { Serializable id = getId( entity, event ); if ( id == null ) { log.idCannotBeExtracted( event.getAffectedOwnerEntityName() ); return; } processWork( tenantIdentifier( event ), entity, id, WorkType.COLLECTION, event, false ); } } private Serializable getId(Object entity, AbstractCollectionEvent event) { Serializable id = event.getAffectedOwnerIdOrNull(); if ( id == null ) { // most likely this recovery is unnecessary since Hibernate Core probably try that EntityEntry entityEntry = event.getSession().getPersistenceContext().getEntry( entity ); id = entityEntry == null ? null : entityEntry.getId(); } return id; } /** * It is not suggested to extend FullTextIndexEventListener, but when needed to implement special * use cases implementors might need this method. If you have to extent this, please report * your use case so that better long term solutions can be discussed. * * @param instance the object instance for which to retrieve the document builder * * @return the {@code DocumentBuilder} for the specified object */ protected AbstractDocumentBuilder getDocumentBuilder(final Object instance) { ExtendedSearchIntegrator integrator = getExtendedSearchFactoryIntegrator(); Class<?> clazz = instance.getClass(); EntityIndexBinding entityIndexBinding = integrator.getIndexBinding( clazz ); if ( entityIndexBinding != null ) { return entityIndexBinding.getDocumentBuilder(); } else { return integrator.getDocumentBuilderContainedEntity( clazz ); } } /** * Required since Hibernate ORM 4.3 */ @Override public boolean requiresPostCommitHanding(EntityPersister persister) { // TODO Tests seem to pass using _false_ but we might be able to take // advantage of this new hook? return false; } public static boolean skipDirtyChecks(ExtendedSearchIntegrator extendedIntegrator) { return !extendedIntegrator.isDirtyChecksEnabled(); } public static boolean eventsDisabled(ExtendedSearchIntegrator extendedIntegrator) { if ( extendedIntegrator.getIndexingMode() == IndexingMode.EVENT ) { return extendedIntegrator.getIndexBindings().size() == 0; } else if ( extendedIntegrator.getIndexingMode() == IndexingMode.MANUAL ) { return true; } else { return false; } } }