|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | + * contributor license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright ownership. |
| 5 | + * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | + * (the "License"); you may not use this file except in compliance with |
| 7 | + * the License. You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | +package org.apache.openejb.cdi.persistence; |
| 18 | + |
| 19 | +import jakarta.enterprise.context.ApplicationScoped; |
| 20 | +import jakarta.enterprise.context.Dependent; |
| 21 | +import jakarta.enterprise.event.Observes; |
| 22 | +import jakarta.enterprise.inject.Any; |
| 23 | +import jakarta.enterprise.inject.Default; |
| 24 | +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; |
| 25 | +import jakarta.enterprise.inject.spi.Extension; |
| 26 | +import jakarta.inject.Qualifier; |
| 27 | +import jakarta.persistence.Cache; |
| 28 | +import jakarta.persistence.EntityManager; |
| 29 | +import jakarta.persistence.EntityManagerFactory; |
| 30 | +import jakarta.persistence.PersistenceUnitUtil; |
| 31 | +import jakarta.persistence.SchemaManager; |
| 32 | +import jakarta.persistence.criteria.CriteriaBuilder; |
| 33 | +import jakarta.persistence.metamodel.Metamodel; |
| 34 | +import org.apache.openejb.assembler.classic.AppInfo; |
| 35 | +import org.apache.openejb.assembler.classic.PersistenceUnitInfo; |
| 36 | +import org.apache.openejb.cdi.OpenEJBLifecycle; |
| 37 | +import org.apache.openejb.loader.SystemInstance; |
| 38 | +import org.apache.openejb.spi.ContainerSystem; |
| 39 | +import org.apache.openejb.util.LogCategory; |
| 40 | +import org.apache.openejb.util.Logger; |
| 41 | + |
| 42 | +import javax.naming.NamingException; |
| 43 | +import java.lang.annotation.Annotation; |
| 44 | +import java.lang.reflect.InvocationHandler; |
| 45 | +import java.lang.reflect.Method; |
| 46 | +import java.lang.reflect.Proxy; |
| 47 | +import java.util.LinkedHashMap; |
| 48 | +import java.util.LinkedHashSet; |
| 49 | +import java.util.Map; |
| 50 | +import java.util.Set; |
| 51 | +import java.util.function.Function; |
| 52 | + |
| 53 | +/** |
| 54 | + * CDI extension registering the beans required by the Jakarta Persistence 3.2 / |
| 55 | + * Jakarta EE 11 CDI integration (Platform specification, "Jakarta Persistence & |
| 56 | + * Jakarta Context and Dependency Injection (CDI) Integration"). |
| 57 | + * |
| 58 | + * <p>For each persistence unit the container must make available: |
| 59 | + * <ul> |
| 60 | + * <li>an {@link EntityManagerFactory} bean, {@code @ApplicationScoped}, whose bean name |
| 61 | + * is the persistence unit name;</li> |
| 62 | + * <li>an {@link EntityManager} bean, in the scope given by the {@code <scope>} element |
| 63 | + * (defaulting to {@code jakarta.transaction.TransactionScoped});</li> |
| 64 | + * <li>{@link CriteriaBuilder}, {@link PersistenceUnitUtil}, {@link Cache}, |
| 65 | + * {@link SchemaManager} and {@link Metamodel} beans, {@code @Dependent}, each simply |
| 66 | + * obtained from the matching getter of the {@code EntityManagerFactory}.</li> |
| 67 | + * </ul> |
| 68 | + * |
| 69 | + * <p>All of them carry the qualifiers given by the {@code <qualifier>} elements of |
| 70 | + * {@code persistence.xml}, or {@code @Default} when none is declared. |
| 71 | + */ |
| 72 | +public class JpaCDIExtension implements Extension { |
| 73 | + |
| 74 | + private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB.createChild("cdi"), JpaCDIExtension.class); |
| 75 | + |
| 76 | + private static final String PERSISTENCE_UNIT_NAMING_CONTEXT = "openejb/PersistenceUnit/"; |
| 77 | + |
| 78 | + /** |
| 79 | + * Default scope of the {@code EntityManager} bean. Resolved reflectively so that the |
| 80 | + * extension keeps working on distributions without the Jakarta Transactions API. |
| 81 | + */ |
| 82 | + private static final String TRANSACTION_SCOPED = "jakarta.transaction.TransactionScoped"; |
| 83 | + |
| 84 | + void registerBeans(@Observes final AfterBeanDiscovery afterBeanDiscovery) { |
| 85 | + final AppInfo appInfo = OpenEJBLifecycle.CURRENT_APP_INFO.get(); |
| 86 | + if (appInfo == null) { |
| 87 | + return; |
| 88 | + } |
| 89 | + |
| 90 | + for (final PersistenceUnitInfo unitInfo : appInfo.persistenceUnits) { |
| 91 | + final Set<Annotation> qualifiers = validateAndCreateQualifiers(unitInfo, afterBeanDiscovery); |
| 92 | + if (qualifiers == null) { |
| 93 | + continue; |
| 94 | + } |
| 95 | + |
| 96 | + final Class<? extends Annotation> entityManagerScope = resolveEntityManagerScope(unitInfo, afterBeanDiscovery); |
| 97 | + if (entityManagerScope == null) { |
| 98 | + continue; |
| 99 | + } |
| 100 | + |
| 101 | + logger.debug("Registering CDI beans for persistence unit '" + unitInfo.name + "'"); |
| 102 | + |
| 103 | + afterBeanDiscovery.addBean() |
| 104 | + .id("tomee.jpa." + EntityManagerFactory.class.getName() + "#" + unitInfo.id) |
| 105 | + .beanClass(EntityManagerFactory.class) |
| 106 | + .types(Object.class, EntityManagerFactory.class) |
| 107 | + .qualifiers(qualifiers.toArray(new Annotation[0])) |
| 108 | + .scope(ApplicationScoped.class) |
| 109 | + .name(unitInfo.name) |
| 110 | + .createWith(cc -> lookupEntityManagerFactory(unitInfo.id)); |
| 111 | + |
| 112 | + // the EntityManager is created per contextual instance and closed when the context ends |
| 113 | + afterBeanDiscovery.addBean() |
| 114 | + .id("tomee.jpa." + EntityManager.class.getName() + "#" + unitInfo.id) |
| 115 | + .beanClass(EntityManager.class) |
| 116 | + .types(Object.class, EntityManager.class) |
| 117 | + .qualifiers(qualifiers.toArray(new Annotation[0])) |
| 118 | + .scope(entityManagerScope) |
| 119 | + .produceWith(instance -> lookupEntityManagerFactory(unitInfo.id).createEntityManager()) |
| 120 | + .disposeWith((em, cc) -> { |
| 121 | + if (em.isOpen()) { |
| 122 | + em.close(); |
| 123 | + } |
| 124 | + }); |
| 125 | + |
| 126 | + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, CriteriaBuilder.class, EntityManagerFactory::getCriteriaBuilder); |
| 127 | + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, PersistenceUnitUtil.class, EntityManagerFactory::getPersistenceUnitUtil); |
| 128 | + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, Cache.class, EntityManagerFactory::getCache); |
| 129 | + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, SchemaManager.class, EntityManagerFactory::getSchemaManager); |
| 130 | + addUtilityBean(afterBeanDiscovery, unitInfo, qualifiers, Metamodel.class, EntityManagerFactory::getMetamodel); |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + /** |
| 135 | + * The five utility beans are {@code @Dependent} and simply delegate to the matching |
| 136 | + * getter of the {@code EntityManagerFactory}. |
| 137 | + */ |
| 138 | + private <T> void addUtilityBean(final AfterBeanDiscovery afterBeanDiscovery, |
| 139 | + final PersistenceUnitInfo unitInfo, |
| 140 | + final Set<Annotation> qualifiers, |
| 141 | + final Class<T> type, |
| 142 | + final Function<EntityManagerFactory, T> accessor) { |
| 143 | + afterBeanDiscovery.addBean() |
| 144 | + .id("tomee.jpa." + type.getName() + "#" + unitInfo.id) |
| 145 | + .beanClass(type) |
| 146 | + .types(Object.class, type) |
| 147 | + .qualifiers(qualifiers.toArray(new Annotation[0])) |
| 148 | + .scope(Dependent.class) |
| 149 | + .createWith(cc -> accessor.apply(lookupEntityManagerFactory(unitInfo.id))); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * Builds the qualifier set of a persistence unit: the {@code <qualifier>} elements, or |
| 154 | + * {@code @Default} when none is declared. {@code @Any} is always added, as for any bean. |
| 155 | + * |
| 156 | + * @return {@code null} if a qualifier is invalid, in which case a definition error has |
| 157 | + * been reported |
| 158 | + */ |
| 159 | + private Set<Annotation> validateAndCreateQualifiers(final PersistenceUnitInfo unitInfo, |
| 160 | + final AfterBeanDiscovery afterBeanDiscovery) { |
| 161 | + final Set<Annotation> qualifiers = new LinkedHashSet<>(); |
| 162 | + qualifiers.add(Any.Literal.INSTANCE); |
| 163 | + |
| 164 | + if (unitInfo.qualifiers.isEmpty()) { |
| 165 | + qualifiers.add(Default.Literal.INSTANCE); |
| 166 | + return qualifiers; |
| 167 | + } |
| 168 | + |
| 169 | + final ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| 170 | + for (final String qualifierName : unitInfo.qualifiers) { |
| 171 | + final Class<?> qualifierClass; |
| 172 | + try { |
| 173 | + qualifierClass = loader.loadClass(qualifierName); |
| 174 | + } catch (final ClassNotFoundException e) { |
| 175 | + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Qualifier class " + qualifierName |
| 176 | + + " of persistence unit " + unitInfo.name + " cannot be loaded", e)); |
| 177 | + return null; |
| 178 | + } |
| 179 | + |
| 180 | + if (!qualifierClass.isAnnotation()) { |
| 181 | + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Qualifier " + qualifierName |
| 182 | + + " of persistence unit " + unitInfo.name + " must be an annotation type")); |
| 183 | + return null; |
| 184 | + } |
| 185 | + |
| 186 | + @SuppressWarnings("unchecked") |
| 187 | + final Class<? extends Annotation> annotationClass = (Class<? extends Annotation>) qualifierClass; |
| 188 | + if (!annotationClass.isAnnotationPresent(Qualifier.class)) { |
| 189 | + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Qualifier " + qualifierName |
| 190 | + + " of persistence unit " + unitInfo.name + " must be annotated with @Qualifier")); |
| 191 | + return null; |
| 192 | + } |
| 193 | + |
| 194 | + qualifiers.add(createAnnotation(annotationClass)); |
| 195 | + } |
| 196 | + |
| 197 | + return qualifiers; |
| 198 | + } |
| 199 | + |
| 200 | + /** |
| 201 | + * Resolves the {@code <scope>} element of the persistence unit, defaulting to |
| 202 | + * {@code jakarta.transaction.TransactionScoped}. |
| 203 | + * |
| 204 | + * @return {@code null} if the scope is invalid, in which case a definition error has |
| 205 | + * been reported |
| 206 | + */ |
| 207 | + @SuppressWarnings("unchecked") |
| 208 | + private Class<? extends Annotation> resolveEntityManagerScope(final PersistenceUnitInfo unitInfo, |
| 209 | + final AfterBeanDiscovery afterBeanDiscovery) { |
| 210 | + final String scope = unitInfo.scope == null || unitInfo.scope.isBlank() ? TRANSACTION_SCOPED : unitInfo.scope.trim(); |
| 211 | + final ClassLoader loader = Thread.currentThread().getContextClassLoader(); |
| 212 | + |
| 213 | + final Class<?> scopeClass; |
| 214 | + try { |
| 215 | + scopeClass = loader.loadClass(scope); |
| 216 | + } catch (final ClassNotFoundException e) { |
| 217 | + if (unitInfo.scope == null || unitInfo.scope.isBlank()) { |
| 218 | + // no Jakarta Transactions on the classpath, fall back to @Dependent |
| 219 | + return Dependent.class; |
| 220 | + } |
| 221 | + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Scope class " + scope |
| 222 | + + " of persistence unit " + unitInfo.name + " cannot be loaded", e)); |
| 223 | + return null; |
| 224 | + } |
| 225 | + |
| 226 | + if (!scopeClass.isAnnotation()) { |
| 227 | + afterBeanDiscovery.addDefinitionError(new IllegalArgumentException("Scope " + scope |
| 228 | + + " of persistence unit " + unitInfo.name + " must be an annotation type")); |
| 229 | + return null; |
| 230 | + } |
| 231 | + |
| 232 | + return (Class<? extends Annotation>) scopeClass; |
| 233 | + } |
| 234 | + |
| 235 | + private EntityManagerFactory lookupEntityManagerFactory(final String unitId) { |
| 236 | + final ContainerSystem containerSystem = SystemInstance.get().getComponent(ContainerSystem.class); |
| 237 | + if (containerSystem == null) { |
| 238 | + throw new IllegalStateException("ContainerSystem is not available"); |
| 239 | + } |
| 240 | + |
| 241 | + final Object instance; |
| 242 | + try { |
| 243 | + instance = containerSystem.getJNDIContext().lookup(PERSISTENCE_UNIT_NAMING_CONTEXT + unitId); |
| 244 | + } catch (final NamingException e) { |
| 245 | + throw new IllegalStateException("Unable to lookup persistence unit " + unitId, e); |
| 246 | + } |
| 247 | + |
| 248 | + if (!(instance instanceof EntityManagerFactory)) { |
| 249 | + throw new IllegalStateException("Persistence unit " + unitId + " is not an EntityManagerFactory, found " |
| 250 | + + (instance == null ? "null" : instance.getClass().getName())); |
| 251 | + } |
| 252 | + return EntityManagerFactory.class.cast(instance); |
| 253 | + } |
| 254 | + |
| 255 | + /** |
| 256 | + * Creates an instance of a member-less annotation type. Members are answered with their |
| 257 | + * default value, which the CDI qualifier rules guarantee to exist. |
| 258 | + */ |
| 259 | + private Annotation createAnnotation(final Class<? extends Annotation> annotationType) { |
| 260 | + final Map<String, Object> values = new LinkedHashMap<>(); |
| 261 | + for (final Method method : annotationType.getDeclaredMethods()) { |
| 262 | + values.put(method.getName(), method.getDefaultValue()); |
| 263 | + } |
| 264 | + |
| 265 | + final InvocationHandler handler = (final Object proxy, final Method method, final Object[] args) -> { |
| 266 | + final String name = method.getName(); |
| 267 | + if ("annotationType".equals(name) && method.getParameterCount() == 0) { |
| 268 | + return annotationType; |
| 269 | + } |
| 270 | + if ("equals".equals(name) && method.getParameterCount() == 1) { |
| 271 | + return annotationType.isInstance(args[0]); |
| 272 | + } |
| 273 | + if ("hashCode".equals(name) && method.getParameterCount() == 0) { |
| 274 | + return annotationType.hashCode(); |
| 275 | + } |
| 276 | + if ("toString".equals(name) && method.getParameterCount() == 0) { |
| 277 | + return "@" + annotationType.getName() + "()"; |
| 278 | + } |
| 279 | + if (values.containsKey(name)) { |
| 280 | + return values.get(name); |
| 281 | + } |
| 282 | + throw new IllegalStateException("Unsupported annotation method: " + method); |
| 283 | + }; |
| 284 | + |
| 285 | + return Annotation.class.cast(Proxy.newProxyInstance( |
| 286 | + annotationType.getClassLoader(), |
| 287 | + new Class<?>[]{annotationType}, |
| 288 | + handler)); |
| 289 | + } |
| 290 | +} |
0 commit comments