diff --git a/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/PersistenceUnitInfo.java b/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/PersistenceUnitInfo.java index 5af0c4266c2..f38ca2df6d4 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/PersistenceUnitInfo.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/PersistenceUnitInfo.java @@ -27,6 +27,9 @@ public class PersistenceUnitInfo extends InfoObject { public String id; public String name; public String provider; + // Jakarta Persistence 3.2 CDI integration: / from persistence.xml + public final List qualifiers = new ArrayList<>(); + public String scope; public String transactionType; public String jtaDataSource; public String nonJtaDataSource; diff --git a/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactory.java b/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactory.java index 2b3f94439cb..8b791d4130c 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactory.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactory.java @@ -279,7 +279,8 @@ public boolean isOpen() { @Override public synchronized void close() { - if (delegate != null) { + // the application may have closed the EMF itself, closing it twice is an error + if (delegate != null && delegate.isOpen()) { delegate.close(); } } diff --git a/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java b/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java index 99f3ec075d8..94e4ecadf6a 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/cdi/OptimizedLoaderService.java @@ -127,6 +127,7 @@ protected List loadExtensions(final ClassLoader classLoader } list.add(new org.apache.openejb.cdi.concurrency.ConcurrencyCDIExtension()); + list.add(new org.apache.openejb.cdi.persistence.JpaCDIExtension()); final Collection extensionCopy = new ArrayList<>(list); diff --git a/container/openejb-core/src/main/java/org/apache/openejb/cdi/persistence/JpaCDIExtension.java b/container/openejb-core/src/main/java/org/apache/openejb/cdi/persistence/JpaCDIExtension.java new file mode 100644 index 00000000000..6261a47a811 --- /dev/null +++ b/container/openejb-core/src/main/java/org/apache/openejb/cdi/persistence/JpaCDIExtension.java @@ -0,0 +1,290 @@ +/* + * 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.cdi.persistence; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.inject.Qualifier; +import jakarta.persistence.Cache; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnitUtil; +import jakarta.persistence.SchemaManager; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.metamodel.Metamodel; +import org.apache.openejb.assembler.classic.AppInfo; +import org.apache.openejb.assembler.classic.PersistenceUnitInfo; +import org.apache.openejb.cdi.OpenEJBLifecycle; +import org.apache.openejb.loader.SystemInstance; +import org.apache.openejb.spi.ContainerSystem; +import org.apache.openejb.util.LogCategory; +import org.apache.openejb.util.Logger; + +import javax.naming.NamingException; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +/** + * CDI extension registering the beans required by the Jakarta Persistence 3.2 / + * Jakarta EE 11 CDI integration (Platform specification, "Jakarta Persistence & + * Jakarta Context and Dependency Injection (CDI) Integration"). + * + *

For each persistence unit the container must make available: + *

    + *
  • an {@link EntityManagerFactory} bean, {@code @ApplicationScoped}, whose bean name + * is the persistence unit name;
  • + *
  • an {@link EntityManager} bean, in the scope given by the {@code } element + * (defaulting to {@code jakarta.transaction.TransactionScoped});
  • + *
  • {@link CriteriaBuilder}, {@link PersistenceUnitUtil}, {@link Cache}, + * {@link SchemaManager} and {@link Metamodel} beans, {@code @Dependent}, each simply + * obtained from the matching getter of the {@code EntityManagerFactory}.
  • + *
+ * + *

All of them carry the qualifiers given by the {@code } elements of + * {@code persistence.xml}, or {@code @Default} when none is declared. + */ +public class JpaCDIExtension implements Extension { + + private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB.createChild("cdi"), JpaCDIExtension.class); + + private static final String PERSISTENCE_UNIT_NAMING_CONTEXT = "openejb/PersistenceUnit/"; + + /** + * Default scope of the {@code EntityManager} bean. Resolved reflectively so that the + * extension keeps working on distributions without the Jakarta Transactions API. + */ + private static final String TRANSACTION_SCOPED = "jakarta.transaction.TransactionScoped"; + + void registerBeans(@Observes final AfterBeanDiscovery afterBeanDiscovery) { + final AppInfo appInfo = OpenEJBLifecycle.CURRENT_APP_INFO.get(); + if (appInfo == null) { + return; + } + + for (final PersistenceUnitInfo unitInfo : appInfo.persistenceUnits) { + final Set qualifiers = validateAndCreateQualifiers(unitInfo, afterBeanDiscovery); + if (qualifiers == null) { + continue; + } + + final Class entityManagerScope = resolveEntityManagerScope(unitInfo, afterBeanDiscovery); + if (entityManagerScope == null) { + continue; + } + + logger.debug("Registering CDI beans for persistence unit '" + unitInfo.name + "'"); + + afterBeanDiscovery.addBean() + .id("tomee.jpa." + EntityManagerFactory.class.getName() + "#" + unitInfo.id) + .beanClass(EntityManagerFactory.class) + .types(Object.class, EntityManagerFactory.class) + .qualifiers(qualifiers.toArray(new Annotation[0])) + .scope(ApplicationScoped.class) + .name(unitInfo.name) + .createWith(cc -> lookupEntityManagerFactory(unitInfo.id)); + + // the EntityManager is created per contextual instance and closed when the context ends + afterBeanDiscovery.addBean() + .id("tomee.jpa." + EntityManager.class.getName() + "#" + unitInfo.id) + .beanClass(EntityManager.class) + .types(Object.class, EntityManager.class) + .qualifiers(qualifiers.toArray(new Annotation[0])) + .scope(entityManagerScope) + .produceWith(instance -> lookupEntityManagerFactory(unitInfo.id).createEntityManager()) + .disposeWith((em, cc) -> { + if (em.isOpen()) { + em.close(); + } + }); + + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, CriteriaBuilder.class, EntityManagerFactory::getCriteriaBuilder); + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, PersistenceUnitUtil.class, EntityManagerFactory::getPersistenceUnitUtil); + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, Cache.class, EntityManagerFactory::getCache); + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, SchemaManager.class, EntityManagerFactory::getSchemaManager); + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, Metamodel.class, EntityManagerFactory::getMetamodel); + } + } + + /** + * The five utility beans are {@code @Dependent} and simply delegate to the matching + * getter of the {@code EntityManagerFactory}. + */ + private void addUtilityBean(final AfterBeanDiscovery afterBeanDiscovery, + final PersistenceUnitInfo unitInfo, + final Set qualifiers, + final Class type, + final Function accessor) { + afterBeanDiscovery.addBean() + .id("tomee.jpa." + type.getName() + "#" + unitInfo.id) + .beanClass(type) + .types(Object.class, type) + .qualifiers(qualifiers.toArray(new Annotation[0])) + .scope(Dependent.class) + .createWith(cc -> accessor.apply(lookupEntityManagerFactory(unitInfo.id))); + } + + /** + * Builds the qualifier set of a persistence unit: the {@code } elements, or + * {@code @Default} when none is declared. {@code @Any} is always added, as for any bean. + * + * @return {@code null} if a qualifier is invalid, in which case a definition error has + * been reported + */ + private Set validateAndCreateQualifiers(final PersistenceUnitInfo unitInfo, + final AfterBeanDiscovery afterBeanDiscovery) { + final Set qualifiers = new LinkedHashSet<>(); + qualifiers.add(Any.Literal.INSTANCE); + + if (unitInfo.qualifiers.isEmpty()) { + qualifiers.add(Default.Literal.INSTANCE); + return qualifiers; + } + + final ClassLoader loader = Thread.currentThread().getContextClassLoader(); + for (final String qualifierName : unitInfo.qualifiers) { + final Class qualifierClass; + try { + qualifierClass = loader.loadClass(qualifierName); + } catch (final ClassNotFoundException e) { + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Qualifier class " + qualifierName + + " of persistence unit " + unitInfo.name + " cannot be loaded", e)); + return null; + } + + if (!qualifierClass.isAnnotation()) { + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Qualifier " + qualifierName + + " of persistence unit " + unitInfo.name + " must be an annotation type")); + return null; + } + + @SuppressWarnings("unchecked") + final Class annotationClass = (Class) qualifierClass; + if (!annotationClass.isAnnotationPresent(Qualifier.class)) { + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Qualifier " + qualifierName + + " of persistence unit " + unitInfo.name + " must be annotated with @Qualifier")); + return null; + } + + qualifiers.add(createAnnotation(annotationClass)); + } + + return qualifiers; + } + + /** + * Resolves the {@code } element of the persistence unit, defaulting to + * {@code jakarta.transaction.TransactionScoped}. + * + * @return {@code null} if the scope is invalid, in which case a definition error has + * been reported + */ + @SuppressWarnings("unchecked") + private Class resolveEntityManagerScope(final PersistenceUnitInfo unitInfo, + final AfterBeanDiscovery afterBeanDiscovery) { + final String scope = unitInfo.scope == null || unitInfo.scope.isBlank() ? TRANSACTION_SCOPED : unitInfo.scope.trim(); + final ClassLoader loader = Thread.currentThread().getContextClassLoader(); + + final Class scopeClass; + try { + scopeClass = loader.loadClass(scope); + } catch (final ClassNotFoundException e) { + if (unitInfo.scope == null || unitInfo.scope.isBlank()) { + // no Jakarta Transactions on the classpath, fall back to @Dependent + return Dependent.class; + } + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Scope class " + scope + + " of persistence unit " + unitInfo.name + " cannot be loaded", e)); + return null; + } + + if (!scopeClass.isAnnotation()) { + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Scope " + scope + + " of persistence unit " + unitInfo.name + " must be an annotation type")); + return null; + } + + return (Class) scopeClass; + } + + private EntityManagerFactory lookupEntityManagerFactory(final String unitId) { + final ContainerSystem containerSystem = SystemInstance.get().getComponent(ContainerSystem.class); + if (containerSystem == null) { + throw new IllegalStateException("ContainerSystem is not available"); + } + + final Object instance; + try { + instance = containerSystem.getJNDIContext().lookup(PERSISTENCE_UNIT_NAMING_CONTEXT + unitId); + } catch (final NamingException e) { + throw new IllegalStateException("Unable to lookup persistence unit " + unitId, e); + } + + if (!(instance instanceof EntityManagerFactory)) { + throw new IllegalStateException("Persistence unit " + unitId + " is not an EntityManagerFactory, found " + + (instance == null ? "null" : instance.getClass().getName())); + } + return EntityManagerFactory.class.cast(instance); + } + + /** + * Creates an instance of a member-less annotation type. Members are answered with their + * default value, which the CDI qualifier rules guarantee to exist. + */ + private Annotation createAnnotation(final Class annotationType) { + final Map values = new LinkedHashMap<>(); + for (final Method method : annotationType.getDeclaredMethods()) { + values.put(method.getName(), method.getDefaultValue()); + } + + final InvocationHandler handler = (final Object proxy, final Method method, final Object[] args) -> { + final String name = method.getName(); + if ("annotationType".equals(name) && method.getParameterCount() == 0) { + return annotationType; + } + if ("equals".equals(name) && method.getParameterCount() == 1) { + return annotationType.isInstance(args[0]); + } + if ("hashCode".equals(name) && method.getParameterCount() == 0) { + return annotationType.hashCode(); + } + if ("toString".equals(name) && method.getParameterCount() == 0) { + return "@" + annotationType.getName() + "()"; + } + if (values.containsKey(name)) { + return values.get(name); + } + throw new IllegalStateException("Unsupported annotation method: " + method); + }; + + return Annotation.class.cast(Proxy.newProxyInstance( + annotationType.getClassLoader(), + new Class[]{annotationType}, + handler)); + } +} diff --git a/container/openejb-core/src/main/java/org/apache/openejb/config/AppInfoBuilder.java b/container/openejb-core/src/main/java/org/apache/openejb/config/AppInfoBuilder.java index 15b41ba69cc..9f73d2b8c00 100644 --- a/container/openejb-core/src/main/java/org/apache/openejb/config/AppInfoBuilder.java +++ b/container/openejb-core/src/main/java/org/apache/openejb/config/AppInfoBuilder.java @@ -691,6 +691,9 @@ private void buildPersistenceModules(final AppModule appModule, final AppInfo ap info.jtaDataSource = persistenceUnit.getJtaDataSource(); info.nonJtaDataSource = persistenceUnit.getNonJtaDataSource(); + info.qualifiers.addAll(persistenceUnit.getQualifier()); + info.scope = persistenceUnit.getScope(); + info.jarFiles.addAll(persistenceUnit.getJarFile()); info.classes.addAll(persistenceUnit.getClazz()); info.mappingFiles.addAll(persistenceUnit.getMappingFile()); @@ -704,6 +707,21 @@ private void buildPersistenceModules(final AppModule appModule, final AppInfo ap PersistenceProviderProperties.apply(appModule, info); + // Jakarta Persistence 3.2: these properties override the corresponding XML elements + final String qualifiersProperty = info.properties.getProperty("jakarta.persistence.qualifiers"); + if (qualifiersProperty != null) { + info.qualifiers.clear(); + for (final String qualifier : qualifiersProperty.split(",")) { + if (!qualifier.isBlank()) { + info.qualifiers.add(qualifier.trim()); + } + } + } + final String scopeProperty = info.properties.getProperty("jakarta.persistence.scope"); + if (scopeProperty != null) { + info.scope = scopeProperty; + } + // Persistence Unit Root Url appInfo.persistenceUnits.add(info); diff --git a/container/openejb-core/src/test/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactoryCloseTest.java b/container/openejb-core/src/test/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactoryCloseTest.java new file mode 100644 index 00000000000..fbb91195a9d --- /dev/null +++ b/container/openejb-core/src/test/java/org/apache/openejb/assembler/classic/ReloadableEntityManagerFactoryCloseTest.java @@ -0,0 +1,101 @@ +/* + * 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.assembler.classic; + +import jakarta.persistence.EntityManagerFactory; +import org.apache.openejb.persistence.PersistenceUnitInfoImpl; +import org.apache.openejb.util.reflection.Reflections; +import org.junit.Test; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; + +/** + * An application may close a container-managed {@code EntityManagerFactory} itself. Undeploy + * must then not call {@code close()} on it a second time, which the providers reject with + * "Attempting to execute an operation on a closed EntityManagerFactory" and which made + * {@code Assembler.destroyApplication} report an undeploy error. + * + * @see TOMEE-4650 + */ +public class ReloadableEntityManagerFactoryCloseTest { + + @Test + public void closeIsNotPropagatedToAnAlreadyClosedDelegate() { + final AtomicInteger closeCalls = new AtomicInteger(); + final EntityManagerFactory delegate = closeCountingEntityManagerFactory(closeCalls); + + final ReloadableEntityManagerFactory remf = newReloadableEntityManagerFactory(delegate); + + // the application closes the EMF itself + remf.close(); + assertEquals(1, closeCalls.get()); + + // undeploy closes it again, which must be a no-op + remf.close(); + assertEquals("close() must not be propagated to an already closed EntityManagerFactory", + 1, closeCalls.get()); + } + + private ReloadableEntityManagerFactory newReloadableEntityManagerFactory(final EntityManagerFactory delegate) { + final PersistenceUnitInfoImpl unitInfo = new PersistenceUnitInfoImpl(); + unitInfo.setPersistenceUnitName("close-test-unit"); + unitInfo.setProperties(new Properties()); + // keep the constructor from eagerly building the real EMF + unitInfo.setLazilyInitialized(true); + + final EntityManagerFactoryCallable callable = new EntityManagerFactoryCallable( + "org.apache.openjpa.persistence.PersistenceProviderImpl", unitInfo, + getClass().getClassLoader(), null, false); + + final ReloadableEntityManagerFactory remf = + new ReloadableEntityManagerFactory(getClass().getClassLoader(), callable, unitInfo); + Reflections.set(remf, "delegate", delegate); + return remf; + } + + /** + * A minimal {@code EntityManagerFactory} counting {@code close()} calls and reporting + * itself as closed afterwards, as the providers do. + */ + private EntityManagerFactory closeCountingEntityManagerFactory(final AtomicInteger closeCalls) { + final boolean[] open = {true}; + + final InvocationHandler handler = (final Object proxy, final Method method, final Object[] args) -> { + switch (method.getName()) { + case "close": + closeCalls.incrementAndGet(); + open[0] = false; + return null; + case "isOpen": + return open[0]; + default: + return null; + } + }; + + return EntityManagerFactory.class.cast(Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[]{EntityManagerFactory.class}, + handler)); + } +} diff --git a/container/openejb-core/src/test/java/org/apache/openejb/cdi/persistence/JpaCDIExtensionPerson.java b/container/openejb-core/src/test/java/org/apache/openejb/cdi/persistence/JpaCDIExtensionPerson.java new file mode 100644 index 00000000000..51cff5f3ce1 --- /dev/null +++ b/container/openejb-core/src/test/java/org/apache/openejb/cdi/persistence/JpaCDIExtensionPerson.java @@ -0,0 +1,48 @@ +/* + * 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.cdi.persistence; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * Entity of the persistence units used by {@link JpaCDIExtensionTest}. + */ +@Entity +public class JpaCDIExtensionPerson { + + @Id + private int id; + + private String name; + + public int getId() { + return id; + } + + public void setId(final int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } +} diff --git a/container/openejb-core/src/test/java/org/apache/openejb/cdi/persistence/JpaCDIExtensionTest.java b/container/openejb-core/src/test/java/org/apache/openejb/cdi/persistence/JpaCDIExtensionTest.java new file mode 100644 index 00000000000..7ccdc4ea9e8 --- /dev/null +++ b/container/openejb-core/src/test/java/org/apache/openejb/cdi/persistence/JpaCDIExtensionTest.java @@ -0,0 +1,151 @@ +/* + * 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.cdi.persistence; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.inject.Qualifier; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceUnitUtil; +import org.apache.openejb.jee.jpa.unit.Persistence; +import org.apache.openejb.jee.jpa.unit.PersistenceUnit; +import org.apache.openejb.junit.ApplicationComposer; +import org.apache.openejb.testing.Configuration; +import org.apache.openejb.testing.Module; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +/** + * Verifies the Jakarta Persistence 3.2 CDI integration: for each persistence unit the + * container registers an {@code EntityManagerFactory} and the five utility beans, carrying + * the qualifiers declared by the {@code } elements of persistence.xml, or + * {@code @Default} when none is declared. + */ +@RunWith(ApplicationComposer.class) +public class JpaCDIExtensionTest { + + @Inject + private QualifiedBean qualifiedBean; + + @Inject + private DefaultBean defaultBean; + + @Configuration + public Properties config() { + final Properties p = new Properties(); + p.setProperty("JpaCDIExtensionTestDb", "new://Resource?type=DataSource"); + p.setProperty("JpaCDIExtensionTestDb.JdbcDriver", "org.hsqldb.jdbcDriver"); + p.setProperty("JpaCDIExtensionTestDb.JdbcUrl", "jdbc:hsqldb:mem:jpa-cdi-extension"); + return p; + } + + @Module + public Persistence persistence() { + final PersistenceUnit qualified = new PersistenceUnit("qualified-unit"); + qualified.addClass(JpaCDIExtensionPerson.class); + qualified.setExcludeUnlistedClasses(true); + qualified.getQualifier().add(PersonUnit.class.getName()); + qualified.setJtaDataSource("JpaCDIExtensionTestDb"); + qualified.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)"); + qualified.getProperties().setProperty("openjpa.RuntimeUnenhancedClasses", "supported"); + + final PersistenceUnit unqualified = new PersistenceUnit("default-unit"); + unqualified.addClass(JpaCDIExtensionPerson.class); + unqualified.setExcludeUnlistedClasses(true); + unqualified.setJtaDataSource("JpaCDIExtensionTestDb"); + unqualified.setProperty("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)"); + unqualified.getProperties().setProperty("openjpa.RuntimeUnenhancedClasses", "supported"); + + final Persistence persistence = new Persistence(qualified); + persistence.getPersistenceUnit().add(unqualified); + persistence.setVersion("3.2"); + return persistence; + } + + @Module + public Class[] beans() { + return new Class[]{QualifiedBean.class, DefaultBean.class}; + } + + @Test + public void qualifiedEntityManagerFactoryIsInjectable() { + assertNotNull("EntityManagerFactory should be injectable via its persistence.xml qualifier", + qualifiedBean.getEntityManagerFactory()); + } + + @Test + public void qualifierSelectsTheMatchingPersistenceUnit() { + // the two units are distinct beans, so the qualifier must not resolve to the default one + assertNotSame(qualifiedBean.getEntityManagerFactory(), defaultBean.getEntityManagerFactory()); + } + + @Test + public void persistenceUnitUtilIsInjectableWithTheQualifier() { + assertNotNull("PersistenceUnitUtil should be injectable", qualifiedBean.getPersistenceUnitUtil()); + } + + @Test + public void unqualifiedPersistenceUnitIsInjectableWithDefaultQualifier() { + assertNotNull("A unit without should be injectable with @Default", + defaultBean.getEntityManagerFactory()); + } + + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE}) + public @interface PersonUnit { + } + + @ApplicationScoped + public static class QualifiedBean { + @Inject + @PersonUnit + private EntityManagerFactory entityManagerFactory; + + @Inject + @PersonUnit + private PersistenceUnitUtil persistenceUnitUtil; + + public EntityManagerFactory getEntityManagerFactory() { + return entityManagerFactory; + } + + public PersistenceUnitUtil getPersistenceUnitUtil() { + return persistenceUnitUtil; + } + + } + + @ApplicationScoped + public static class DefaultBean { + @Inject + private EntityManagerFactory entityManagerFactory; + + public EntityManagerFactory getEntityManagerFactory() { + return entityManagerFactory; + } + } +} diff --git a/container/openejb-jee/src/main/java/org/apache/openejb/jee/jpa/unit/PersistenceUnit.java b/container/openejb-jee/src/main/java/org/apache/openejb/jee/jpa/unit/PersistenceUnit.java index 0af278cab2e..447cfb4d632 100644 --- a/container/openejb-jee/src/main/java/org/apache/openejb/jee/jpa/unit/PersistenceUnit.java +++ b/container/openejb-jee/src/main/java/org/apache/openejb/jee/jpa/unit/PersistenceUnit.java @@ -82,6 +82,8 @@ @XmlType(name = "", propOrder = { "description", "provider", + "qualifier", + "scope", "jtaDataSource", "nonJtaDataSource", "mappingFile", @@ -99,6 +101,10 @@ public class PersistenceUnit { protected String description; protected String provider; + @XmlElement(name = "qualifier") + protected List qualifier; + @XmlElement(name = "scope") + protected String scope; @XmlElement(name = "jta-data-source") protected String jtaDataSource; @XmlElement(name = "non-jta-data-source") @@ -174,6 +180,21 @@ public void setProvider(final Class value) { setProvider(value == null ? null : value.getName()); } + public List getQualifier() { + if (qualifier == null) { + qualifier = new ArrayList<>(); + } + return this.qualifier; + } + + public String getScope() { + return scope; + } + + public void setScope(final String value) { + this.scope = value; + } + public String getJtaDataSource() { return jtaDataSource; }