/*
* Copyright 2004-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.compass.gps.device.hibernate.embedded;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.compass.core.Compass;
import org.compass.core.CompassException;
import org.compass.core.CompassSession;
import org.compass.core.CompassTransaction;
import org.compass.core.config.CompassConfiguration;
import org.compass.core.config.CompassConfigurationFactory;
import org.compass.core.config.CompassEnvironment;
import org.compass.core.config.CompassSettings;
import org.compass.core.mapping.Cascade;
import org.compass.core.mapping.ResourceMapping;
import org.compass.core.spi.InternalCompass;
import org.compass.core.transaction.JTASyncTransactionFactory;
import org.compass.core.transaction.LocalTransactionFactory;
import org.compass.core.util.ClassUtils;
import org.compass.gps.device.hibernate.lifecycle.HibernateMirrorFilter;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.CollectionEntry;
import org.hibernate.engine.EntityEntry;
import org.hibernate.event.*;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Value;
import org.hibernate.transaction.CMTTransactionFactory;
import org.hibernate.transaction.JTATransactionFactory;
/**
* An Hibernate event listener allowing to run Compass embedded within Hibernate. The embedded mode
* will allow to automatically (with minimal configuration) get Compass configured to mirror changes
* done through Hibernate to the search engine, as well as simply indexing the whole database content.
*
* <p>Configuration of this listener is simple:
* <pre>
* <hibernate-configuration>
* <session-factory>
*
* <event type="post-update">
* <listener class="org.compass.gps.device.hibernate.embedded.CompassEventListener"/>
* </event>
* <event type="post-insert">
* <listener class="org.compass.gps.device.hibernate.embedded.CompassEventListener"/>
* </event>
* <event type="post-delete">
* <listener class="org.compass.gps.device.hibernate.embedded.CompassEventListener"/>
* </event>
* <event type="post-collection-recreate">
* <listener class="org.compass.gps.device.hibernate.embedded.CompassEventListener"/>
* </event>
* <event type="post-collection-remove">
* <listener class="org.compass.gps.device.hibernate.embedded.CompassEventListener"/>
* </event>
* <event type="post-collection-update">
* <listener class="org.compass.gps.device.hibernate.embedded.CompassEventListener"/>
* </event>
*
* </session-factory>
* </hibernate-configuration>
* </pre>
*
* <p>When using Hiberante annotations or entity manager Compass also contains Hibernate search event class
* so it will be automatically detected. In such a case, there is no need for the event listener configuration.
*
* <p>Once the above configuration is set, then Compass is "installed" within Hibernate. In order to enable
* Compass, the search engine connection url must be set using Hibernate properties configuration. For example:
* <code><property name="compass.engine.connection">testindex</property></code>.
*
* <p>Compass will automatically go over the mapped classes in Hibernate and will check if they have Compass
* mappings. If they do, they will be added to the searchable entities. If no such searchable classes are found,
* this listener will perform no operations.
*
* <p>Compass additional configuration can be set using typical Hiberante properties configuration using the
* <code>compass.</code> prefix. If using an external Compass configuration file is preferred, then the
* <code>compass.hibernate.config</code> can be configured and point to the location of a Compass configuration
* file.
*
* <p>Embedded Compass also allows to use {@link org.compass.gps.CompassGps#index()} in order to complely reindex
* the database. See {@link org.compass.gps.device.hibernate.embedded.HibernateHelper} for more information. In
* order to configure the Compass instance that will be used to index the database, the <code>gps.index.</code>
* can be used.
*
* <p>Transaction management is automatically bounded to Hibernate by using Compass local transaction. If other
* transaction strategies are used (such as JTA Sync or XA) then the Compass transaction will be bounded to them
* and not the Hibernate transaction.
*
* <p>A user defined {@link org.compass.gps.device.hibernate.lifecycle.HibernateMirrorFilter} can be used to
* filter out mirror operations. In order to configure one, the <code>compass.hibernate.mirrorFilter</code>
* can be used with the implementation class FQN.
*
* <p>In order to get the {@link Compass} instnace bounded to this Hibernate configuration, the
* {@link HibernateHelper} can be used. This is mainly used in order to perform search operations on the
* index and get a Compass Gps in order to reindex the database.
*
* @author kimchy
*/
public class CompassEventListener implements PostDeleteEventListener, PostInsertEventListener, PostUpdateEventListener,
PostCollectionRecreateEventListener, PostCollectionRemoveEventListener, PostCollectionUpdateEventListener,
Initializable {
public final static Log log = LogFactory.getLog(CompassEventListener.class);
public static final String COMPASS_PREFIX = "compass";
public static final String COMPASS_GPS_INDEX_PREFIX = "gps.index.";
public static final String COMPASS_CONFIG_LOCATION = "compass.hibernate.config";
public static final String COMPASS_MIRROR_FILTER = "compass.hibernate.mirrorFilter";
public static final String COMPASS_PROCESS_COLLECTIONS = "compass.hibernate.processCollections";
private static ThreadLocal<WeakHashMap<Configuration, CompassHolder>> contexts = new ThreadLocal<WeakHashMap<Configuration, CompassHolder>>();
private CompassHolder compassHolder;
private boolean processCollections = true;
public void initialize(Configuration cfg) {
compassHolder = getCompassHolder(cfg);
}
public Compass getCompass() {
return this.compassHolder.compass;
}
public Properties getIndexSettings() {
return this.compassHolder.indexSettings;
}
public void onPostDelete(PostDeleteEvent event) {
if (compassHolder == null) {
return;
}
Object entity = event.getEntity();
if (!hasMappingForEntity(entity.getClass(), Cascade.DELETE)) {
return;
}
if (compassHolder.mirrorFilter != null) {
if (compassHolder.mirrorFilter.shouldFilterDelete(event)) {
return;
}
}
TransactionSyncHolder holder = getOrCreateHolder(event.getSession());
if (log.isTraceEnabled()) {
log.trace("Deleting [" + entity + "]");
}
holder.session.delete(entity);
afterOperation(holder);
}
public void onPostInsert(PostInsertEvent event) {
if (compassHolder == null) {
return;
}
Object entity = event.getEntity();
if (!hasMappingForEntity(entity.getClass(), Cascade.CREATE)) {
return;
}
if (compassHolder.mirrorFilter != null) {
if (compassHolder.mirrorFilter.shouldFilterInsert(event)) {
return;
}
}
TransactionSyncHolder holder = getOrCreateHolder(event.getSession());
if (log.isTraceEnabled()) {
log.trace("Creating [" + entity + "]");
}
Collection<CollectionEntry> collectionsBefore = null;
if (processCollections) {
collectionsBefore = new HashSet<CollectionEntry>(event.getSession().getPersistenceContext().getCollectionEntries().values());
}
holder.session.create(entity);
if (processCollections) {
Collection<CollectionEntry> collectionsAfter = event.getSession().getPersistenceContext().getCollectionEntries().values();
for (CollectionEntry collection : collectionsAfter) {
if (!collectionsBefore.contains(collection)) {
collection.setProcessed(true);
}
}
}
afterOperation(holder);
}
public void onPostUpdate(PostUpdateEvent event) {
if (compassHolder == null) {
return;
}
Object entity = event.getEntity();
if (!hasMappingForEntity(entity.getClass(), Cascade.SAVE)) {
return;
}
if (compassHolder.mirrorFilter != null) {
if (compassHolder.mirrorFilter.shouldFilterUpdate(event)) {
return;
}
}
TransactionSyncHolder holder = getOrCreateHolder(event.getSession());
if (log.isTraceEnabled()) {
log.trace("Updating [" + entity + "]");
}
Collection<CollectionEntry> collectionsBefore = null;
if (processCollections) {
collectionsBefore = new HashSet<CollectionEntry>(event.getSession().getPersistenceContext().getCollectionEntries().values());
}
holder.session.save(entity);
if (processCollections) {
Collection<CollectionEntry> collectionsAfter = event.getSession().getPersistenceContext().getCollectionEntries().values();
for (CollectionEntry collection : collectionsAfter) {
if (!collectionsBefore.contains(collection)) {
collection.setProcessed(true);
}
}
}
afterOperation(holder);
}
public void onPostRecreateCollection(PostCollectionRecreateEvent postCollectionRecreateEvent) {
processCollectionEvent(postCollectionRecreateEvent);
}
public void onPostRemoveCollection(PostCollectionRemoveEvent postCollectionRemoveEvent) {
processCollectionEvent(postCollectionRemoveEvent);
}
public void onPostUpdateCollection(PostCollectionUpdateEvent postCollectionUpdateEvent) {
processCollectionEvent(postCollectionUpdateEvent);
}
private void processCollectionEvent(AbstractCollectionEvent event) {
if (compassHolder == null) {
return;
}
final Object entity = event.getAffectedOwnerOrNull();
if (entity == null) {
//Hibernate cannot determine every single time the owner especially incase 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;
}
CollectionEntry collectionEntry = event.getSession().getPersistenceContext().getCollectionEntry(event.getCollection());
if (collectionEntry != null && collectionEntry.getLoadedPersister() == null) {
// ignore this entry, since Hibernate will cause NPE when doing SAVE
// TODO is there a better way to solve this?
return;
}
if (!hasMappingForEntity(entity.getClass(), Cascade.SAVE)) {
return;
}
Serializable id = getId(entity, event);
if (id == null) {
log.warn("Unable to reindex entity on collection change, id cannot be extracted: " + event.getAffectedOwnerEntityName());
return;
}
if (compassHolder.mirrorFilter != null) {
if (compassHolder.mirrorFilter.shouldFilterCollection(event)) {
return;
}
}
TransactionSyncHolder holder = getOrCreateHolder(event.getSession());
if (log.isTraceEnabled()) {
log.trace("Updating [" + entity + "]");
}
holder.session.save(entity);
afterOperation(holder);
}
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;
}
private TransactionSyncHolder getOrCreateHolder(EventSource session) {
if (session.isTransactionInProgress()) {
Transaction transaction = session.getTransaction();
TransactionSyncHolder holder = compassHolder.syncHolderPerTx.get(transaction);
if (holder == null) {
holder = new TransactionSyncHolder();
holder.session = compassHolder.compass.openSession();
holder.tr = holder.session.beginTransaction();
holder.transacted = true;
transaction.registerSynchronization(new CompassEmbeddedSyncronization(holder, transaction));
compassHolder.syncHolderPerTx.put(transaction, holder);
}
return holder;
} else {
TransactionSyncHolder holder = new TransactionSyncHolder();
holder.session = compassHolder.compass.openSession();
holder.tr = holder.session.beginTransaction();
holder.transacted = false;
return holder;
}
}
private void afterOperation(TransactionSyncHolder holder) {
if (holder.transacted) {
return;
}
holder.tr.commit();
holder.session.close();
}
private CompassHolder getCompassHolder(Configuration cfg) {
WeakHashMap<Configuration, CompassHolder> contextMap = contexts.get();
if (contextMap == null) {
contextMap = new WeakHashMap<Configuration, CompassHolder>(2);
contexts.set(contextMap);
}
CompassHolder compassHolder = contextMap.get(cfg);
if (compassHolder == null) {
compassHolder = initCompassHolder(cfg);
if (compassHolder != null) {
if (log.isDebugEnabled()) {
log.debug("Regsitering new Compass Holder [" + compassHolder + "]");
}
contextMap.put(cfg, compassHolder);
}
}
return compassHolder;
}
private CompassHolder initCompassHolder(Configuration cfg) {
Properties compassProperties = new Properties();
//noinspection unchecked
Properties props = cfg.getProperties();
for (Map.Entry entry : props.entrySet()) {
String key = (String) entry.getKey();
if (key.startsWith(COMPASS_PREFIX)) {
compassProperties.put(entry.getKey(), entry.getValue());
}
if (key.startsWith(COMPASS_GPS_INDEX_PREFIX)) {
compassProperties.put(entry.getKey(), entry.getValue());
}
}
if (compassProperties.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("No Compass properties defined, disabling Compass");
}
return null;
}
if (compassProperties.getProperty(CompassEnvironment.CONNECTION) == null) {
if (log.isDebugEnabled()) {
log.debug("No Compass [" + CompassEnvironment.CONNECTION + "] property defined, disabling Compass");
}
return null;
}
processCollections = compassProperties.getProperty(COMPASS_PROCESS_COLLECTIONS, "true").equalsIgnoreCase("true");
CompassConfiguration compassConfiguration = CompassConfigurationFactory.newConfiguration();
CompassSettings settings = compassConfiguration.getSettings();
settings.addSettings(compassProperties);
String configLocation = (String) compassProperties.get(COMPASS_CONFIG_LOCATION);
if (configLocation != null) {
compassConfiguration.configure(configLocation);
}
boolean atleastOneClassAdded = false;
for (Iterator it = cfg.getClassMappings(); it.hasNext();) {
PersistentClass clazz = (PersistentClass) it.next();
Class<?> mappedClass = clazz.getMappedClass();
for (Iterator propIt = clazz.getPropertyIterator(); propIt.hasNext();) {
Property prop = (Property) propIt.next();
Value value = prop.getValue();
if (value instanceof Component) {
Component component = (Component) value;
try {
atleastOneClassAdded |= compassConfiguration.tryAddClass(ClassUtils.forName(component.getComponentClassName(), settings.getClassLoader()));
} catch (ClassNotFoundException e) {
log.warn("Failed to load component class [" + component.getComponentClassName() + "]", e);
}
}
}
Value idValue = clazz.getIdentifierProperty().getValue();
if (idValue instanceof Component) {
Component component = (Component) idValue;
try {
atleastOneClassAdded |= compassConfiguration.tryAddClass(ClassUtils.forName(component.getComponentClassName(), settings.getClassLoader()));
} catch (ClassNotFoundException e) {
log.warn("Failed to load component class [" + component.getComponentClassName() + "]", e);
}
}
atleastOneClassAdded |= compassConfiguration.tryAddClass(mappedClass);
}
if (!atleastOneClassAdded) {
if (log.isDebugEnabled()) {
log.debug("No searchable class mappings found in Hibernate class mappings, disabling Compass");
}
return null;
}
CompassHolder compassHolder = new CompassHolder();
compassHolder.compassProperties = compassProperties;
compassHolder.commitBeforeCompletion = settings.getSettingAsBoolean(CompassEnvironment.Transaction.COMMIT_BEFORE_COMPLETION, false);
String transactionFactory = (String) compassProperties.get(CompassEnvironment.Transaction.FACTORY);
if (transactionFactory == null) {
String hibernateTransactionStrategy = cfg.getProperty(Environment.TRANSACTION_STRATEGY);
if (CMTTransactionFactory.class.getName().equals(hibernateTransactionStrategy) || JTATransactionFactory.class.getName().equals(hibernateTransactionStrategy)) {
// hibernate is configured with JTA, automatically configure Compass to use its JTASync (by default)
compassHolder.hibernateControlledTransaction = false;
compassConfiguration.setSetting(CompassEnvironment.Transaction.FACTORY, JTASyncTransactionFactory.class.getName());
} else {
// Hibernate JDBC transaction manager, let Compass use the local transaction manager
compassHolder.hibernateControlledTransaction = true;
// if the settings is configured to use local transaciton, disable thread bound setting since
// we are using Hibernate to managed transaction scope (using the transaction to holder map) and not thread locals
if (settings.getSetting(CompassEnvironment.Transaction.DISABLE_THREAD_BOUND_LOCAL_TRANSATION) == null) {
settings.setBooleanSetting(CompassEnvironment.Transaction.DISABLE_THREAD_BOUND_LOCAL_TRANSATION, true);
}
}
} else if (LocalTransactionFactory.class.getName().equals(transactionFactory)) {
compassHolder.hibernateControlledTransaction = true;
// if the settings is configured to use local transaciton, disable thread bound setting since
// we are using Hibernate to managed transaction scope (using the transaction to holder map) and not thread locals
if (settings.getSetting(CompassEnvironment.Transaction.DISABLE_THREAD_BOUND_LOCAL_TRANSATION) == null) {
settings.setBooleanSetting(CompassEnvironment.Transaction.DISABLE_THREAD_BOUND_LOCAL_TRANSATION, true);
}
} else {
// Hibernate is not controlling the transaction (using JTA Sync or XA), don't commit/rollback
// with Hibernate transaction listeners
compassHolder.hibernateControlledTransaction = false;
}
compassHolder.indexSettings = new Properties();
for (Map.Entry entry : compassProperties.entrySet()) {
String key = (String) entry.getKey();
if (key.startsWith(COMPASS_GPS_INDEX_PREFIX)) {
compassHolder.indexSettings.put(key.substring(COMPASS_GPS_INDEX_PREFIX.length()), entry.getValue());
}
}
String mirrorFilterClass = compassHolder.compassProperties.getProperty(COMPASS_MIRROR_FILTER);
if (mirrorFilterClass != null) {
try {
compassHolder.mirrorFilter = (HibernateMirrorFilter) ClassUtils.forName(mirrorFilterClass, compassConfiguration.getSettings().getClassLoader()).newInstance();
} catch (Exception e) {
throw new CompassException("Failed to create mirror filter [" + mirrorFilterClass + "]", e);
}
}
compassHolder.compass = compassConfiguration.buildCompass();
return compassHolder;
}
private boolean hasMappingForEntity(Class clazz, Cascade cascade) {
ResourceMapping resourceMapping = ((InternalCompass) compassHolder.compass).getMapping().getRootMappingByClass(clazz);
if (resourceMapping != null) {
return true;
}
resourceMapping = ((InternalCompass) compassHolder.compass).getMapping().getNonRootMappingByClass(clazz);
if (resourceMapping == null) {
return false;
}
return resourceMapping.operationAllowed(cascade);
}
private class CompassHolder {
ConcurrentHashMap<Transaction, TransactionSyncHolder> syncHolderPerTx = new ConcurrentHashMap<Transaction, TransactionSyncHolder>();
Properties compassProperties;
Properties indexSettings;
boolean commitBeforeCompletion;
boolean hibernateControlledTransaction;
HibernateMirrorFilter mirrorFilter;
Compass compass;
}
private class TransactionSyncHolder {
public CompassSession session;
public CompassTransaction tr;
public boolean transacted;
}
private class CompassEmbeddedSyncronization implements Synchronization {
private Transaction transaction;
private TransactionSyncHolder holder;
private CompassEmbeddedSyncronization(TransactionSyncHolder holder, Transaction transaction) {
this.holder = holder;
this.transaction = transaction;
}
public void beforeCompletion() {
if (!compassHolder.commitBeforeCompletion) {
return;
}
if (holder.session.isClosed()) {
return;
}
if (compassHolder.hibernateControlledTransaction) {
if (log.isTraceEnabled()) {
log.trace("Committing compass transaction using Hibernate synchronization beforeCompletion on thread [" +
Thread.currentThread().getName() + "]");
}
holder.tr.commit();
}
}
public void afterCompletion(int status) {
try {
if (holder.session.isClosed()) {
return;
}
if (!compassHolder.commitBeforeCompletion) {
if (compassHolder.hibernateControlledTransaction) {
try {
if (status == Status.STATUS_COMMITTED) {
if (log.isTraceEnabled()) {
log.trace("Committing compass transaction using Hibernate synchronization afterCompletion on thread [" +
Thread.currentThread().getName() + "]");
}
holder.tr.commit();
} else {
if (log.isTraceEnabled()) {
log.trace("Rolling back compass transaction using Hibernate synchronization afterCompletion on thread [" +
Thread.currentThread().getName() + "]");
}
holder.tr.rollback();
}
} finally {
holder.session.close();
}
}
}
} catch (Exception e) {
// TODO swallow??????
log.error("Exception occured when sync with transaction", e);
} finally {
compassHolder.syncHolderPerTx.remove(transaction);
}
}
}
}