/* * JBoss, Home of Professional Open Source * Copyright 2009 Red Hat Inc. and/or its affiliates and other * contributors as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a full listing of * individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.infinispan.query.backend; import org.hibernate.search.backend.TransactionContext; import org.hibernate.search.backend.spi.Work; import org.hibernate.search.backend.spi.WorkType; import org.hibernate.search.engine.spi.EntityIndexBinder; import org.hibernate.search.spi.SearchFactoryIntegrator; 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.context.Flag; import org.infinispan.context.InvocationContext; import org.infinispan.factories.KnownComponentNames; import org.infinispan.factories.annotations.ComponentName; import org.infinispan.factories.annotations.Inject; import org.infinispan.interceptors.base.CommandInterceptor; import org.infinispan.marshall.MarshalledValue; import org.infinispan.query.Transformer; import org.infinispan.query.logging.Log; import org.infinispan.util.concurrent.ConcurrentMapFactory; import org.infinispan.util.logging.LogFactory; import javax.transaction.TransactionManager; import javax.transaction.TransactionSynchronizationRegistry; import java.io.Serializable; import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 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 * @since 4.0 */ public class QueryInterceptor extends CommandInterceptor { private final SearchFactoryIntegrator searchFactory; private final ConcurrentMap<Class<?>,Boolean> knownClasses = ConcurrentMapFactory.makeConcurrentMap(); private final Lock mutating = new ReentrantLock(); private final KeyTransformationHandler keyTransformationHandler = new KeyTransformationHandler(); protected TransactionManager transactionManager; protected TransactionSynchronizationRegistry transactionSynchronizationRegistry; protected ExecutorService asyncExecutor; private static final Log log = LogFactory.getLog(QueryInterceptor.class, Log.class); @Override protected Log getLog() { return log; } public QueryInterceptor(SearchFactoryIntegrator searchFactory) { this.searchFactory = searchFactory; } @Inject public void injectDependencies( @ComponentName(KnownComponentNames.ASYNC_TRANSPORT_EXECUTOR) ExecutorService e) { this.asyncExecutor = e; } protected boolean shouldModifyIndexes(InvocationContext ctx) { return ! ctx.hasFlag(Flag.SKIP_INDEXING); } /** * Use this executor for Async operations * @return */ public ExecutorService getAsyncExecutor() { return asyncExecutor; } @Override public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable { // This method will get the put() calls on the cache and then send them into Lucene once it's successful. // do the actual put first. Object toReturn = invokeNextInterceptor(ctx, command); if (shouldModifyIndexes(ctx)) { // First making a check to see if the key is already in the cache or not. If it isn't we can add the key no problem, // otherwise we need to be updating the indexes as opposed to simply adding to the indexes. getLog().debug("Infinispan Query indexing is triggered"); Object key = command.getKey(); Object value = extractValue(command.getValue()); if (updateKnownTypesIfNeeded(value)) { // This means that the entry is just modified so we need to update the indexes and not add to them. updateIndexes(value, extractValue(key)); } else { if (updateKnownTypesIfNeeded(toReturn)) { removeFromIndexes(toReturn, extractValue(command.getKey())); } } } return toReturn; } @Override public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable { // remove the object out of the cache first. Object valueRemoved = invokeNextInterceptor(ctx, command); if (command.isSuccessful() && !command.isNonExistent() && shouldModifyIndexes(ctx)) { Object value = extractValue(valueRemoved); if (updateKnownTypesIfNeeded( value )) { removeFromIndexes(value, extractValue(command.getKey())); } } return valueRemoved; } @Override public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable { Object valueReplaced = invokeNextInterceptor(ctx, command); if (valueReplaced != null && command.isSuccessful() && shouldModifyIndexes(ctx)) { Object[] parameters = command.getParameters(); Object p1 = extractValue(parameters[1]); Object p2 = extractValue(parameters[2]); boolean originalIsIndexed = updateKnownTypesIfNeeded( p1 ); boolean newValueIsIndexed = updateKnownTypesIfNeeded( p2 ); Object key = extractValue(command.getKey()); if (p1 != null && originalIsIndexed) { removeFromIndexes(p1, key); } if (newValueIsIndexed) { updateIndexes(p2, key); } } return valueReplaced; } @Override public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable { Object mapPut = invokeNextInterceptor(ctx, command); if (shouldModifyIndexes(ctx)) { Map<Object, Object> dataMap = command.getMap(); // Loop through all the keys and put those key, value pairings into lucene. for (Map.Entry<Object, Object> entry : dataMap.entrySet()) { Object value = extractValue(entry.getValue()); if (updateKnownTypesIfNeeded(value)) { updateIndexes(value, extractValue(entry.getKey())); } } } return mapPut; } @Override public Object visitClearCommand(InvocationContext ctx, ClearCommand command) throws Throwable { // This method is called when somebody calls a cache.clear() and we will need to wipe everything in the indexes. Object returnValue = invokeNextInterceptor(ctx, command); if (shouldModifyIndexes(ctx)) { if (getLog().isTraceEnabled()) getLog().trace("shouldModifyIndexes() is true and we can clear the indexes"); for (Class c : this.knownClasses.keySet()) { EntityIndexBinder binder = this.searchFactory.getIndexBindingForEntity(c); if ( binder != null ) { //check as not all known classes are indexed searchFactory.getWorker().performWork(new Work<Object>(c, (Serializable)null, WorkType.PURGE_ALL), new TransactionalEventTransactionContext(transactionManager, transactionSynchronizationRegistry)); } } } return returnValue; } // Method that will be called when data needs to be removed from Lucene. protected void removeFromIndexes(Object value, Object key) { // The key here is the String representation of the key that is stored in the cache. // The key is going to be the documentID for Lucene. // The object parameter is the actual value that needs to be removed from lucene. if (value == null) throw new NullPointerException("Cannot handle a null value!"); TransactionContext transactionContext = new TransactionalEventTransactionContext(transactionManager, transactionSynchronizationRegistry); searchFactory.getWorker().performWork(new Work<Object>(value, keyToString(key), WorkType.DELETE), transactionContext); } protected void updateIndexes(Object value, Object key){ // The key here is the String representation of the key that is stored in the cache. // The key is going to be the documentID for Lucene. // The object parameter is the actual value that needs to be removed from lucene. if (value == null) throw new NullPointerException("Cannot handle a null value!"); TransactionContext transactionContext = new TransactionalEventTransactionContext(transactionManager, transactionSynchronizationRegistry); searchFactory.getWorker().performWork(new Work<Object>(value, keyToString(key), WorkType.UPDATE), transactionContext); } private Object extractValue(Object wrappedValue) { if (wrappedValue instanceof MarshalledValue) return ((MarshalledValue) wrappedValue).get(); else return wrappedValue; } public void enableClasses(Class<?>[] classes) { if ( classes == null || classes.length == 0 ) { return; } enableClassesIncrementally(classes, false); } private void enableClassesIncrementally(Class<?>[] classes, boolean locked) { ArrayList<Class<?>> toAdd = null; for (Class<?> type : classes) { if (!knownClasses.containsKey(type)) { if (toAdd==null) toAdd = new ArrayList<Class<?>>(classes.length); toAdd.add(type); } } if (toAdd == null) { return; } if (locked) { Set<Class<?>> existingClasses = knownClasses.keySet(); int index = existingClasses.size(); Class<?>[] all = existingClasses.toArray(new Class[existingClasses.size()+toAdd.size()]); for (Class<?> toAddClass : toAdd) { all[index++] = toAddClass; } searchFactory.addClasses(all); for (Class<?> type : toAdd) { if (searchFactory.getIndexBindingForEntity(type) != null) { knownClasses.put(type, Boolean.TRUE); } else { knownClasses.put(type, Boolean.FALSE); } } } else { mutating.lock(); try { enableClassesIncrementally(classes, true); } finally { mutating.unlock(); } } } private boolean updateKnownTypesIfNeeded(Object value) { if ( value != null ) { Class<?> potentialNewType = value.getClass(); if ( ! this.knownClasses.containsKey(potentialNewType) ) { mutating.lock(); try { enableClassesIncrementally( new Class[]{potentialNewType}, true); } finally { mutating.unlock(); } } return this.knownClasses.get(potentialNewType); } else { return false; } } public void registerKeyTransformer(Class<?> keyClass, Class<? extends Transformer> transformerClass) { keyTransformationHandler.registerTransformer(keyClass, transformerClass); } private String keyToString(Object key) { return keyTransformationHandler.keyToString(key); } public KeyTransformationHandler getKeyTransformationHandler() { return keyTransformationHandler; } }