/* * 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; import java.util.HashMap; import java.util.Map; import javax.persistence.EntityManagerFactory; import org.compass.core.util.Assert; import org.compass.gps.CompassGpsException; import org.compass.gps.PassiveMirrorGpsDevice; import org.compass.gps.device.jpa.entities.EntityInformation; import org.compass.gps.device.jpa.entities.JpaEntitiesLocator; import org.compass.gps.device.jpa.entities.JpaEntitiesLocatorDetector; import org.compass.gps.device.jpa.extractor.NativeJpaExtractor; import org.compass.gps.device.jpa.extractor.NativeJpaHelper; import org.compass.gps.device.jpa.indexer.JpaIndexEntitiesIndexer; import org.compass.gps.device.jpa.indexer.JpaIndexEntitiesIndexerDetector; import org.compass.gps.device.jpa.lifecycle.JpaEntityLifecycleInjector; import org.compass.gps.device.jpa.lifecycle.JpaEntityLifecycleInjectorDetector; import org.compass.gps.device.jpa.queryprovider.DefaultJpaQueryProvider; import org.compass.gps.device.jpa.queryprovider.JpaQueryProvider; import org.compass.gps.device.support.parallel.AbstractParallelGpsDevice; import org.compass.gps.device.support.parallel.IndexEntitiesIndexer; import org.compass.gps.device.support.parallel.IndexEntity; /** * <p>A Java Persistence API Gps Device (EJB3 Persistence). * * <p>The jpa device provides support for using jpa to index a database. The path can * be viewed as: Database <-> EntityManager(JPA) <-> Objects <-> Compass::Gps * <-> Compass::Core (Search Engine). What it means is that for every object that has both * jpa and compass mappings, you will be able to index it's data, as well as real time mirroring of * data changes. * * <p>When creating the object, an <code>EntityManagerFactory</code> must be provided to the Device. * * <p>Indexing uses {@link JpaEntitiesLocator} to locate all the entities that can be * indexed (i.e. entities that have both Compass and JPA mappings). Most of the time * the {@link org.compass.gps.device.jpa.entities.DefaultJpaEntitiesLocator} is enough, but * special JPA implementation one can be provided. If none is provided, the device will use the {@link * JpaEntitiesLocatorDetector} to auto detect the correct locator (which defaults to the ({@link * org.compass.gps.device.jpa.entities.DefaultJpaEntitiesLocator}). * * <p>The indexing process itself is done through an implementation of * {@link org.compass.gps.device.jpa.indexer.JpaIndexEntitiesIndexer}. There are several implemenations * for it including a default one that uses plain JPA APIs. Specific implementations (such as Hibernate * and OpenJPA) are used for better performance. * * <p>Mirroring can be done in two ways. The first one is using JPA official API, implemeting * an Entity Lifecycle listener and specifing it for each entity class via annotations. Compass * comes with helper base clases for it, {@link AbstractCompassJpaEntityListener} and * {@link AbstractDeviceJpaEntityListener}. As far as integrating Compass with JPA for mirroring, * this is the less preferable way. The second option for mirroring is to use the * {@link JpaEntityLifecycleInjector}, which will use the internal JPA implementation to * inject global lifecycle event listerens (sadly, there is no option to do that with the * <code>EntityManagerFactory</code> API). If the {@link #setInjectEntityLifecycleListener(boolean)} is * set to <code>true</code> (defaults to <code>false</code>), the device will try to use the injector to * inject global event listeners. If no {@link JpaEntityLifecycleInjector} is defined, the device will * try to autodetect the injector based on the current support for specific JPA implementations using * the {@link JpaEntityLifecycleInjectorDetector}. See its javadoc for a list of the current JPA * implementations supported. * * <p>Mirroring can be turned off using the {@link #setMirrorDataChanges(boolean)} to <code>false</code>. * It defaults to <code>true<code>. * * <p>The device allows for {@link NativeJpaExtractor} to be set, for applications * that use a framework or by themself wrap the actual <code>EntityManagerFactory</code> implementation. * * <p>For advance usage, the device allows for {@link EntityManagerWrapper} to be set, * allowing to control the creation of <code>EntityManager</code>s, and transactions. * The {@link DefaultEntityManagerWrapper} should suffice for most cases. * * <p>The device extends the parallel device provinding supprot for parallel indexing. * * @author kimchy */ public class JpaGpsDevice extends AbstractParallelGpsDevice implements PassiveMirrorGpsDevice { /** * Creates a new JpaGpsDevice. Note that its name ({@link #setName(String)} and * entity manager factory ({@link #setEntityManagerFactory(javax.persistence.EntityManagerFactory)} * must be set. */ public JpaGpsDevice() { } /** * Creates a new device with a specific name and an entity manager factory. */ public JpaGpsDevice(String name, EntityManagerFactory entityManagerFactory) { setName(name); setEntityManagerFactory(entityManagerFactory); } private boolean mirrorDataChanges = true; private int fetchCount = 200; private EntityManagerFactory entityManagerFactory; private EntityManagerWrapper entityManagerWrapper; private JpaEntityLifecycleInjector lifecycleInjector; private boolean injectEntityLifecycleListener; private NativeJpaExtractor nativeJpaExtractor; private EntityManagerFactory nativeEntityManagerFactory; private JpaEntitiesLocator entitiesLocator; private Map<Class<?>, JpaQueryProvider> queryProviderByClass = new HashMap<Class<?>, JpaQueryProvider>(); private Map<String, JpaQueryProvider> queryProviderByName = new HashMap<String, JpaQueryProvider>(); private JpaIndexEntitiesIndexer entitiesIndexer; protected void doStart() throws CompassGpsException { Assert.notNull(entityManagerFactory, buildMessage("Must set JPA EntityManagerFactory")); if (entityManagerWrapper == null) { entityManagerWrapper = new DefaultEntityManagerWrapper(); } entityManagerWrapper.setUp(entityManagerFactory); nativeEntityManagerFactory = entityManagerFactory; if (nativeJpaExtractor != null) { nativeEntityManagerFactory = nativeJpaExtractor.extractNative(nativeEntityManagerFactory); if (nativeEntityManagerFactory == null) { throw new JpaGpsDeviceException(buildMessage("Native EntityManager extractor returned null")); } if (log.isDebugEnabled()) { log.debug(buildMessage("Using native EntityManagerFactory [" + nativeEntityManagerFactory.getClass().getName() + "] extracted by [" + nativeJpaExtractor.getClass().getName() + "]")); } } else { nativeEntityManagerFactory = NativeJpaHelper.extractNativeJpa(entityManagerFactory); if (log.isDebugEnabled()) { log.debug(buildMessage("Using native EntityManagerFactory [" + nativeEntityManagerFactory.getClass().getName() + "] using default extractor")); } } if (entitiesLocator == null) { entitiesLocator = JpaEntitiesLocatorDetector.detectLocator(nativeEntityManagerFactory, compassGps.getMirrorCompass().getSettings()); if (log.isDebugEnabled()) { log.debug(buildMessage("Using index entityLocator [" + entitiesLocator.getClass().getName() + "]")); } } injectLifecycle(); if (entitiesIndexer == null) { entitiesIndexer = JpaIndexEntitiesIndexerDetector.detectEntitiesIndexer(nativeEntityManagerFactory, compassGps.getMirrorCompass().getSettings()); } if (log.isDebugEnabled()) { log.debug(buildMessage("Using entities indexer [" + entitiesIndexer.getClass().getName() + "]")); } entitiesIndexer.setJpaGpsDevice(this); } protected void doStop() throws CompassGpsException { removeLifecycle(); } @Override public void refresh() throws CompassGpsException { if (lifecycleInjector != null && lifecycleInjector.requireRefresh()) { removeLifecycle(); injectLifecycle(); } } protected IndexEntity[] doGetIndexEntities() throws CompassGpsException { EntityInformation[] entitiesInformation = entitiesLocator.locate(nativeEntityManagerFactory, this); // apply specific select statements for (EntityInformation entityInformation : entitiesInformation) { if (queryProviderByClass.get(entityInformation.getEntityClass()) != null) { entityInformation.setQueryProvider(queryProviderByClass.get(entityInformation.getEntityClass())); } if (queryProviderByName.get(entityInformation.getName()) != null) { entityInformation.setQueryProvider(queryProviderByName.get(entityInformation.getName())); } } return entitiesInformation; } protected IndexEntitiesIndexer doGetIndexEntitiesIndexer() { return entitiesIndexer; } public EntityManagerFactory getEntityManagerFactory() { return this.entityManagerFactory; } public EntityManagerFactory getNativeEntityManagerFactory() { return this.nativeEntityManagerFactory; } /** * @see org.compass.gps.MirrorDataChangesGpsDevice#isMirrorDataChanges() */ public boolean isMirrorDataChanges() { return mirrorDataChanges; } /** * @see org.compass.gps.MirrorDataChangesGpsDevice#setMirrorDataChanges(boolean) */ public void setMirrorDataChanges(boolean mirrorDataChanges) { this.mirrorDataChanges = mirrorDataChanges; } /** * Sets the Jpa <code>EntityManagerFactory</code>. This is manadatory for the Jpa device. * * @param entityManagerFactory The entity manager factory the device will use. */ public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { this.entityManagerFactory = entityManagerFactory; } /** * Sets the Entity Manager factory wrapper to control the entity manager operations. This is optional since the * device has sensible defaults for it. * * @param entityManagerWrapper The entity manager wrapper to control the manager operations. */ public void setEntityManagerWrapper(EntityManagerWrapper entityManagerWrapper) { this.entityManagerWrapper = entityManagerWrapper; } /** * Returns the Entity Manager factory wrapper to control the entity manager operations. */ public EntityManagerWrapper getEntityManagerWrapper() { return entityManagerWrapper; } /** * <p>Sets a specialized native entity manager factory extractor. * For applications that use a framework or by themself wrap the actual * <code>EntityManagerFactory</code> implementation. * * The native extractor is mainly used for specialized {@link JpaEntityLifecycleInjector} * and {@link JpaEntitiesLocator}. */ public void setNativeExtractor(NativeJpaExtractor nativeJpaExtractor) { this.nativeJpaExtractor = nativeJpaExtractor; } /** * Returns the native extractor. */ public NativeJpaExtractor getNativeJpaExtractor() { return nativeJpaExtractor; } /** * Sets if the device should try and automatically inject global entity lifecycle * listeners using either the provided {@link JpaEntityLifecycleInjector}, or if not * set, using the {@link JpaEntityLifecycleInjectorDetector}. Defaults to <code>false</code>. */ public void setInjectEntityLifecycleListener(boolean injectEntityLifecycleListener) { this.injectEntityLifecycleListener = injectEntityLifecycleListener; } /** * If the {@link #setLifecycleInjector(org.compass.gps.device.jpa.lifecycle.JpaEntityLifecycleInjector)} is * set to <code>true</code>, the global lifecycle injector that will be used to inject global lifecycle * event listerens to the underlying implementation of the <code>EntityManagerFactory</code>. If not set, * the {@link JpaEntitiesLocatorDetector} will be used to auto-detect it. */ public void setLifecycleInjector(JpaEntityLifecycleInjector lifecycleInjector) { this.lifecycleInjector = lifecycleInjector; } /** * Sets a specific enteties locator, which is responsible for locating enteties * that need to be indexed. Not a required parameter, since will use the * {@link JpaEntitiesLocatorDetector} to auto detect that correct one. */ public void setEntitiesLocator(JpaEntitiesLocator entitiesLocator) { this.entitiesLocator = entitiesLocator; } /** * Sets the fetch count for the indexing process. A large number will perform the indexing faster, * but will consume more memory. Defaults to <code>200</code>. */ public void setFetchCount(int fetchCount) { this.fetchCount = fetchCount; } /** * Returns the fetch count for the indexing process. A large number will perform the indexing faster, * but will consume more memory. Default to <code>200</code>. */ public int getFetchCount() { return this.fetchCount; } /** * <p>Sets a specific select statement for the index process of the given * entity class. The same as {@link #setIndexQueryProvider(Class,JpaQueryProvider)} * using {@link org.compass.gps.device.jpa.queryprovider.DefaultJpaQueryProvider}. * * <p>Certain JPA implementations have specific query providers with possible * enhanced functionality in regards to indexing. When using this method * instead of providing their specific implementation might mean less functionality * from the indexer. * * <p>Note, this information is used when the device starts. * * @param entityClass The Entity class to associate the select query with * @param selectQuery The select query to execute when indexing the given entity */ public void setIndexSelectQuery(Class<?> entityClass, String selectQuery) { setIndexQueryProvider(entityClass, new DefaultJpaQueryProvider(selectQuery)); } /** * Sets a specific select statement for the index process of the given * entity name. The same as {@link #setIndexQueryProvider(String,JpaQueryProvider)} * using {@link org.compass.gps.device.jpa.queryprovider.DefaultJpaQueryProvider}. * * <p>Certain JPA implementations have specific query providers with possible * enhanced functionality in regards to indexing. When using this method * instead of providing their specific implementation might mean less functionality * from the indexer. * * <p>Note, this information is used when the device starts. * * @param entityName The entity name to associate the select query with * @param selectQuery The select query to execute when indexing the given entity */ public void setIndexSelectQuery(String entityName, String selectQuery) { setIndexQueryProvider(entityName, new DefaultJpaQueryProvider(selectQuery)); } /** * <p>Sets a specific query provider for the index process of the given entity class. * <p>Note, this information is used when the device starts. * * @param entityClass The Entity class to associate the query provider with * @param queryProvider The query provider to execute when indexing the given entity */ public void setIndexQueryProvider(Class<?> entityClass, JpaQueryProvider queryProvider) { queryProviderByClass.put(entityClass, queryProvider); } /** * <p>Sets a specific query provider for the index process of the given entity name. * <p>Note, this information is used when the device starts. * * @param entityName The Entity name to associate the query provider with * @param queryProvider The query provider to execute when indexing the given entity */ public void setIndexQueryProvider(String entityName, JpaQueryProvider queryProvider) { queryProviderByName.put(entityName, queryProvider); } /** * <p>Sets an index entity info that will control how the given entity will * be indexed. */ public void setIndexEntityInfo(JpaIndexEntityInfo indexEntityInfo) { if (indexEntityInfo.getEntityName() == null) { throw new IllegalArgumentException("entityName must not be null"); } setIndexQueryProvider(indexEntityInfo.getEntityName(), indexEntityInfo.getQueryProvider()); } /** * Sets a custom entities indexer that will be used to index the data. By default will * be detected automatically based on the actual implemenation of JPA used and will try * to use it. */ public void setEntitiesIndexer(JpaIndexEntitiesIndexer entitiesIndexer) { this.entitiesIndexer = entitiesIndexer; } private void injectLifecycle() { if (injectEntityLifecycleListener && mirrorDataChanges) { if (lifecycleInjector == null) { lifecycleInjector = JpaEntityLifecycleInjectorDetector.detectInjector(nativeEntityManagerFactory, compassGps.getMirrorCompass().getSettings()); } if (lifecycleInjector == null) { throw new JpaGpsDeviceException(buildMessage("Failed to locate lifecycleInjector")); } if (log.isDebugEnabled()) { log.debug(buildMessage("Using lifecycleInjector [" + lifecycleInjector.getClass().getName() + "]")); } lifecycleInjector.injectLifecycle(nativeEntityManagerFactory, this); } } private void removeLifecycle() { if (injectEntityLifecycleListener && mirrorDataChanges) { lifecycleInjector.removeLifecycle(nativeEntityManagerFactory, this); } } }