/**
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.openejb.core.cmp.jpa;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.core.CoreDeploymentInfo;
import org.apache.openejb.core.ThreadContext;
import org.apache.openejb.core.cmp.CmpCallback;
import org.apache.openejb.core.cmp.CmpEngine;
import org.apache.openejb.core.cmp.ComplexKeyGenerator;
import org.apache.openejb.core.cmp.KeyGenerator;
import org.apache.openejb.core.cmp.SimpleKeyGenerator;
import org.apache.openejb.core.cmp.cmp2.Cmp2KeyGenerator;
import org.apache.openejb.core.cmp.cmp2.Cmp2Util;
import org.apache.openjpa.event.AbstractLifecycleListener;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import javax.ejb.CreateException;
import javax.ejb.EJBException;
import javax.ejb.EJBLocalObject;
import javax.ejb.EJBObject;
import javax.ejb.EntityBean;
import javax.ejb.FinderException;
import javax.ejb.RemoveException;
import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.transaction.Status;
import javax.transaction.TransactionManager;
import javax.transaction.TransactionSynchronizationRegistry;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class JpaCmpEngine implements CmpEngine {
private static final Object[] NO_ARGS = new Object[0];
public static final String CMP_PERSISTENCE_CONTEXT_REF_NAME = "openejb/cmp";
/**
* Used to notify call CMP callback methods.
*/
private final CmpCallback cmpCallback;
private final TransactionManager transactionManager;
private final TransactionSynchronizationRegistry synchronizationRegistry;
/**
* Thread local to track the beans we are creating to avoid an extra ejbStore callback
*/
private final ThreadLocal<Set<EntityBean>> creating = new ThreadLocal<Set<EntityBean>>() {
protected Set<EntityBean> initialValue() {
return new HashSet<EntityBean>();
}
};
/**
* Listener added to entity managers.
*/
protected Object entityManagerListener;
public JpaCmpEngine(CmpCallback cmpCallback, TransactionManager transactionManager, TransactionSynchronizationRegistry synchronizationRegistry) {
this.cmpCallback = cmpCallback;
this.transactionManager = transactionManager;
this.synchronizationRegistry = synchronizationRegistry;
}
public synchronized void deploy(CoreDeploymentInfo deploymentInfo) throws OpenEJBException {
configureKeyGenerator(deploymentInfo);
}
public synchronized void undeploy(CoreDeploymentInfo deploymentInfo) throws OpenEJBException {
deploymentInfo.setKeyGenerator(null);
}
private EntityManager getEntityManager(CoreDeploymentInfo deploymentInfo) {
EntityManager entityManager = null;
try {
entityManager = (EntityManager) deploymentInfo.getJndiEnc().lookup("java:comp/env/" + CMP_PERSISTENCE_CONTEXT_REF_NAME);
} catch (NamingException ignroed) {
}
if (entityManager == null) {
throw new EJBException("Entity manager not found at \"openejb/cmp\" in jndi ejb " + deploymentInfo.getDeploymentID());
}
registerListener(entityManager);
return entityManager;
}
private synchronized void registerListener(EntityManager entityManager) {
if (entityManager instanceof OpenJPAEntityManagerSPI) {
OpenJPAEntityManagerSPI openjpaEM = (OpenJPAEntityManagerSPI) entityManager;
OpenJPAEntityManagerFactorySPI openjpaEMF = (OpenJPAEntityManagerFactorySPI) openjpaEM.getEntityManagerFactory();
if (entityManagerListener == null) {
entityManagerListener = new OpenJPALifecycleListener();
}
openjpaEMF.addLifecycleListener(entityManagerListener, (Class[])null);
return;
}
Object delegate = entityManager.getDelegate();
if (delegate != entityManager && delegate instanceof EntityManager) {
registerListener((EntityManager) delegate);
}
}
public Object createBean(EntityBean bean, ThreadContext callContext) throws CreateException {
// TODO verify that extract primary key requires a flush followed by a merge
boolean startedTx = startTransaction("persist");
creating.get().add(bean);
try {
CoreDeploymentInfo deploymentInfo = callContext.getDeploymentInfo();
EntityManager entityManager = getEntityManager(deploymentInfo);
entityManager.persist(bean);
entityManager.flush();
bean = entityManager.merge(bean);
// extract the primary key from the bean
KeyGenerator kg = deploymentInfo.getKeyGenerator();
Object primaryKey = kg.getPrimaryKey(bean);
// add to transaction cache
getTransactionCache().put(deploymentInfo.getCmpImplClass(), primaryKey, bean);
return primaryKey;
} finally {
creating.get().remove(bean);
commitTransaction(startedTx, "persist");
}
}
public Object loadBean(ThreadContext callContext, Object primaryKey) {
boolean startedTx = startTransaction("load");
try {
CoreDeploymentInfo deploymentInfo = callContext.getDeploymentInfo();
Class<?> beanClass = deploymentInfo.getCmpImplClass();
// First check the transaction cache
Object bean = getTransactionCache().get(beanClass, primaryKey);
if (bean == null) {
// Try to load it from the entity manager
EntityManager entityManager = getEntityManager(deploymentInfo);
bean = entityManager.find(beanClass, primaryKey);
}
return bean;
} finally {
commitTransaction(startedTx, "load");
}
}
public void storeBeanIfNoTx(ThreadContext callContext, Object bean) {
boolean startedTx = startTransaction("store");
if (startedTx) {
CoreDeploymentInfo deploymentInfo = callContext.getDeploymentInfo();
try {
EntityManager entityManager = getEntityManager(deploymentInfo);
entityManager.merge(bean);
} finally {
commitTransaction(startedTx, "store");
}
}
}
public void removeBean(ThreadContext callContext) {
boolean startedTx = startTransaction("remove");
try {
CoreDeploymentInfo deploymentInfo = callContext.getDeploymentInfo();
Class<?> beanClass = deploymentInfo.getCmpImplClass();
EntityManager entityManager = getEntityManager(deploymentInfo);
Object primaryKey = callContext.getPrimaryKey();
// First check the transaction cache
Object bean = getTransactionCache().get(beanClass, primaryKey);
if (bean == null) {
// Try to load it from the entity manager
bean = entityManager.find(beanClass, primaryKey);
}
// remove the bean
entityManager.remove(bean);
getTransactionCache().remove(beanClass, primaryKey);
} finally {
commitTransaction(startedTx, "remove");
}
}
public List<Object> queryBeans(ThreadContext callContext, Method queryMethod, Object[] args) throws FinderException {
CoreDeploymentInfo deploymentInfo = callContext.getDeploymentInfo();
EntityManager entityManager = getEntityManager(deploymentInfo);
StringBuilder queryName = new StringBuilder();
queryName.append(deploymentInfo.getAbstractSchemaName()).append(".").append(queryMethod.getName());
String shortName = queryName.toString();
if (queryMethod.getParameterTypes().length > 0) {
queryName.append('(');
boolean first = true;
for (Class<?> parameterType : queryMethod.getParameterTypes()) {
if (!first) queryName.append(',');
queryName.append(parameterType.getCanonicalName());
first = false;
}
queryName.append(')');
}
String fullName = queryName.toString();
Query query = createNamedQuery(entityManager, fullName);
if (query == null) {
query = createNamedQuery(entityManager, shortName);
if (query == null) {
throw new FinderException("No query defined for method " + fullName);
}
}
return executeQuery(query, args);
}
public List<Object> queryBeans(CoreDeploymentInfo deploymentInfo, String signature, Object[] args) throws FinderException {
EntityManager entityManager = getEntityManager(deploymentInfo);
Query query = createNamedQuery(entityManager, signature);
if (query == null) {
int parenIndex = signature.indexOf('(');
if (parenIndex > 0) {
String shortName = signature.substring(0, parenIndex);
query = createNamedQuery(entityManager, shortName);
}
if (query == null) {
throw new FinderException("No query defined for method " + signature);
}
}
return executeQuery(query, args);
}
private List<Object> executeQuery(Query query, Object[] args) {
// process args
if (args == null) {
args = NO_ARGS;
}
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
// ejb proxies need to be swapped out for real instance classes
if (arg instanceof EJBObject) {
arg = Cmp2Util.getEntityBean(((EJBObject) arg));
}
if (arg instanceof EJBLocalObject) {
arg = Cmp2Util.getEntityBean(((EJBLocalObject) arg));
}
query.setParameter(i + 1, arg);
}
// todo results should not be iterated over, but should instead
// perform all work in a wrapper list on demand by the application code
List results = query.getResultList();
for (Object value : results) {
if (value instanceof EntityBean) {
// todo don't activate beans already activated
EntityBean entity = (EntityBean) value;
cmpCallback.setEntityContext(entity);
cmpCallback.ejbActivate(entity);
}
}
//noinspection unchecked
return results;
}
private Query createNamedQuery(EntityManager entityManager, String name) {
try {
return entityManager.createNamedQuery(name);
} catch (IllegalArgumentException ignored) {
// soooo lame that jpa throws an exception instead of returning null....
ignored.printStackTrace();
return null;
}
}
private boolean startTransaction(String operation) {
try {
if (Status.STATUS_NO_TRANSACTION == transactionManager.getStatus()) {
transactionManager.begin();
return true;
}
return false;
} catch (Exception e) {
throw new EJBException("Unable to start transaction for " + operation + " operation", e);
}
}
private void commitTransaction(boolean startedTx, String operation) {
try {
if (startedTx) {
transactionManager.commit();
}
} catch (Exception e) {
throw new EJBException("Unable to complete transaction for " + operation + " operation", e);
}
}
private void configureKeyGenerator(CoreDeploymentInfo di) throws OpenEJBException {
if (di.isCmp2()) {
di.setKeyGenerator(new Cmp2KeyGenerator());
} else {
String primaryKeyField = di.getPrimaryKeyField();
Class cmpBeanImpl = di.getCmpImplClass();
if (primaryKeyField != null) {
di.setKeyGenerator(new SimpleKeyGenerator(cmpBeanImpl, primaryKeyField));
} else if (Object.class.equals(di.getPrimaryKeyClass())) {
di.setKeyGenerator(new SimpleKeyGenerator(cmpBeanImpl, "OpenEJB_pk"));
} else {
di.setKeyGenerator(new ComplexKeyGenerator(cmpBeanImpl, di.getPrimaryKeyClass()));
}
}
}
// todo remove when OpenJPA fixes the new-remove-new-find bug
private TransactionCache getTransactionCache() {
TransactionCache transactionCache = (TransactionCache) synchronizationRegistry.getResource(TransactionCache.class);
if (transactionCache == null) {
transactionCache = new TransactionCache();
synchronizationRegistry.putResource(TransactionCache.class, transactionCache);
}
return transactionCache;
}
private static class TransactionCache {
private final Map<Class,Map<Object,Object>> cache = new HashMap<Class,Map<Object,Object>>();
public Object get(Class clazz, Object primaryKey) {
Map<Object, Object> instances = cache.get(clazz);
if (instances == null) return null;
return instances.get(primaryKey);
}
public void put(Class clazz, Object primaryKey, Object value) {
Map<Object, Object> instances = cache.get(clazz);
if (instances == null) {
instances = new HashMap<Object, Object>();
cache.put(clazz, instances);
}
instances.put(primaryKey, value);
}
public Object remove(Class clazz, Object primaryKey) {
Map<Object, Object> instances = cache.get(clazz);
if (instances == null) return null;
return instances.remove(primaryKey);
}
}
private class OpenJPALifecycleListener extends AbstractLifecycleListener {
// protected void eventOccurred(LifecycleEvent event) {
// int type = event.getType();
// switch (type) {
// case LifecycleEvent.BEFORE_PERSIST:
// System.out.println("BEFORE_PERSIST");
// break;
// case LifecycleEvent.AFTER_PERSIST:
// System.out.println("AFTER_PERSIST");
// break;
// case LifecycleEvent.AFTER_LOAD:
// System.out.println("AFTER_LOAD");
// break;
// case LifecycleEvent.BEFORE_STORE:
// System.out.println("BEFORE_STORE");
// break;
// case LifecycleEvent.AFTER_STORE:
// System.out.println("AFTER_STORE");
// break;
// case LifecycleEvent.BEFORE_CLEAR:
// System.out.println("BEFORE_CLEAR");
// break;
// case LifecycleEvent.AFTER_CLEAR:
// System.out.println("AFTER_CLEAR");
// break;
// case LifecycleEvent.BEFORE_DELETE:
// System.out.println("BEFORE_DELETE");
// break;
// case LifecycleEvent.AFTER_DELETE:
// System.out.println("AFTER_DELETE");
// break;
// case LifecycleEvent.BEFORE_DIRTY:
// System.out.println("BEFORE_DIRTY");
// break;
// case LifecycleEvent.AFTER_DIRTY:
// System.out.println("AFTER_DIRTY");
// break;
// case LifecycleEvent.BEFORE_DIRTY_FLUSHED:
// System.out.println("BEFORE_DIRTY_FLUSHED");
// break;
// case LifecycleEvent.AFTER_DIRTY_FLUSHED:
// System.out.println("AFTER_DIRTY_FLUSHED");
// break;
// case LifecycleEvent.BEFORE_DETACH:
// System.out.println("BEFORE_DETACH");
// break;
// case LifecycleEvent.AFTER_DETACH:
// System.out.println("AFTER_DETACH");
// break;
// case LifecycleEvent.BEFORE_ATTACH:
// System.out.println("BEFORE_ATTACH");
// break;
// case LifecycleEvent.AFTER_ATTACH:
// System.out.println("AFTER_ATTACH");
// break;
// case LifecycleEvent.AFTER_REFRESH:
// System.out.println("AFTER_REFRESH");
// break;
// default:
// System.out.println("default");
// break;
// }
// super.eventOccurred(event);
// }
public void afterLoad(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
Object bean = lifecycleEvent.getSource();
// This may seem a bit strange to call ejbActivate immedately followed by ejbLoad,
// but it is completely legal. Since the ejbActivate method is not allowed to access
// persistent state of the bean (EJB 3.0fr 8.5.2) there should be no concern that the
// call back method clears the bean state before ejbLoad is called.
cmpCallback.setEntityContext((EntityBean) bean);
cmpCallback.ejbActivate((EntityBean) bean);
cmpCallback.ejbLoad((EntityBean) bean);
}
public void beforeStore(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
EntityBean bean = (EntityBean) lifecycleEvent.getSource();
if (!creating.get().contains(bean)) {
cmpCallback.ejbStore(bean);
}
}
public void afterAttach(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
Object bean = lifecycleEvent.getSource();
cmpCallback.setEntityContext((EntityBean) bean);
}
public void beforeDelete(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
try {
Object bean = lifecycleEvent.getSource();
cmpCallback.ejbRemove((EntityBean) bean);
} catch (RemoveException e) {
throw new PersistenceException(e);
}
}
public void afterDetach(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
// todo detach is called after ejbRemove which does not need ejbPassivate
Object bean = lifecycleEvent.getSource();
cmpCallback.ejbPassivate((EntityBean) bean);
cmpCallback.unsetEntityContext((EntityBean) bean);
}
public void beforePersist(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
public void afterRefresh(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
public void beforeDetach(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
public void beforeAttach(LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
}
}