package org.infinispan.query.backend; import java.io.Serializable; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicBoolean; import javax.transaction.TransactionManager; import javax.transaction.TransactionSynchronizationRegistry; import org.hibernate.search.backend.TransactionContext; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.backend.spi.Worker; import org.hibernate.search.spi.SearchIntegrator; import org.infinispan.Cache; import org.infinispan.commands.FlagAffectedCommand; import org.infinispan.commands.tx.PrepareCommand; import org.infinispan.commands.write.ClearCommand; import org.infinispan.commands.write.PutKeyValueCommand; import org.infinispan.commands.write.PutMapCommand; import org.infinispan.commands.write.RemoveCommand; import org.infinispan.commands.write.ReplaceCommand; import org.infinispan.commands.write.WriteCommand; import org.infinispan.commons.util.EnumUtil; import org.infinispan.compat.TypeConverter; import org.infinispan.container.DataContainer; import org.infinispan.container.entries.InternalCacheEntry; import org.infinispan.context.InvocationContext; import org.infinispan.context.impl.FlagBitSets; import org.infinispan.context.impl.TxInvocationContext; import org.infinispan.distribution.DistributionManager; import org.infinispan.factories.KnownComponentNames; import org.infinispan.factories.annotations.ComponentName; import org.infinispan.factories.annotations.Inject; import org.infinispan.factories.annotations.Start; import org.infinispan.factories.annotations.Stop; import org.infinispan.interceptors.DDAsyncInterceptor; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.query.Transformer; import org.infinispan.query.impl.DefaultSearchWorkCreator; import org.infinispan.query.logging.Log; import org.infinispan.registry.InternalCacheRegistry; import org.infinispan.remoting.rpc.RpcManager; import org.infinispan.util.logging.LogFactory; /** * This interceptor will be created when the System Property "infinispan.query.indexLocalOnly" is "false" * <p/> * This type of interceptor will allow the indexing of data even when it comes from other caches within a cluster. * <p/> * However, if the a cache would not be putting the data locally, the interceptor will not index it. * * @author Navin Surtani * @author Sanne Grinovero <sanne@hibernate.org> (C) 2011 Red Hat Inc. * @author Marko Luksa * @author anistor@redhat.com * @since 4.0 */ public final class QueryInterceptor extends DDAsyncInterceptor { private final IndexModificationStrategy indexingMode; private final SearchIntegrator searchFactory; private final KeyTransformationHandler keyTransformationHandler = new KeyTransformationHandler(); private final AtomicBoolean stopping = new AtomicBoolean(false); private QueryKnownClasses queryKnownClasses; private SearchWorkCreator<Object> searchWorkCreator = new DefaultSearchWorkCreator<>(); private SearchFactoryHandler searchFactoryHandler; private DataContainer dataContainer; protected TransactionManager transactionManager; protected TransactionSynchronizationRegistry transactionSynchronizationRegistry; private DistributionManager distributionManager; private RpcManager rpcManager; protected ExecutorService asyncExecutor; protected TypeConverter typeConverter; private static final Log log = LogFactory.getLog(QueryInterceptor.class, Log.class); /** * The classes declared by the indexing config as indexable. In 8.2 this can be null, indicating that no classes * were declared and we are running in the (deprecated) autodetect mode. Autodetect mode will be removed in 9.0. */ private Class<?>[] indexedEntities; public QueryInterceptor(SearchIntegrator searchFactory, IndexModificationStrategy indexingMode) { this.searchFactory = searchFactory; this.indexingMode = indexingMode; } @Inject @SuppressWarnings("unused") protected void injectDependencies(TransactionManager transactionManager, TransactionSynchronizationRegistry transactionSynchronizationRegistry, Cache cache, EmbeddedCacheManager cacheManager, InternalCacheRegistry internalCacheRegistry, DistributionManager distributionManager, RpcManager rpcManager, DataContainer dataContainer, @ComponentName(KnownComponentNames.ASYNC_TRANSPORT_EXECUTOR) ExecutorService e, TypeConverter typeConverter) { this.transactionManager = transactionManager; this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; this.distributionManager = distributionManager; this.rpcManager = rpcManager; this.asyncExecutor = e; this.dataContainer = dataContainer; Set<Class<?>> indexedEntities = cache.getCacheConfiguration().indexing().indexedEntities(); this.indexedEntities = indexedEntities.isEmpty() ? null : indexedEntities.toArray(new Class<?>[indexedEntities.size()]); this.queryKnownClasses = indexedEntities.isEmpty() ? new QueryKnownClasses(cache.getName(), cacheManager, internalCacheRegistry) : new QueryKnownClasses(indexedEntities); this.searchFactoryHandler = new SearchFactoryHandler(this.searchFactory, this.queryKnownClasses, new TransactionHelper(transactionManager)); this.typeConverter = typeConverter; } @Start protected void start() { if (indexedEntities == null) { queryKnownClasses.start(searchFactoryHandler); Set<Class<?>> classes = queryKnownClasses.keys(); Class<?>[] classesArray = classes.toArray(new Class<?>[classes.size()]); //Important to enable them all in a single call, much more efficient: enableClasses(classesArray); } stopping.set(false); } @Stop protected void stop() { queryKnownClasses.stop(); } public void prepareForStopping() { stopping.set(true); } protected boolean shouldModifyIndexes(FlagAffectedCommand command, InvocationContext ctx, Object key) { return indexingMode.shouldModifyIndexes(command, ctx, distributionManager, rpcManager, key); } /** * Use this executor for Async operations */ public ExecutorService getAsyncExecutor() { return asyncExecutor; } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> processPutKeyValueCommand(((PutKeyValueCommand) rCommand), rCtx, rv, null)); } @Override public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { // remove the object out of the cache first. return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> processRemoveCommand(((RemoveCommand) rCommand), rCtx, rv, null)); } @Override public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> processReplaceCommand(((ReplaceCommand) rCommand), rCtx, rv, null)); } @Override public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { command.setFlagsBitSet(EnumUtil.diffBitSets(command.getFlagsBitSet(), FlagBitSets.IGNORE_RETURN_VALUES)); return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { Map<Object, Object> previousValues = (Map<Object, Object>) rv; processPutMapCommand(((PutMapCommand) rCommand), rCtx, previousValues, null); }); } @Override public Object visitClearCommand(final InvocationContext ctx, final ClearCommand command) throws Throwable { // This method is called when somebody calls a cache.clear() and we will need to wipe everything in the indexes. return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> processClearCommand(((ClearCommand) rCommand), rCtx, null)); } /** * Remove all entries from all known indexes */ public void purgeAllIndexes() { purgeAllIndexes(null); } public void purgeIndex(Class<?> entityType) { purgeIndex(null, entityType); } private void purgeIndex(TransactionContext transactionContext, Class<?> entityType) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; Boolean isIndexable = queryKnownClasses.get(entityType); if (isIndexable != null && isIndexable.booleanValue()) { if (searchFactoryHandler.hasIndex(entityType)) { performSearchWorks(searchWorkCreator.createPerEntityTypeWorks((Class<Object>) entityType, WorkType.PURGE_ALL), transactionContext); } } } private void purgeAllIndexes(TransactionContext transactionContext) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; for (Class c : queryKnownClasses.keys()) { if (searchFactoryHandler.hasIndex(c)) { //noinspection unchecked performSearchWorks(searchWorkCreator.createPerEntityTypeWorks(c, WorkType.PURGE_ALL), transactionContext); } } } // Method that will be called when data needs to be removed from Lucene. protected void removeFromIndexes(final Object value, final Object key, final TransactionContext transactionContext) { performSearchWork(value, keyToString(key), WorkType.DELETE, transactionContext); } protected void updateIndexes(final boolean usingSkipIndexCleanupFlag, final Object value, final Object key, final TransactionContext transactionContext) { // Note: it's generally unsafe to assume there is no previous entry to cleanup: always use UPDATE // unless the specific flag is allowing this. performSearchWork(value, keyToString(key), usingSkipIndexCleanupFlag ? WorkType.ADD : WorkType.UPDATE, transactionContext); } private void performSearchWork(Object value, Serializable id, WorkType workType, TransactionContext transactionContext) { if (value == null) throw new NullPointerException("Cannot handle a null value!"); Collection<Work> works = searchWorkCreator.createPerEntityWorks(value, id, workType); performSearchWorks(works, transactionContext); } private void performSearchWorks(Collection<Work> works, TransactionContext transactionContext) { Worker worker = searchFactory.getWorker(); for (Work work : works) { worker.performWork(work, transactionContext); } } public boolean hasIndex(final Class<?> c) { return searchFactoryHandler.hasIndex(c); } private Object extractValue(Object wrappedValue) { if (typeConverter != null) { return typeConverter.unboxValue(wrappedValue); } return wrappedValue; } public void enableClasses(Class[] classes) { searchFactoryHandler.enableClasses(classes); } public boolean updateKnownTypesIfNeeded(Object value) { return searchFactoryHandler.updateKnownTypesIfNeeded(value); } public void registerKeyTransformer(Class<?> keyClass, Class<? extends Transformer> transformerClass) { keyTransformationHandler.registerTransformer(keyClass, transformerClass); } private String keyToString(Object key) { return keyTransformationHandler.keyToString(extractValue(key)); } public KeyTransformationHandler getKeyTransformationHandler() { return keyTransformationHandler; } public SearchIntegrator getSearchFactory() { return searchFactory; } /** * Customize work creation during indexing * @param searchWorkCreator custom {@link org.infinispan.query.backend.SearchWorkCreator} */ public void setSearchWorkCreator(SearchWorkCreator<Object> searchWorkCreator) { this.searchWorkCreator = searchWorkCreator; } public SearchWorkCreator<Object> getSearchWorkCreator() { return searchWorkCreator; } /** * In case of a remotely originating transactions we don't have a chance to visit the single * commands but receive this "batch". We then need the before-apply snapshot of some types * to route the cleanup commands to the correct indexes. * Note we don't need to visit the CommitCommand as the indexing context is registered * as a transaction sync. */ @Override public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable { final WriteCommand[] writeCommands = command.getModifications(); final Object[] stateBeforePrepare = new Object[writeCommands.length]; for (int i = 0; i < writeCommands.length; i++) { final WriteCommand writeCommand = writeCommands[i]; if (writeCommand instanceof PutKeyValueCommand) { InternalCacheEntry internalCacheEntry = dataContainer.get(((PutKeyValueCommand) writeCommand).getKey()); stateBeforePrepare[i] = internalCacheEntry != null ? internalCacheEntry.getValue() : null; } else if (writeCommand instanceof PutMapCommand) { stateBeforePrepare[i] = getPreviousValues(((PutMapCommand) writeCommand).getMap().keySet()); } else if (writeCommand instanceof RemoveCommand) { InternalCacheEntry internalCacheEntry = dataContainer.get(((RemoveCommand) writeCommand).getKey()); stateBeforePrepare[i] = internalCacheEntry != null ? internalCacheEntry.getValue() : null; } else if (writeCommand instanceof ReplaceCommand) { InternalCacheEntry internalCacheEntry = dataContainer.get(((ReplaceCommand) writeCommand).getKey()); stateBeforePrepare[i] = internalCacheEntry != null ? internalCacheEntry.getValue() : null; } } return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) -> { TxInvocationContext txInvocationContext = (TxInvocationContext) rCtx; if (txInvocationContext.isTransactionValid()) { final TransactionContext transactionContext = makeTransactionalEventContext(); for (int i = 0; i < writeCommands.length; i++) { final WriteCommand writeCommand = writeCommands[i]; if (writeCommand instanceof PutKeyValueCommand) { processPutKeyValueCommand((PutKeyValueCommand) writeCommand, txInvocationContext, stateBeforePrepare[i], transactionContext); } else if (writeCommand instanceof PutMapCommand) { processPutMapCommand((PutMapCommand) writeCommand, txInvocationContext, (Map<Object, Object>) stateBeforePrepare[i], transactionContext); } else if (writeCommand instanceof RemoveCommand) { processRemoveCommand((RemoveCommand) writeCommand, txInvocationContext, stateBeforePrepare[i], transactionContext); } else if (writeCommand instanceof ReplaceCommand) { processReplaceCommand((ReplaceCommand) writeCommand, txInvocationContext, stateBeforePrepare[i], transactionContext); } else if (writeCommand instanceof ClearCommand) { processClearCommand((ClearCommand) writeCommand, txInvocationContext, transactionContext); } } } }); } private Map<Object, Object> getPreviousValues(Set<Object> keySet) { Map<Object, Object> previousValues = new HashMap<>(); for (Object key : keySet) { InternalCacheEntry internalCacheEntry = dataContainer.get(key); Object previousValue = internalCacheEntry != null ? internalCacheEntry.getValue() : null; previousValues.put(key, previousValue); } return previousValues; } /** * Indexing management of a ReplaceCommand * * @param command the ReplaceCommand * @param ctx the InvocationContext * @param valueReplaced the previous value on this key * @param transactionContext Optional for lazy initialization, or reuse an existing context. */ private void processReplaceCommand(final ReplaceCommand command, final InvocationContext ctx, final Object valueReplaced, TransactionContext transactionContext) { if (valueReplaced != null && command.isSuccessful()) { Object key = extractValue(command.getKey()); if (shouldModifyIndexes(command, ctx, key)) { final boolean usingSkipIndexCleanupFlag = usingSkipIndexCleanup(command); Object p2 = extractValue(command.getNewValue()); final boolean newValueIsIndexed = updateKnownTypesIfNeeded(p2); if (!usingSkipIndexCleanupFlag) { final Object p1 = extractValue(command.getOldValue()); final boolean originalIsIndexed = updateKnownTypesIfNeeded(p1); if (p1 != null && originalIsIndexed) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; removeFromIndexes(p1, key, transactionContext); } } if (newValueIsIndexed) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; updateIndexes(usingSkipIndexCleanupFlag, p2, key, transactionContext); } } } } /** * Indexing management of a RemoveCommand * * @param command the visited RemoveCommand * @param ctx the InvocationContext of the RemoveCommand * @param valueRemoved the value before the removal * @param transactionContext Optional for lazy initialization, or reuse an existing context. */ private void processRemoveCommand(final RemoveCommand command, final InvocationContext ctx, final Object valueRemoved, TransactionContext transactionContext) { if (command.isSuccessful() && !command.isNonExistent()) { Object key = extractValue(command.getKey()); if (shouldModifyIndexes(command, ctx, key)) { final Object value = extractValue(valueRemoved); if (updateKnownTypesIfNeeded(value)) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; removeFromIndexes(value, key, transactionContext); } } } } /** * Indexing management of a PutMapCommand * * @param command the visited PutMapCommand * @param ctx the InvocationContext of the PutMapCommand * @param previousValues a map with the previous values, before processing the given PutMapCommand * @param transactionContext Optional for lazy initialization, or reuse an existing context. */ private void processPutMapCommand(final PutMapCommand command, final InvocationContext ctx, final Map<Object, Object> previousValues, TransactionContext transactionContext) { Map<Object, Object> dataMap = command.getMap(); final boolean usingSkipIndexCleanupFlag = usingSkipIndexCleanup(command); // Loop through all the keys and put those key-value pairings into lucene. for (Map.Entry<Object, Object> entry : previousValues.entrySet()) { Object originalKey = entry.getKey(); final Object key = extractValue(originalKey); final Object value = extractValue(dataMap.get(originalKey)); final Object previousValue = extractValue(entry.getValue()); if (!usingSkipIndexCleanupFlag && updateKnownTypesIfNeeded(previousValue)) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; if (shouldModifyIndexes(command, ctx, key)) { removeFromIndexes(previousValue, key, transactionContext); } } if (updateKnownTypesIfNeeded(value)) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; if (shouldModifyIndexes(command, ctx, key)) { updateIndexes(usingSkipIndexCleanupFlag, value, key, transactionContext); } } } } /** * Indexing management of a PutKeyValueCommand * * @param command the visited PutKeyValueCommand * @param ctx the InvocationContext of the PutKeyValueCommand * @param previousValue the value being replaced by the put operation * @param transactionContext Optional for lazy initialization, or reuse an existing context. */ private void processPutKeyValueCommand(final PutKeyValueCommand command, final InvocationContext ctx, final Object previousValue, TransactionContext transactionContext) { final boolean usingSkipIndexCleanupFlag = usingSkipIndexCleanup(command); //whatever the new type, we might still need to cleanup for the previous value (and schedule removal first!) Object value = extractValue(command.getValue()); Object key = command.getKey(); if (!usingSkipIndexCleanupFlag && updateKnownTypesIfNeeded(previousValue) && shouldRemove(value, previousValue)) { if (shouldModifyIndexes(command, ctx, key)) { transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; removeFromIndexes(previousValue, extractValue(key), transactionContext); } } if (updateKnownTypesIfNeeded(value)) { if (shouldModifyIndexes(command, ctx, key)) { // This means that the entry is just modified so we need to update the indexes and not add to them. transactionContext = transactionContext == null ? makeTransactionalEventContext() : transactionContext; updateIndexes(usingSkipIndexCleanupFlag, value, extractValue(key), transactionContext); } } } private boolean shouldRemove(Object value, Object previousValue) { if (getSearchWorkCreator() instanceof ExtendedSearchWorkCreator) { ExtendedSearchWorkCreator eswc = ExtendedSearchWorkCreator.class.cast(getSearchWorkCreator()); return eswc.shouldRemove(new SearchWorkCreatorContext(previousValue, value)); } else { return !(value == null || previousValue == null) && !value.getClass().equals(previousValue.getClass()); } } /** * Indexing management of the Clear command * * @param command the ClearCommand * @param ctx the InvocationContext of the PutKeyValueCommand * @param transactionContext Optional for lazy initialization, or to reuse an existing transactional context. */ private void processClearCommand(final ClearCommand command, final InvocationContext ctx, TransactionContext transactionContext) { if (shouldModifyIndexes(command, ctx, null)) { purgeAllIndexes(transactionContext); } } private TransactionContext makeTransactionalEventContext() { return new TransactionalEventTransactionContext(transactionManager, transactionSynchronizationRegistry); } private boolean usingSkipIndexCleanup(final FlagAffectedCommand command) { return command != null && command.hasAnyFlag(FlagBitSets.SKIP_INDEX_CLEANUP); } public IndexModificationStrategy getIndexModificationMode() { return indexingMode; } public boolean isStopping() { return stopping.get(); } }