package org.infinispan.query.backend; import java.util.ArrayList; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import javax.transaction.Transaction; import org.hibernate.search.spi.SearchIntegrator; import org.infinispan.notifications.Listener; import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified; import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent; import org.infinispan.notifications.cachelistener.event.CacheEntryModifiedEvent; import org.infinispan.query.logging.Log; import org.infinispan.util.KeyValuePair; import org.infinispan.util.logging.LogFactory; /** * Wrapper around SearchIntegrator with guards to allow concurrent access * * @author gustavonalle * @since 7.0 */ final class SearchFactoryHandler { private static final Log log = LogFactory.getLog(SearchFactoryHandler.class, Log.class); private final SearchIntegrator searchFactory; private final QueryKnownClasses queryKnownClasses; private final TransactionHelper transactionHelper; private final Object cacheListener = new CacheListener(); private final ReentrantLock mutating = new ReentrantLock(); SearchFactoryHandler(final SearchIntegrator searchFactory, final QueryKnownClasses queryKnownClasses, final TransactionHelper transactionHelper) { this.searchFactory = searchFactory; this.queryKnownClasses = queryKnownClasses; this.transactionHelper = transactionHelper; } boolean updateKnownTypesIfNeeded(final Object value) { if (value != null) { final Class<?> potentialNewType = value.getClass(); final Boolean existingBoolean = queryKnownClasses.get(potentialNewType); if (existingBoolean != null) { return existingBoolean; } else { handleOnDemandRegistration(false, potentialNewType); Boolean isIndexable = queryKnownClasses.get(potentialNewType); return isIndexable != null ? isIndexable : false; } } else { return false; } } private void handleOnDemandRegistration(boolean allowUndeclared, Class<?>... classes) { List<Class<?>> reducedSet = new ArrayList<>(classes.length); for (Class<?> type : classes) { if (!queryKnownClasses.containsKey(type)) { reducedSet.add(type); } } if (!reducedSet.isEmpty()) { if (queryKnownClasses.isAutodetectEnabled()) { Class<?>[] toAdd = reducedSet.toArray(new Class[reducedSet.size()]); updateSearchFactory(toAdd); for (Class<?> c : toAdd) { queryKnownClasses.put(c, hasIndex(c)); } } else if (!allowUndeclared) { log.detectedUnknownIndexedEntities(queryKnownClasses.getCacheName(), reducedSet.toString()); } } } private void updateSearchFactory(final Class<?>... classes) { mutating.lock(); try { //Need to re-filter the new types while holding the lock final List<Class<?>> reducedSet = new ArrayList<>(classes.length); for (Class<?> type : classes) { if (!hasIndex(type)) { reducedSet.add(type); } } if (reducedSet.isEmpty()) { return; } final Class<?>[] newtypes = reducedSet.toArray(new Class<?>[reducedSet.size()]); Transaction tx = transactionHelper.suspendTxIfExists(); try { searchFactory.addClasses(newtypes); } finally { transactionHelper.resume(tx); } for (Class<?> type : newtypes) { if (hasIndex(type)) { log.detectedUnknownIndexedEntity(queryKnownClasses.getCacheName(), type.getName()); } } } finally { mutating.unlock(); } } /** * Checks if an index exists for the given class. This is not intended to test whether the entity class is indexable * (via annotations or programmatically). * * @param c the class to check * @return true if an index exists, false otherwise */ boolean hasIndex(final Class<?> c) { return searchFactory.getIndexBinding(c) != null; } private void handleClusterRegistryRegistration(final Class<?> clazz) { if (hasIndex(clazz)) { return; } updateSearchFactory(clazz); } void enableClasses(Class[] classes) { handleOnDemandRegistration(true, classes); } Object getCacheListener() { return cacheListener; } /** * A listener used to update the SearchFactoryHandler when an indexable class is added to cache. */ @Listener final class CacheListener { @CacheEntryCreated public void created(CacheEntryCreatedEvent<KeyValuePair<String, Class>, Boolean> e) { if (!e.isOriginLocal() && !e.isPre() && e.getValue()) { handleClusterRegistryRegistration(e.getKey().getValue()); } } @CacheEntryModified public void modified(CacheEntryModifiedEvent<KeyValuePair<String, Class>, Boolean> e) { if (!e.isOriginLocal() && !e.isPre() && e.getValue()) { handleClusterRegistryRegistration(e.getKey().getValue()); } } } }