/*
* 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.BeanContext;
import org.apache.openejb.OpenEJBException;
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.openejb.core.transaction.TransactionPolicy;
import org.apache.openejb.core.transaction.TransactionType;
import org.apache.openjpa.event.AbstractLifecycleListener;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAEntityManagerSPI;
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.InitialContext;
import javax.naming.NamingException;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.apache.openejb.core.transaction.EjbTransactionUtil.afterInvoke;
import static org.apache.openejb.core.transaction.EjbTransactionUtil.createTransactionPolicy;
public class JpaCmpEngine implements CmpEngine {
private static final Object[] NO_ARGS = new Object[0];
public static final String CMP_PERSISTENCE_CONTEXT_REF_NAME = "comp/env/openejb/cmp";
/**
* Used to notify call CMP callback methods.
*/
private final CmpCallback cmpCallback;
/**
* 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(final CmpCallback cmpCallback) {
this.cmpCallback = cmpCallback;
}
public synchronized void deploy(final BeanContext beanContext) throws OpenEJBException {
configureKeyGenerator(beanContext);
}
public synchronized void undeploy(final BeanContext beanContext) throws OpenEJBException {
beanContext.setKeyGenerator(null);
}
private EntityManager getEntityManager(final BeanContext beanContext) {
EntityManager entityManager = null;
try {
entityManager = (EntityManager) beanContext.getJndiEnc().lookup(CMP_PERSISTENCE_CONTEXT_REF_NAME);
} catch (final NamingException ignored) {
//TODO see OPENEJB-1259 temporary hack until geronimo jndi integration works better
try {
entityManager = (EntityManager) new InitialContext().lookup("java:" + CMP_PERSISTENCE_CONTEXT_REF_NAME);
} catch (final NamingException ignored2) {
//ignore
}
}
if (entityManager == null) {
throw new EJBException("Entity manager not found at \"openejb/cmp\" in jndi ejb " + beanContext.getDeploymentID());
}
registerListener(entityManager);
return entityManager;
}
private synchronized void registerListener(final EntityManager entityManager) {
if (entityManager instanceof OpenJPAEntityManagerSPI) {
final OpenJPAEntityManagerSPI openjpaEM = (OpenJPAEntityManagerSPI) entityManager;
final OpenJPAEntityManagerFactorySPI openjpaEMF = (OpenJPAEntityManagerFactorySPI) openjpaEM.getEntityManagerFactory();
if (entityManagerListener == null) {
entityManagerListener = new OpenJPALifecycleListener();
}
openjpaEMF.addLifecycleListener(entityManagerListener, (Class[]) null);
return;
}
final Object delegate = entityManager.getDelegate();
if (delegate != entityManager && delegate instanceof EntityManager) {
registerListener((EntityManager) delegate);
}
}
public Object createBean(EntityBean bean, final ThreadContext callContext) throws CreateException {
// TODO verify that extract primary key requires a flush followed by a merge
final TransactionPolicy txPolicy = startTransaction("persist", callContext);
creating.get().add(bean);
try {
final BeanContext beanContext = callContext.getBeanContext();
final EntityManager entityManager = getEntityManager(beanContext);
entityManager.persist(bean);
entityManager.flush();
bean = entityManager.merge(bean);
// extract the primary key from the bean
final KeyGenerator kg = beanContext.getKeyGenerator();
final Object primaryKey = kg.getPrimaryKey(bean);
return primaryKey;
} finally {
creating.get().remove(bean);
commitTransaction("persist", callContext, txPolicy);
}
}
public Object loadBean(final ThreadContext callContext, final Object primaryKey) {
final TransactionPolicy txPolicy = startTransaction("load", callContext);
try {
final BeanContext beanContext = callContext.getBeanContext();
final Class<?> beanClass = beanContext.getCmpImplClass();
// Try to load it from the entity manager
final EntityManager entityManager = getEntityManager(beanContext);
return entityManager.find(beanClass, primaryKey);
} finally {
commitTransaction("load", callContext, txPolicy);
}
}
public void storeBeanIfNoTx(final ThreadContext callContext, final Object bean) {
final TransactionPolicy callerTxPolicy = callContext.getTransactionPolicy();
if (callerTxPolicy != null && callerTxPolicy.isTransactionActive()) {
return;
}
final TransactionPolicy txPolicy = startTransaction("store", callContext);
try {
// only store if we started a new transaction
if (txPolicy.isNewTransaction()) {
final EntityManager entityManager = getEntityManager(callContext.getBeanContext());
entityManager.merge(bean);
}
} finally {
commitTransaction("store", callContext, txPolicy);
}
}
public void removeBean(final ThreadContext callContext) {
final TransactionPolicy txPolicy = startTransaction("remove", callContext);
try {
final BeanContext deploymentInfo = callContext.getBeanContext();
final Class<?> beanClass = deploymentInfo.getCmpImplClass();
final EntityManager entityManager = getEntityManager(deploymentInfo);
final Object primaryKey = callContext.getPrimaryKey();
// Try to load it from the entity manager
final Object bean = entityManager.find(beanClass, primaryKey);
// remove the bean
entityManager.remove(bean);
} finally {
commitTransaction("remove", callContext, txPolicy);
}
}
public List<Object> queryBeans(final ThreadContext callContext, final Method queryMethod, final Object[] args) throws FinderException {
final BeanContext deploymentInfo = callContext.getBeanContext();
final EntityManager entityManager = getEntityManager(deploymentInfo);
final StringBuilder queryName = new StringBuilder();
queryName.append(deploymentInfo.getAbstractSchemaName()).append(".").append(queryMethod.getName());
final String shortName = queryName.toString();
if (queryMethod.getParameterTypes().length > 0) {
queryName.append('(');
boolean first = true;
for (final Class<?> parameterType : queryMethod.getParameterTypes()) {
if (!first) {
queryName.append(',');
}
queryName.append(parameterType.getCanonicalName());
first = false;
}
queryName.append(')');
}
final 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 executeSelectQuery(query, args);
}
public List<Object> queryBeans(final BeanContext beanContext, final String signature, final Object[] args) throws FinderException {
final EntityManager entityManager = getEntityManager(beanContext);
Query query = createNamedQuery(entityManager, signature);
if (query == null) {
final int parenIndex = signature.indexOf('(');
if (parenIndex > 0) {
final String shortName = signature.substring(0, parenIndex);
query = createNamedQuery(entityManager, shortName);
}
if (query == null) {
throw new FinderException("No query defined for method " + signature);
}
}
return executeSelectQuery(query, args);
}
private List<Object> executeSelectQuery(final 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);
}
try {
query.getParameter(i + 1);
} catch (final IllegalArgumentException e) {
// IllegalArgumentException means that the parameter with the
// specified position does not exist
continue;
}
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
final List results = query.getResultList();
for (final Object value : results) {
if (value instanceof EntityBean) {
// todo don't activate beans already activated
final EntityBean entity = (EntityBean) value;
cmpCallback.setEntityContext(entity);
cmpCallback.ejbActivate(entity);
}
}
//noinspection unchecked
return results;
}
public int executeUpdateQuery(final BeanContext beanContext, final String signature, Object[] args) throws FinderException {
final EntityManager entityManager = getEntityManager(beanContext);
Query query = createNamedQuery(entityManager, signature);
if (query == null) {
final int parenIndex = signature.indexOf('(');
if (parenIndex > 0) {
final String shortName = signature.substring(0, parenIndex);
query = createNamedQuery(entityManager, shortName);
}
if (query == null) {
throw new FinderException("No query defined for method " + signature);
}
}
// 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);
}
final int result = query.executeUpdate();
return result;
}
private Query createNamedQuery(final EntityManager entityManager, final String name) {
try {
return entityManager.createNamedQuery(name);
} catch (final IllegalArgumentException ignored) {
// soooo lame that jpa throws an exception instead of returning null....
ignored.printStackTrace();
return null;
}
}
private TransactionPolicy startTransaction(final String operation, final ThreadContext callContext) {
try {
final TransactionPolicy txPolicy = createTransactionPolicy(TransactionType.Required, callContext);
return txPolicy;
} catch (final Exception e) {
throw new EJBException("Unable to start transaction for " + operation + " operation", e);
}
}
private void commitTransaction(final String operation, final ThreadContext callContext, final TransactionPolicy txPolicy) {
try {
afterInvoke(txPolicy, callContext);
} catch (final Exception e) {
throw new EJBException("Unable to complete transaction for " + operation + " operation", e);
}
}
private void configureKeyGenerator(final BeanContext di) throws OpenEJBException {
if (di.isCmp2()) {
di.setKeyGenerator(new Cmp2KeyGenerator());
} else {
final String primaryKeyField = di.getPrimaryKeyField();
final 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()));
}
}
}
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(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
final 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(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
final EntityBean bean = (EntityBean) lifecycleEvent.getSource();
if (!creating.get().contains(bean)) {
cmpCallback.ejbStore(bean);
}
}
public void afterAttach(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
final Object bean = lifecycleEvent.getSource();
cmpCallback.setEntityContext((EntityBean) bean);
}
public void beforeDelete(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
try {
final Object bean = lifecycleEvent.getSource();
cmpCallback.ejbRemove((EntityBean) bean);
} catch (final RemoveException e) {
throw new PersistenceException(e);
}
}
public void afterDetach(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
// todo detach is called after ejbRemove which does not need ejbPassivate
final Object bean = lifecycleEvent.getSource();
cmpCallback.ejbPassivate((EntityBean) bean);
cmpCallback.unsetEntityContext((EntityBean) bean);
}
public void beforePersist(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
public void afterRefresh(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
public void beforeDetach(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
public void beforeAttach(final LifecycleEvent lifecycleEvent) {
eventOccurred(lifecycleEvent);
}
}
}