/* * 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.jpa.embedded.openjpa; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Map; import java.util.Properties; import javax.persistence.EntityNotFoundException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.openjpa.conf.OpenJPAConfiguration; import org.apache.openjpa.conf.OpenJPAConfigurationImpl; import org.apache.openjpa.conf.OpenJPAVersion; import org.apache.openjpa.event.BeginTransactionListener; import org.apache.openjpa.event.BrokerFactoryEvent; import org.apache.openjpa.event.BrokerFactoryListener; import org.apache.openjpa.event.EndTransactionListener; import org.apache.openjpa.event.RemoteCommitEvent; import org.apache.openjpa.event.RemoteCommitListener; import org.apache.openjpa.event.TransactionEvent; import org.apache.openjpa.kernel.Broker; import org.apache.openjpa.kernel.BrokerFactory; import org.apache.openjpa.lib.conf.AbstractProductDerivation; import org.apache.openjpa.lib.conf.Configuration; import org.apache.openjpa.lib.conf.ConfigurationProvider; import org.apache.openjpa.persistence.EntityManagerFactoryImpl; import org.apache.openjpa.persistence.Extent; import org.apache.openjpa.persistence.OpenJPAEntityManager; import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory; import org.apache.openjpa.persistence.OpenJPAPersistence; import org.apache.openjpa.util.OpenJPAId; import org.compass.core.Compass; 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.transaction.JTASyncTransactionFactory; import org.compass.core.transaction.LocalTransactionFactory; import org.compass.gps.device.jpa.JpaGpsDevice; import org.compass.gps.device.jpa.embedded.DefaultJpaCompassGps; import org.compass.gps.device.jpa.lifecycle.OpenJPAJpaEntityLifecycleInjector; /** * An OpenJPA Compass product derivation. Simply by adding the Compass jar to the class path this product * derivation will be registered with OpenJPA. This derivation will not add Compass support to OpenJPA if * no Compass setting is set. The single required setting (for example, within the persistence.xml) is * Compass engine connection setting ({@link org.compass.core.config.CompassEnvironment#CONNECTION}. * * <p>The embedded Open JPA support uses Compass GPS and adds an "embedded" Compass, or adds a searchable * feature to Open JPA by registering a {@link org.compass.core.Compass} instance and a {@link org.compass.gps.device.jpa.JpaGpsDevice} * instance with OpenJPA. It registers mirroring listeners (after delete/store/persist) to automatically * mirror changes done through OpenJPA to the Compass index. It also registeres transaction listeners * to synchronize between Compass transactions and Open JPA transactions. * * <p>Use {@link org.compass.gps.device.jpa.embedded.openjpa.OpenJPAHelper} in order to access the registered * {@link org.compass.core.Compass} instnace and {@link org.compass.gps.device.jpa.JpaGpsDevice} instance assigned * to the {@link javax.persistence.EntityManagerFactory}. Also use it to get {@link org.compass.core.CompassSession} * assigned to an {@link javax.persistence.EntityManager}. * * <p>The Compass instnace used for mirroring can be configured by adding <code>compass</code> prefixed settings. * Additional settings that only control the Compass instnace created for indexing should be set using * <code>gps.index.compass.</code>. For more information on indexing and mirroring Compass please check * {@link org.compass.gps.impl.SingleCompassGps}. * * <p>Specific properties that this plugin can use: * <ul> * <li>compass.openjpa.reindexOnStartup: Set it to <code>true</code> in order to perform full reindex of the database on startup. * Defaults to <code>false</code>.</li> * <li>compass.openjpa.registerRemoteCommitListener: Set it to <code>true</code> in order to register for remote commits * notifications. Defaults to <code>false</code>.</li> * <li>compass.openjpa.indexQuery.[entity name/class]: Specific select query that will be used to perform the indexing * for the mentioned specific entity name / class. Note, before calling {@link org.compass.gps.CompassGps#index()} there * is an option the programmatically control this.</li> * <li>compass.openjpa.config: A classpath that points to Compass configuration.</li> * </ul> * * @author kimchy */ public class CompassProductDerivation extends AbstractProductDerivation { private static final Log log = LogFactory.getLog(CompassProductDerivation.class); public static final String COMPASS_USER_OBJECT_KEY = CompassProductDerivation.class.getName() + ".compass"; public static final String COMPASS_SESSION_USER_OBJECT_KEY = CompassProductDerivation.class.getName() + ".compassSession"; public static final String COMPASS_TRANSACTION_USER_OBJECT_KEY = CompassProductDerivation.class.getName() + ".compassTransaction"; public static final String COMPASS_GPS_USER_OBJECT_KEY = CompassProductDerivation.class.getName() + ".gps"; public static final String COMPASS_INDEX_SETTINGS_USER_OBJECT_KEY = CompassProductDerivation.class.getName() + ".indexprops"; private static final String COMPASS_PREFIX = "compass"; private static final String COMPASS_GPS_INDEX_PREFIX = "gps.index."; public static final String REINDEX_ON_STARTUP = "compass.openjpa.reindexOnStartup"; public static final String REGISTER_REMOTE_COMMIT_LISTENER = "compass.openjpa.registerRemoteCommitListener"; public static final String INDEX_QUERY_PREFIX = "compass.openjpa.indexQuery."; public static final String COMPASS_CONFIG_LOCATION = "compass.openjpa.config"; // this is only used when installed in a pre-1.0 version of OpenJPA private static final Map<OpenJPAConfiguration, CompassProductDerivation> derivations = new IdentityHashMap<OpenJPAConfiguration, CompassProductDerivation>(); private Compass compass; private DefaultJpaCompassGps jpaCompassGps; private boolean commitBeforeCompletion; private boolean openJpaControlledTransaction; private Properties compassProperties; public int getType() { return TYPE_FEATURE; } @Override public boolean beforeConfigurationLoad(Configuration config) { if (!(config instanceof OpenJPAConfiguration)) { return false; } final OpenJPAConfigurationImpl openJpaConfig = (OpenJPAConfigurationImpl) config; // Compass can make use of changed object IDs when receiving remote // commit events; reset the default setting to true. openJpaConfig.remoteProviderPlugin.setTransmitPersistedObjectIds(true); // In 0.x releases of OpenJPA, the BrokerFactoryEventManager does not exist. // This check will prevent us from triggering a NoSuchMethodError. if (!isReleasedVersion()) { openJpaConfig.getLog(OpenJPAConfiguration.LOG_RUNTIME).warn( "Compass cannot automatically install itself into pre-1.0 versions of OpenJPA. To complete " + "Compass installation, you must invoke CompassProductDerivation.installCompass()."); derivations.put(openJpaConfig, this); return false; } openJpaConfig.getBrokerFactoryEventManager().addListener(new BrokerFactoryListener() { public void afterBrokerFactoryCreate(BrokerFactoryEvent event) { installIntoFactory(event.getBrokerFactory()); } public void eventFired(BrokerFactoryEvent event) { if (event.getEventType() == BrokerFactoryEvent.BROKER_FACTORY_CREATED) afterBrokerFactoryCreate(event); } }); return false; } /** * @deprecated This is only needed for pre-1.0 versions of OpenJPA. */ public static void installCompass(BrokerFactory factory) { if (factory.getUserObject(COMPASS_USER_OBJECT_KEY) != null) return; CompassProductDerivation derivation = derivations.get(factory.getConfiguration()); if (derivation == null) throw new IllegalStateException("no CompassProductDerivation instance registered for this configuration"); derivation.installIntoFactory(factory); } @Override public boolean beforeConfigurationConstruct(ConfigurationProvider cp) { //noinspection unchecked compassProperties = new Properties(); Map<String, Object> openJpaProps = cp.getProperties(); loadCompassProps(openJpaProps); return super.beforeConfigurationConstruct(cp); } private void installIntoFactory(BrokerFactory factory) { if (compassProperties.isEmpty()) { if (log.isDebugEnabled()) { log.debug("No Compass properties found in configuraiton, disabling Compass"); } return; } if (compassProperties.getProperty(CompassEnvironment.CONNECTION) == null) { if (log.isDebugEnabled()) { log.debug("No Compass [" + CompassEnvironment.CONNECTION + "] property defined, disabling Compass"); } return; } OpenJPAConfiguration openJpaConfig = factory.getConfiguration(); 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); } Collection<Class> classes = openJpaConfig.getMetaDataRepositoryInstance().loadPersistentTypes(true, null); for (Class jpaClass : classes) { compassConfiguration.tryAddClass(jpaClass); } OpenJPAEntityManagerFactory emf = toEntityManagerFactory(factory); // create some default settings String transactionFactory = (String) compassProperties.get(CompassEnvironment.Transaction.FACTORY); if (transactionFactory == null) { OpenJPAEntityManager em = emf.createEntityManager(); boolean isJTA = em.isManaged(); em.close(); if (isJTA) { transactionFactory = JTASyncTransactionFactory.class.getName(); openJpaControlledTransaction = false; } else { transactionFactory = LocalTransactionFactory.class.getName(); openJpaControlledTransaction = true; } settings.setSetting(CompassEnvironment.Transaction.FACTORY, transactionFactory); } else { // JPA is not controlling the transaction (using JTA Sync or XA), don't commit/rollback // with OpenJPA transaction listeners openJpaControlledTransaction = false; } // if the settings is configured to use local transaciton, disable thread bound setting since // we are using OpenJPA to managed transaction scope (using user objects on the em) and not thread locals // will only be taken into account when using local transactions if (settings.getSetting(CompassEnvironment.Transaction.DISABLE_THREAD_BOUND_LOCAL_TRANSATION) == null) { // if no emf is defined settings.setBooleanSetting(CompassEnvironment.Transaction.DISABLE_THREAD_BOUND_LOCAL_TRANSATION, true); } compass = compassConfiguration.buildCompass(); commitBeforeCompletion = settings.getSettingAsBoolean(CompassEnvironment.Transaction.COMMIT_BEFORE_COMPLETION, false); factory.putUserObject(COMPASS_USER_OBJECT_KEY, compass); registerListeners(factory); // extract index properties so they will be used Properties indexProps = new Properties(); for (Map.Entry entry : compassProperties.entrySet()) { String key = (String) entry.getKey(); if (key.startsWith(COMPASS_GPS_INDEX_PREFIX)) { indexProps.put(key.substring(COMPASS_GPS_INDEX_PREFIX.length()), entry.getValue()); } } factory.putUserObject(COMPASS_INDEX_SETTINGS_USER_OBJECT_KEY, indexProps); // start an internal JPA device and Gps for mirroring JpaGpsDevice jpaGpsDevice = new JpaGpsDevice(DefaultJpaCompassGps.JPA_DEVICE_NAME, emf); jpaGpsDevice.setMirrorDataChanges(true); jpaGpsDevice.setInjectEntityLifecycleListener(true); for (Map.Entry entry : compassProperties.entrySet()) { String key = (String) entry.getKey(); if (key.startsWith(INDEX_QUERY_PREFIX)) { String entityName = key.substring(INDEX_QUERY_PREFIX.length()); String selectQuery = (String) entry.getValue(); jpaGpsDevice.setIndexSelectQuery(entityName, selectQuery); } } OpenJPAJpaEntityLifecycleInjector lifecycleInjector = new OpenJPAJpaEntityLifecycleInjector(); lifecycleInjector.setEventListener(new EmbeddedOpenJPAEventListener(jpaGpsDevice)); jpaGpsDevice.setLifecycleInjector(lifecycleInjector); jpaCompassGps = new DefaultJpaCompassGps(); jpaCompassGps.setCompass(compass); jpaCompassGps.addGpsDevice(jpaGpsDevice); // before we start the Gps, open and close a broker emf.createEntityManager().close(); jpaCompassGps.start(); String reindexOnStartup = (String) compassProperties.get(REINDEX_ON_STARTUP); if ("true".equalsIgnoreCase(reindexOnStartup)) { jpaCompassGps.index(); } factory.putUserObject(COMPASS_GPS_USER_OBJECT_KEY, jpaCompassGps); } private void loadCompassProps(Map<String, Object> openJpaProps) { for (Map.Entry<String, Object> entry : openJpaProps.entrySet()) { if (entry.getKey().startsWith(COMPASS_PREFIX)) { compassProperties.put(entry.getKey(), entry.getValue()); } if (entry.getKey().startsWith(COMPASS_GPS_INDEX_PREFIX)) { compassProperties.put(entry.getKey(), entry.getValue()); } } } private OpenJPAEntityManagerFactory toEntityManagerFactory(BrokerFactory factory) { try { Class cls; try { cls = Class.forName("org.apache.openjpa.persistence.JPAFacadeHelper"); } catch (ClassNotFoundException e) { cls = OpenJPAPersistence.class; } return (OpenJPAEntityManagerFactory) cls.getMethod("toEntityManagerFactory", BrokerFactory.class).invoke(null, factory); } catch (NoSuchMethodException e) { throw new IllegalStateException(e); } catch (IllegalAccessException e) { throw new IllegalStateException(e); } catch (InvocationTargetException e) { if (e.getCause() instanceof RuntimeException) throw (RuntimeException) e.getCause(); else throw new RuntimeException(e); } } protected void registerListeners(BrokerFactory brokerFactory) { brokerFactory.addTransactionListener(new BeginTransactionListener() { public void afterBegin(TransactionEvent transactionEvent) { Broker broker = (Broker) transactionEvent.getSource(); CompassSession session = compass.openSession(); broker.putUserObject(COMPASS_SESSION_USER_OBJECT_KEY, session); CompassTransaction tr = session.beginTransaction(); broker.putUserObject(COMPASS_TRANSACTION_USER_OBJECT_KEY, tr); } }); brokerFactory.addTransactionListener(new EndTransactionListener() { public void beforeCommit(TransactionEvent transactionEvent) { if (commitBeforeCompletion) { commit(transactionEvent); } } public void afterCommit(TransactionEvent transactionEvent) { // TODO maybe beforeCommit should occur here (when using jdbc) } public void afterRollback(TransactionEvent transactionEvent) { } public void afterStateTransitions(TransactionEvent transactionEvent) { } public void afterCommitComplete(TransactionEvent transactionEvent) { if (!commitBeforeCompletion) { commit(transactionEvent); } } public void afterRollbackComplete(TransactionEvent transactionEvent) { rollback(transactionEvent); } private void commit(TransactionEvent trEvent) { Broker broker = (Broker) trEvent.getSource(); CompassTransaction tr = (CompassTransaction) broker.getUserObject(COMPASS_TRANSACTION_USER_OBJECT_KEY); CompassSession session = (CompassSession) broker.getUserObject(COMPASS_SESSION_USER_OBJECT_KEY); try { if (openJpaControlledTransaction) { try { tr.commit(); } finally { session.close(); } } } finally { broker.putUserObject(COMPASS_TRANSACTION_USER_OBJECT_KEY, null); broker.putUserObject(COMPASS_SESSION_USER_OBJECT_KEY, null); } } private void rollback(TransactionEvent trEvent) { Broker broker = (Broker) trEvent.getSource(); CompassTransaction tr = (CompassTransaction) broker.getUserObject(COMPASS_TRANSACTION_USER_OBJECT_KEY); CompassSession session = (CompassSession) broker.getUserObject(COMPASS_SESSION_USER_OBJECT_KEY); try { if (openJpaControlledTransaction) { try { tr.rollback(); } finally { session.close(); } } } finally { broker.putUserObject(COMPASS_TRANSACTION_USER_OBJECT_KEY, null); broker.putUserObject(COMPASS_SESSION_USER_OBJECT_KEY, null); } } }); String registerRemoteCommitListener = (String) compassProperties.get(REGISTER_REMOTE_COMMIT_LISTENER); if ("true".equalsIgnoreCase(registerRemoteCommitListener)) { brokerFactory.getConfiguration().getRemoteCommitEventManager().addListener(new CompassRemoteCommitListener( toEntityManagerFactory(brokerFactory), compass)); } } public void beforeConfigurationClose(Configuration configuration) { if (jpaCompassGps != null) { jpaCompassGps.stop(); } if (compass != null) { compass.close(); } } private static class CompassRemoteCommitListener implements RemoteCommitListener { private static final Log log = LogFactory.getLog(CompassRemoteCommitListener.class); private final EntityManagerFactoryImpl emf; private final Compass compass; private CompassRemoteCommitListener(OpenJPAEntityManagerFactory emf, Compass compass) { // casting to EMFImpl so that this code can work with pre-1.0 and post-1.0 versions // of OpenJPA. this.emf = (EntityManagerFactoryImpl) emf; this.compass = compass; } @SuppressWarnings({"unchecked"}) public void afterCommit(RemoteCommitEvent event) { OpenJPAEntityManager em = emf.createEntityManager(); CompassSession session = compass.openSession(); CompassTransaction tr = null; try { tr = session.beginTransaction(); switch (event.getPayloadType()) { case RemoteCommitEvent.PAYLOAD_OIDS: reindexTypesByName(event.getPersistedTypeNames(), em, session); reindexObjectsById(event.getUpdatedObjectIds(), em, session); deleteObjectsById(event.getDeletedObjectIds(), session); break; case RemoteCommitEvent.PAYLOAD_OIDS_WITH_ADDS: reindexObjectsById(event.getPersistedObjectIds(), em, session); reindexObjectsById(event.getUpdatedObjectIds(), em, session); deleteObjectsById(event.getDeletedObjectIds(), session); break; case RemoteCommitEvent.PAYLOAD_EXTENTS: Collection c = new HashSet(); c.addAll(event.getPersistedTypeNames()); c.addAll(event.getUpdatedTypeNames()); c.addAll(event.getDeletedTypeNames()); reindexTypesByName(c, em, session); break; case RemoteCommitEvent.PAYLOAD_LOCAL_STALE_DETECTION: reindexObjectsById(event.getUpdatedObjectIds(), em, session); break; default: log.warn("Unknown remote commit event type [" + event.getPayloadType() + "], ignoring..."); } tr.commit(); } catch (Exception e) { log.error("Failed to perform remote commit syncronization", e); if (tr != null) { tr.rollback(); } } finally { if (session != null) { session.close(); } if (em != null) { em.close(); } } } @SuppressWarnings({"unchecked"}) private void reindexObjectsById(Collection oids, OpenJPAEntityManager em, CompassSession session) { for (OpenJPAId oid : (Collection<OpenJPAId>) oids) { reindexOid(oid, em, session); } } @SuppressWarnings({"unchecked"}) private void deleteObjectsById(Collection oids, CompassSession session) { for (OpenJPAId oid : (Collection<OpenJPAId>) oids) { delete(oid, session); } } @SuppressWarnings({"unchecked"}) private void reindexTypesByName(Collection typeNames, OpenJPAEntityManager em, CompassSession session) { ClassLoader loader = emf.getConfiguration().getClassResolverInstance().getClassLoader(null, null); for (String typeName : (Collection<String>) typeNames) { try { Class cls = Class.forName(typeName, true, loader); // delete all objects matching the given type session.delete(session.queryBuilder().matchAll().setTypes(new Class[]{cls})); Extent extent = em.createExtent(cls, true); for (Object o : extent.list()) { reindex(o, session); } } catch (ClassNotFoundException e) { log.error("Failed to find class", e); } } } @SuppressWarnings({"unchecked"}) private void reindexOid(OpenJPAId oid, OpenJPAEntityManager em, CompassSession session) { try { Object o = em.find(oid.getType(), oid.getIdObject()); reindex(o, session); } catch (EntityNotFoundException e) { delete(oid, session); } } private void reindex(Object o, CompassSession session) { session.save(o); } private void delete(OpenJPAId oid, CompassSession session) { Class cls = oid.getType(); Object id = oid.getIdObject(); session.delete(cls, id); } public void close() { } } public static boolean isReleasedVersion() { if (OpenJPAVersion.MAJOR_RELEASE < 1) return false; if (OpenJPAVersion.MAJOR_RELEASE == 1 && OpenJPAVersion.MINOR_RELEASE == 0 && OpenJPAVersion.PATCH_RELEASE == 0) { // OpenJPA changed things during the 1.0.0-SNAPSHOT // release period. try { Class.forName("org.apache.openjpa.event.BrokerFactoryEvent", false, OpenJPAVersion.class.getClassLoader()); return true; } catch (ClassNotFoundException cnfe) { return false; } } return true; } }