From fefb889d7f4b6b4a27f351c2589f6b14601c5524 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Wed, 1 May 2019 15:16:47 -0700 Subject: [PATCH] JTA/JPA CDI integrations Addresses #496. Signed-off-by: Laird Nelson --- .gitignore | 2 + bom/pom.xml | 25 + .../src/main/docs/extensions/01_overview.adoc | 7 + docs/src/main/docs/extensions/05_cdi_jta.adoc | 84 ++ examples/integrations/cdi/jpa/pom.xml | 245 ++++ .../integrations/cdi/jpa/Greeting.java | 104 ++ .../cdi/jpa/HelloWorldApplication.java | 59 + .../cdi/jpa/HelloWorldResource.java | 175 +++ .../cdi/jpa/JPAExceptionMapper.java | 67 + .../integrations/cdi/jpa/package-info.java | 21 + .../jpa/src/main/resources/META-INF/beans.xml | 25 + .../META-INF/microprofile-config.properties | 19 + .../main/resources/META-INF/persistence.xml | 52 + examples/integrations/cdi/pom.xml | 1 + .../HikariCPBackedDataSourceExtension.java | 12 +- .../hikaricp/cdi/config/package-info.java | 14 +- .../datasource/hikaricp/cdi/package-info.java | 9 +- integrations/cdi/eclipselink-cdi/README.adoc | 21 + integrations/cdi/eclipselink-cdi/pom.xml | 94 ++ .../cdi/eclipselink/CDISEPlatform.java | 336 +++++ .../cdi/eclipselink/package-info.java | 24 + integrations/cdi/jpa-cdi/README.adoc | 30 + integrations/cdi/jpa-cdi/pom.xml | 159 +++ .../BeanManagerBackedDataSourceProvider.java | 154 +++ .../integrations/cdi/jpa/JpaExtension.java | 803 ++++++++++++ .../cdi/jpa/PersistenceUnitInfoBean.java | 871 +++++++++++++ .../integrations/cdi/jpa/package-info.java | 27 + .../integrations/cdi/jpa/jaxb/package.html | 30 + .../src/main/resources/META-INF/beans.xml | 25 + .../javax.enterprise.inject.spi.Extension | 16 + .../src/main/resources/messages.properties | 15 + integrations/cdi/jpa-weld/README.adoc | 30 + integrations/cdi/jpa-weld/pom.xml | 162 +++ .../cdi/jpa/weld/TransactionObserver.java | 143 +++ .../jpa/weld/WeldJpaInjectionServices.java | 1121 +++++++++++++++++ .../WeldJpaInjectionServicesExtension.java | 119 ++ .../cdi/jpa/weld/package-info.java | 26 + .../src/main/resources/META-INF/beans.xml | 30 + .../javax.enterprise.inject.spi.Extension | 16 + .../org.jboss.weld.bootstrap.api.Service | 16 + .../src/main/resources/messages.properties | 25 + .../cdi/jpa/weld/TestIntegration.java | 119 ++ .../jpa-weld/src/test/java/logging.properties | 24 + .../test/resources/META-INF/persistence.xml | 48 + integrations/cdi/jta-cdi/README.adoc | 23 + integrations/cdi/jta-cdi/pom.xml | 109 ++ .../jta/cdi/DelegatingTransactionManager.java | 272 ++++ ...ingTransactionSynchronizationRegistry.java | 306 +++++ .../jta/cdi/NarayanaExtension.java | 234 ++++ .../jta/cdi/NarayanaTransactionManager.java | 245 ++++ ...anaTransactionSynchronizationRegistry.java | 74 ++ .../integrations/jta/cdi/package-info.java | 25 + .../src/main/resources/META-INF/beans.xml | 26 + .../javax.enterprise.inject.spi.Extension | 16 + .../resources/default-jbossts-properties.xml | 153 +++ .../src/main/resources/messages.properties | 19 + .../TestTransactionalAnnotationSupport.java | 99 ++ integrations/cdi/jta-weld/README.adoc | 23 + integrations/cdi/jta-weld/pom.xml | 104 ++ .../jta/weld/NarayanaTransactionServices.java | 284 +++++ .../integrations/jta/weld/package-info.java | 26 + .../org.jboss.weld.bootstrap.api.Service | 16 + .../src/main/resources/messages.properties | 16 + ...TestAutomaticUserTransactionInjection.java | 93 ++ integrations/cdi/pom.xml | 5 + javadocs/pom.xml | 19 + pom.xml | 104 +- 67 files changed, 7680 insertions(+), 16 deletions(-) create mode 100644 docs/src/main/docs/extensions/05_cdi_jta.adoc create mode 100644 examples/integrations/cdi/jpa/pom.xml create mode 100644 examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java create mode 100644 examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java create mode 100644 examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java create mode 100644 examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java create mode 100644 examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java create mode 100644 examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml create mode 100644 examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties create mode 100644 examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml create mode 100644 integrations/cdi/eclipselink-cdi/README.adoc create mode 100644 integrations/cdi/eclipselink-cdi/pom.xml create mode 100644 integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java create mode 100644 integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java create mode 100644 integrations/cdi/jpa-cdi/README.adoc create mode 100644 integrations/cdi/jpa-cdi/pom.xml create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java create mode 100644 integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html create mode 100644 integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml create mode 100644 integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension create mode 100644 integrations/cdi/jpa-cdi/src/main/resources/messages.properties create mode 100644 integrations/cdi/jpa-weld/README.adoc create mode 100644 integrations/cdi/jpa-weld/pom.xml create mode 100644 integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java create mode 100644 integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java create mode 100644 integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java create mode 100644 integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java create mode 100644 integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml create mode 100644 integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension create mode 100644 integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service create mode 100644 integrations/cdi/jpa-weld/src/main/resources/messages.properties create mode 100644 integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java create mode 100644 integrations/cdi/jpa-weld/src/test/java/logging.properties create mode 100644 integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml create mode 100644 integrations/cdi/jta-cdi/README.adoc create mode 100644 integrations/cdi/jta-cdi/pom.xml create mode 100644 integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java create mode 100644 integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java create mode 100644 integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java create mode 100644 integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java create mode 100644 integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java create mode 100644 integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java create mode 100644 integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml create mode 100644 integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension create mode 100644 integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml create mode 100644 integrations/cdi/jta-cdi/src/main/resources/messages.properties create mode 100644 integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java create mode 100644 integrations/cdi/jta-weld/README.adoc create mode 100644 integrations/cdi/jta-weld/pom.xml create mode 100644 integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java create mode 100644 integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java create mode 100644 integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service create mode 100644 integrations/cdi/jta-weld/src/main/resources/messages.properties create mode 100644 integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java diff --git a/.gitignore b/.gitignore index bb39c50bea0..000016a72ef 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,5 @@ node/ # Other *~ user.txt +ObjectStore/ +PutObjectStoreDirHere/ diff --git a/bom/pom.xml b/bom/pom.xml index 4fa274383c3..e58aae0fee9 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -403,11 +403,36 @@ helidon-integrations-cdi-datasource-hikaricp ${project.version} + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + ${project.version} + io.helidon.integrations.cdi helidon-integrations-cdi-jedis ${project.version} + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa-weld + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + ${project.version} + io.helidon.integrations.cdi helidon-integrations-cdi-oci-objectstorage diff --git a/docs/src/main/docs/extensions/01_overview.adoc b/docs/src/main/docs/extensions/01_overview.adoc index 618f70fb7b0..e2330e1e562 100644 --- a/docs/src/main/docs/extensions/01_overview.adoc +++ b/docs/src/main/docs/extensions/01_overview.adoc @@ -51,4 +51,11 @@ Create and inject a Jedis pool in your application code. Create and inject an Oracle Cloud Infrastructure Object Storage client in your application code. -- + +[CARD] +.Java Transaction API objects +[link=extensions/05_cdi_jta.adoc] +-- +Use the Java Transaction API in your application code. +-- ==== diff --git a/docs/src/main/docs/extensions/05_cdi_jta.adoc b/docs/src/main/docs/extensions/05_cdi_jta.adoc new file mode 100644 index 00000000000..ea04e913efd --- /dev/null +++ b/docs/src/main/docs/extensions/05_cdi_jta.adoc @@ -0,0 +1,84 @@ +/////////////////////////////////////////////////////////////////////////////// + + Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + + 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. + +/////////////////////////////////////////////////////////////////////////////// + += CDI extension for JTA +:description: Helidon CDI extension for JTA +:keywords: helidon, java, microservices, microprofile, extensions, cdi, jta + +This https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#spi[CDI +portable extension] provides support for JTA (Java Transaction API) +transactions in your Helidon MicroProfile applications. + +== Prerequsites + +Declare the following dependency fragment in your project's `pom.xml`: + +[source,xml] +---- + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + runtime + + + + javax.transaction + javax.transaction-api + provided + +---- + +== Declaring a method to be transactional + +The following example shows how to declare a transactional method. + +[source,java] +.Transactional method declaration +---- +@Transactional(Transactional.TxType.REQUIRED) +public void doSomethingTransactionally() { + +} +---- + +The extension ensures that a transaction is started before and +committed after the method executes. If the method throws an +exception, the transaction will be rolled back. + +You can further specify the transactional behavior of the extension by +using different instances of the `Transactional` annotation. For more +information, see the +https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/Transactional.html[`Transactional` +annotation documentation]. + +Transactional method support is implemented by CDI interception +facilities. Among other things, this means that the method to which +you apply the `Transactional` annotation must not be `private` and +must in all other ways be a _business method_. See the +https://jcp.org/aboutJava/communityprocess/mrel/jsr318/index3.html[Java +Interceptors specification] for more details. + +During a transactional method invocation, the extension makes the +following objects available for injection via the `Inject` annotation: + +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/UserTransaction.html[`UserTransaction`] +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/Transaction.html[`Transaction`] +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/UserTransactionManager.html[`TransactionManager`] +* https://static.javadoc.io/javax.transaction/javax.transaction-api/1.2/javax/transaction/UserTransactionSynchronizationRegistry.html[`TransactionSynchronizationRegistry`] + + diff --git a/examples/integrations/cdi/jpa/pom.xml b/examples/integrations/cdi/jpa/pom.xml new file mode 100644 index 00000000000..b6672207117 --- /dev/null +++ b/examples/integrations/cdi/jpa/pom.xml @@ -0,0 +1,245 @@ + + + + 4.0.0 + + io.helidon.examples.integrations.cdi + helidon-examples-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-examples-jpa + Helidon CDI Extensions Examples JPA + + + libs + + + + + + src/main/resources + true + + + + + com.ethlo.persistence.tools + eclipselink-maven-plugin + 2.7.1.1 + + + javax.annotation + javax.annotation-api + ${version.lib.annotation-api} + + + javax.xml.bind + jaxb-api + ${version.lib.jaxb-api} + + + + + weave + process-classes + + weave + + + + modelgen + generate-sources + + modelgen + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + + + + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${project.build.directory}/${dependenciesDirectory} + false + false + true + true + runtime + test + + + + + + maven-jar-plugin + + + + true + ${dependenciesDirectory} + io.helidon.microprofile.server.Main + + + + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + + + com.h2database + h2 + runtime + + + org.jboss.weld.se + weld-se-core + runtime + + + org.jboss.spec.javax.el + jboss-el-api_3.0_spec + + + org.jboss.spec.javax.interceptor + jboss-interceptors-api_1.2_spec + + + + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + ${project.version} + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + ${project.version} + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-datasource-hikaricp + ${project.version} + runtime + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jpa-weld + ${project.version} + runtime + + + org.jboss + jandex + runtime + + + io.helidon.microprofile.server + helidon-microprofile-server + ${project.version} + runtime + + + org.glassfish.hk2.external + javax.inject + + + + + io.helidon.microprofile.config + helidon-microprofile-config-cdi + ${project.version} + runtime + + + org.eclipse.microprofile.config + microprofile-config-api + runtime + + + + + jakarta.persistence + jakarta.persistence-api + provided + + + javax.transaction + javax.transaction-api + provided + + + + + javax.annotation + javax.annotation-api + compile + + + javax.enterprise + cdi-api + compile + + + javax.inject + javax.inject + compile + + + javax.ws.rs + javax.ws.rs-api + compile + + + diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java new file mode 100644 index 00000000000..50999617f87 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/Greeting.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.integrations.cdi.jpa; + +import java.util.Objects; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * A contrived representation for example purposes only of a two-part + * greeting as might be stored in a database. + */ +@Access(AccessType.FIELD) +@Entity(name = "Greeting") +@Table(name = "GREETING") +public class Greeting { + + @Id + @Column(name = "FIRSTPART", insertable = true, nullable = false, updatable = false) + private String firstPart; + + @Basic(optional = false) + @Column(name = "SECONDPART", insertable = true, nullable = false, updatable = true) + private String secondPart; + + /** + * Creates a new {@link Greeting}; required by the JPA + * specification and for no other purpose. + * + * @deprecated Please use the {@link #Greeting(String, + * String)} constructor instead. + * + * @see #Greeting(String, String) + */ + @Deprecated + protected Greeting() { + super(); + } + + /** + * Creates a new {@link Greeting}. + * + * @param firstPart the first part of the greeting; must not be + * {@code null} + * + * @param secondPart the second part of the greeting; must not be + * {@code null} + * + * @exception NullPointerException if {@code firstPart} or {@code + * secondPart} is {@code null} + */ + public Greeting(final String firstPart, final String secondPart) { + super(); + this.firstPart = Objects.requireNonNull(firstPart); + this.secondPart = Objects.requireNonNull(secondPart); + } + + /** + * Sets the second part of this greeting. + * + * @param secondPart the second part of this greeting; must not be + * {@code null} + * + * @exception NullPointerException if {@code secondPart} is {@code + * null} + */ + public void setSecondPart(final String secondPart) { + this.secondPart = Objects.requireNonNull(secondPart); + } + + /** + * Returns a {@link String} representation of the second part of + * this {@link Greeting}. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link String} representation of the + * second part of this {@link Greeting} + */ + @Override + public String toString() { + return this.secondPart; + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java new file mode 100644 index 00000000000..d7f9c1cc9d9 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldApplication.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.integrations.cdi.jpa; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.core.Application; + +/** + * An example {@link Application} demonstrating the modular + * integration of JPA and JTA with Helidon MicroProfile. + */ +@ApplicationScoped +public class HelloWorldApplication extends Application { + + private final Set> classes; + + /** + * Creates a new {@link HelloWorldApplication}. + */ + public HelloWorldApplication() { + super(); + final Set> classes = new HashSet<>(); + classes.add(HelloWorldResource.class); + classes.add(JPAExceptionMapper.class); + this.classes = Collections.unmodifiableSet(classes); + } + + /** + * Returns a non-{@code null} {@link Set} of {@link Class}es that + * comprise this JAX-RS application. + * + * @return a non-{@code null}, {@linkplain + * Collections#unmodifiableSet(Set) unmodifiable Set} + * + * @see HelloWorldResource + */ + @Override + public Set> getClasses() { + return this.classes; + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java new file mode 100644 index 00000000000..7965af9161e --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/HelloWorldResource.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.integrations.cdi.jpa; + +import java.net.URI; +import java.util.Objects; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityNotFoundException; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceException; // for javadoc only +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.Transactional; +import javax.transaction.Transactional.TxType; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * A JAX-RS root resource class that manipulates greetings in a + * database. + * + * @see #get(String) + * + * @see #post(String, String) + */ +@Path("") +@RequestScoped +public class HelloWorldResource { + + /** + * The {@link EntityManager} used by this class. + * + *

Note that it behaves as though there is a transaction manager + * in effect, because there is.

+ */ + @PersistenceContext(unitName = "test") + private EntityManager entityManager; + + /** + * A {@link Transaction} that is guaranteed to be non-{@code null} + * only when a transactional method is executing. + * + * @see #post(String, String) + */ + @Inject + private Transaction transaction; + + /** + * Creates a new {@link HelloWorldResource}. + */ + public HelloWorldResource() { + super(); + } + + /** + * Returns a {@link Response} with a status of {@code 404} when + * invoked. + * + * @return a non-{@code null} {@link Response} + */ + @GET + @Path("favicon.ico") + public Response getFavicon() { + return Response.status(404).build(); + } + + /** + * When handed a {@link String} like, say, "{@code hello}", responds + * with the second part of the composite greeting as found via an + * {@link EntityManager}. + * + * @param firstPart the first part of the greeting; must not be + * {@code null} + * + * @return the second part of the greeting; never {@code null} + * + * @exception NullPointerException if {@code firstPart} was {@code + * null} + * + * @exception PersistenceException if the {@link EntityManager} + * encountered an error + */ + @GET + @Path("{firstPart}") + @Produces(MediaType.TEXT_PLAIN) + public String get(@PathParam("firstPart") final String firstPart) { + Objects.requireNonNull(firstPart); + assert this.entityManager != null; + final Greeting greeting = this.entityManager.find(Greeting.class, firstPart); + assert greeting != null; + return greeting.toString(); + } + + /** + * When handed two parts of a greeting, like, say, "{@code hello}" + * and "{@code world}", stores a new {@link Greeting} entity in the + * database appropriately. + * + * @param firstPart the first part of the greeting; must not be + * {@code null} + * + * @param secondPart the second part of the greeting; must not be + * {@code null} + * + * @return the {@link String} representation of the resulting {@link + * Greeting}'s identifier; never {@code null} + * + * @exception NullPointerException if {@code firstPart} or {@code + * secondPart} was {@code null} + * + * @exception PersistenceException if the {@link EntityManager} + * encountered an error + * + * @exception SystemException if something went wrong with the + * transaction + */ + @POST + @Path("{firstPart}") + @Consumes(MediaType.TEXT_PLAIN) + @Produces(MediaType.TEXT_PLAIN) + @Transactional(TxType.REQUIRED) + public Response post(@PathParam("firstPart") final String firstPart, + final String secondPart) + throws SystemException { + Objects.requireNonNull(firstPart); + Objects.requireNonNull(secondPart); + assert this.transaction != null; + assert this.transaction.getStatus() == Status.STATUS_ACTIVE; + assert this.entityManager != null; + assert this.entityManager.isJoinedToTransaction(); + Greeting greeting = null; + // See https://tools.ietf.org/html/rfc7231#section-4.3.3; we + // track whether JPA does an insert or an update. + boolean created = false; + try { + greeting = this.entityManager.getReference(Greeting.class, firstPart); + assert greeting != null; + greeting.setSecondPart(secondPart); + } catch (final EntityNotFoundException entityNotFoundException) { + greeting = new Greeting(firstPart, secondPart); + this.entityManager.persist(greeting); + created = true; + } + assert this.entityManager.contains(greeting); + if (created) { + return Response.created(URI.create(firstPart)).build(); + } else { + return Response.ok(firstPart).build(); + } + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java new file mode 100644 index 00000000000..51c1fb720ee --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/JPAExceptionMapper.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.examples.integrations.cdi.jpa; + +import javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityNotFoundException; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * An {@link ExceptionMapper} that handles {@link + * PersistenceException}s. + * + * @see ExceptionMapper + */ +@ApplicationScoped +@Provider +public class JPAExceptionMapper implements ExceptionMapper { + + /** + * Creates a new {@link JPAExceptionMapper}. + */ + public JPAExceptionMapper() { + super(); + } + + /** + * Returns an appropriate non-{@code null} {@link Response} for the + * supplied {@link PersistenceException}. + * + * @param persistenceException the {@link PersistenceException} that + * caused this {@link JPAExceptionMapper} to be invoked; may be + * {@code null} + * + * @return a non-{@code null} {@link Response} representing the + * error + */ + @Override + public Response toResponse(final PersistenceException persistenceException) { + final Response returnValue; + if (persistenceException instanceof NoResultException + || persistenceException instanceof EntityNotFoundException) { + returnValue = Response.status(404).build(); + } else { + returnValue = null; + throw persistenceException; + } + return returnValue; + } + +} diff --git a/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java new file mode 100644 index 00000000000..fe314923757 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/java/io/helidon/examples/integrations/cdi/jpa/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides classes and interfaces demonstrating the usage of JPA and + * JTA integration within Helidon MicroProfile. + */ +package io.helidon.examples.integrations.cdi.jpa; diff --git a/examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml b/examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..89f9c163080 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties b/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties new file mode 100644 index 00000000000..09f0967ec67 --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/microprofile-config.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +javax.sql.DataSource.test.dataSourceClassName=org.h2.jdbcx.JdbcDataSource +javax.sql.DataSource.test.dataSource.url=jdbc:h2:mem:test;INIT=CREATE TABLE GREETING (FIRSTPART VARCHAR NOT NULL, SECONDPART VARCHAR NOT NULL, PRIMARY KEY (FIRSTPART))\\;INSERT INTO GREETING (FIRSTPART, SECONDPART) VALUES ('hello', 'world') +javax.sql.DataSource.test.username=sa +javax.sql.DataSource.test.password= diff --git a/examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml b/examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000000..108d28d641e --- /dev/null +++ b/examples/integrations/cdi/jpa/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,52 @@ + + + + + test + io.helidon.examples.integrations.cdi.jpa.Greeting + + + + + + + + + + + + + + + + + + + diff --git a/examples/integrations/cdi/pom.xml b/examples/integrations/cdi/pom.xml index 3e066f938a3..b4e32749f3e 100644 --- a/examples/integrations/cdi/pom.xml +++ b/examples/integrations/cdi/pom.xml @@ -35,6 +35,7 @@ datasource-hikaricp datasource-hikaricp-mysql jedis + jpa oci-objectstorage diff --git a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java index 923e35b7711..fba2c465b96 100644 --- a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java +++ b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/HikariCPBackedDataSourceExtension.java @@ -239,7 +239,7 @@ private static Entry toProperties(final DataSourceDefinition assert name != null; final String value = propertyString.substring(equalsIndex + 1); assert value != null; - properties.setProperty("dataSource." + name, value); + properties.setProperty("dataSource." + name.trim(), value.trim()); } } @@ -327,34 +327,34 @@ private static Entry toProperties(final DataSourceDefinition final String databaseName = dsd.databaseName(); assert databaseName != null; if (!databaseName.isEmpty()) { - properties.setProperty("datasource.databaseName", databaseName); + properties.setProperty("dataSource.databaseName", databaseName); } // description -> dataSource.description (standard DataSource property) final String description = dsd.description(); assert description != null; if (!description.isEmpty()) { - properties.setProperty("datasource.description", description); + properties.setProperty("dataSource.description", description); } // portNumber -> dataSource.portNumber (standard DataSource property) final int portNumber = dsd.portNumber(); if (portNumber >= 0) { - properties.setProperty("datasource.portNumber", String.valueOf(portNumber)); + properties.setProperty("dataSource.portNumber", String.valueOf(portNumber)); } // serverName -> dataSource.serverName (standard DataSource property) final String serverName = dsd.serverName(); assert serverName != null; if (!serverName.isEmpty()) { - properties.setProperty("datasource.serverName", serverName); + properties.setProperty("dataSource.serverName", serverName); } // url -> dataSource.url (standard DataSource property) final String url = dsd.url(); assert url != null; if (!url.isEmpty()) { - properties.setProperty("datasource.url", url); + properties.setProperty("dataSource.url", url); } return returnValue; diff --git a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java index e8ca5483138..f394401b458 100644 --- a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java +++ b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/config/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,15 @@ */ /** - * Provides classes and interfaces marrying MicroProfile Config - * constructs and {@link + * Provides classes and interfaces marrying MicroProfile Config constructs and {@link * io.helidon.service.configuration.api.ServiceConfiguration} - * constructs for the Hikari Connection Pool. + * constructs for the Hikari Connection Pool. + * + * @see + * io.helidon.integrations.datasource.hikaricp.cdi.config.HikariCP */ package io.helidon.integrations.datasource.hikaricp.cdi.config; diff --git a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java index f20f96ab4f9..5b744133f25 100644 --- a/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java +++ b/integrations/cdi/datasource-hikaricp/src/main/java/io/helidon/integrations/datasource/hikaricp/cdi/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2019 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,10 @@ /** * CDI integration for the Hikari - * connection pool. + * href="https://github.com/brettwooldridge/HikariCP/blob/HikariCP-2.7.8/README.md#-hikaricpits-fasterhikari-hikal%C4%93-origin-japanese-light-ray" + * target="_parent">Hikari connection pool. + * + * @see + * io.helidon.integrations.datasource.hikaricp.cdi.HikariCPBackedDataSourceExtension */ package io.helidon.integrations.datasource.hikaricp.cdi; diff --git a/integrations/cdi/eclipselink-cdi/README.adoc b/integrations/cdi/eclipselink-cdi/README.adoc new file mode 100644 index 00000000000..c5162e4fa9a --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/README.adoc @@ -0,0 +1,21 @@ += Helidon Eclipselink CDI Integration + +The Helidon Eclipselink Integration project contains a +https://www.eclipse.org/eclipselink/api/2.7/org/eclipse/persistence/platform/server/ServerPlatform.html[`ServerPlatform`] +implementation for use with Helidon's link:../jpa-cdi[JPA CDI integration]. + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + += Usage + +Ensure that this library is on your application's runtime classpath, +and that you are actually using Eclipselink as your JPA provider. +Then ensure that the following vendor-specific property is present in +your `META-INF/persistence.xml` resource: + +[source,xml] +---- + +---- + diff --git a/integrations/cdi/eclipselink-cdi/pom.xml b/integrations/cdi/eclipselink-cdi/pom.xml new file mode 100644 index 00000000000..ae5d6e6363a --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/pom.xml @@ -0,0 +1,94 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-eclipselink + Helidon CDI Integrations Eclipselink + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.slf4j + slf4j-simple + test + + + + + org.jboss + jandex + runtime + + + + + javax.enterprise + cdi-api + provided + + + javax.transaction + javax.transaction-api + provided + + + + + org.eclipse.persistence + org.eclipse.persistence.jpa + compile + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + diff --git a/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java new file mode 100644 index 00000000000..3d6d06f152e --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/CDISEPlatform.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.eclipselink; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; +import java.util.concurrent.Executor; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.util.AnnotationLiteral; +import javax.inject.Qualifier; +import javax.management.MBeanServer; +import javax.transaction.TransactionManager; + +import org.eclipse.persistence.platform.server.JMXServerPlatformBase; +import org.eclipse.persistence.platform.server.ServerPlatformBase; // for javadoc only +import org.eclipse.persistence.sessions.DatabaseSession; +import org.eclipse.persistence.sessions.JNDIConnector; +import org.eclipse.persistence.transaction.JTATransactionController; + +/** + * A {@link JMXServerPlatformBase} that arranges things such that CDI, + * not JNDI, will be used to acquire a {@link TransactionManager} and + * {@link MBeanServer}. + * + *

Most users will not use this class directly, but will supply its + * fully-qualified name as the value of the {@code + * eclipselink.target-server} Eclipselink JPA extension property + * in a {@code + * META-INF/persistence.xml} file.

+ * + *

For example:

+ * + *
<property name="eclipselink.target-server"
+ *          value="io.helidon.integrations.cdi.eclipselink.CDISEPlatform"/>
+ * + * @see #getExternalTransactionControllerClass() + */ +public class CDISEPlatform extends JMXServerPlatformBase { + + + /* + * Constructors. + */ + + + /** + * Creates a {@link CDISEPlatform}. + * + * @param session the {@link DatabaseSession} this platform will + * wrap; must not be {@code null} + * + * @see JMXServerPlatformBase#JMXServerPlatformBase(DatabaseSession) + */ + public CDISEPlatform(final DatabaseSession session) { + super(session); + } + + + /* + * Instance methods. + */ + + + /** + * Sets the name of the platform. + * + *

The format of the platform name is subject to change without + * notice.

+ * + * @see #getServerNameAndVersion() + */ + @Override + protected void initializeServerNameAndVersion() { + this.serverNameAndVersion = this.getClass().getSimpleName(); + } + + /** + * Uses CDI to find a relevant {@link MBeanServer}, caches it, and + * returns it. + * + *

This method may return {@code null}.

+ * + *

Overrides of this method may return {@code null}.

+ * + *

If there is no such {@link MBeanServer} then the {@link + * MBeanServer} found and cached by the {@linkplain + * JMXServerPlatformBase#getMBeanServer() superclass + * implementation of this method} is returned instead.

+ * + * @return an {@link MBeanServer}, or {@code null} + */ + @Override + public MBeanServer getMBeanServer() { + if (this.mBeanServer == null) { + final CDI cdi = CDI.current(); + if (cdi != null) { + Instance instance = cdi.select(MBeanServer.class, Eclipselink.Literal.INSTANCE); + assert instance != null; + if (instance.isUnsatisfied()) { + instance = cdi.select(MBeanServer.class); + } + if (!instance.isUnsatisfied()) { + final MBeanServer mBeanServer = instance.get(); + assert mBeanServer != null; + this.mBeanServer = mBeanServer; + } + } + } + return super.getMBeanServer(); + } + + /** + * Uses CDI to find a relevant {@link Executor} whose {@link + * Executor#execute(Runnable)} method will be used to submit the + * supplied {@link Runnable}. + * + *

If there is no such {@link Executor}, then the {@linkplain + * JMXServerPlatformBase#launchContainerRunnable(Runnable) + * superclass implementation of this method} is used instead.

+ * + * @param runnable the {@link Runnable} to launch; should not be + * {@code null} + * + * @see JMXServerPlatformBase#launchContainerRunnable(Runnable) + */ + @Override + public void launchContainerRunnable(final Runnable runnable) { + if (runnable == null) { + super.launchContainerRunnable(null); + } else { + final CDI cdi = CDI.current(); + if (cdi == null) { + super.launchContainerRunnable(runnable); + } else { + Instance executorInstance = cdi.select(Executor.class, Eclipselink.Literal.INSTANCE); + assert executorInstance != null; + if (executorInstance.isUnsatisfied()) { + executorInstance = cdi.select(Executor.class); + } + assert executorInstance != null; + final Executor executor; + if (executorInstance.isUnsatisfied()) { + executor = null; + } else { + executor = executorInstance.get(); + } + if (executor != null) { + executor.execute(runnable); + } else { + super.launchContainerRunnable(runnable); + } + } + } + } + + /** + * Overrides the {@link + * ServerPlatformBase#initializeExternalTransactionController()} + * method to {@linkplain #disableJTA() disable JTA} if there is no + * {@link TransactionManager} bean present in CDI before invoking + * the {@linkplain + * ServerPlatformBase#initializeExternalTransactionController() + * superclass implementation}. + * + * @see ServerPlatformBase#initializeExternalTransactionController() + */ + @Override + public void initializeExternalTransactionController() { + final CDI cdi = CDI.current(); + if (cdi == null || cdi.select(TransactionManager.class).isUnsatisfied()) { + this.disableJTA(); + } + super.initializeExternalTransactionController(); + } + + /** + * Returns a non-{@code null} {@link Class} that extends {@link + * org.eclipse.persistence.transaction.AbstractTransactionController}, + * namely {@link TransactionController}. + * + * @return a non-{@code null} {@link Class} that extends {@link + * org.eclipse.persistence.transaction.AbstractTransactionController} + * + * @see + * org.eclipse.persistence.transaction.AbstractTransactionController + * + * @see TransactionController + */ + @Override + public Class getExternalTransactionControllerClass() { + if (this.externalTransactionControllerClass == null) { + this.externalTransactionControllerClass = TransactionController.class; + } + return this.externalTransactionControllerClass; + } + + /** + * Returns {@link JNDIConnector#UNDEFINED_LOOKUP} when invoked. + * + * @return {@link JNDIConnector#UNDEFINED_LOOKUP} + */ + @Override + public final int getJNDIConnectorLookupType() { + return JNDIConnector.UNDEFINED_LOOKUP; + } + + + /* + * Inner and nested classes. + */ + + + /** + * A {@link JTATransactionController} whose {@link + * #acquireTransactionManager()} method uses CDI, not JNDI, to + * return a {@link TransactionManager} instance. + * + * @see #acquireTransactionManager() + * + * @see JTATransactionController + * + * @see CDISEPlatform#getExternalTransactionControllerClass() + */ + public static class TransactionController extends JTATransactionController { + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link TransactionController}. + */ + public TransactionController() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Returns a non-{@code null} {@link TransactionManager}. + * + *

This method never returns {@code null}.

+ * + * @return a non-{@code null} {@link TransactionManager} + * + * @exception NullPointerException if in exceedingly rare + * specification-violating cases the return value of {@link + * CDI#current()} is {@code null}, or if the {@link + * Instance#get()} method returns {@code null} + * + * @exception RuntimeException if the {@link Instance#get()} + * method encounters an error providing a {@link + * TransactionManager} + * + * @see JTATransactionController#acquireTransactionManager() + */ + @Override + protected TransactionManager acquireTransactionManager() { + return Objects.requireNonNull(CDI.current().select(TransactionManager.class).get()); + } + + } + + /** + * A {@link Qualifier} used to designate various things as being + * related to Eclipselink in some way. + * + *

The typical end user will apply this annotation to an + * implementation of {@link Executor} if she wants that particular + * {@link Executor} used by the {@link + * CDISEPlatform#launchContainerRunnable(Runnable)} method.

+ * + *

The {@link Eclipselink} qualifier may also be used to + * annotate an implementation of {@link MBeanServer} for use by + * the {@link CDISEPlatform#getMBeanServer()} method.

+ * + * @see CDISEPlatform#launchContainerRunnable(Runnable) + * + * @see CDISEPlatform#getMBeanServer() + */ + @Documented + @Qualifier + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.TYPE }) + public @interface Eclipselink { + + /** + * An {@link AnnotationLiteral} that implements {@link + * Eclipselink}. + */ + class Literal extends AnnotationLiteral implements Eclipselink { + + /** + * The single instance of the {@link Literal} class. + */ + public static final Eclipselink INSTANCE = new Literal(); + + /** + * The version of this class for Java serialization + * purposes. + */ + private static final long serialVersionUID = 1L; + + } + + } + +} diff --git a/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java new file mode 100644 index 00000000000..44f3ad8774c --- /dev/null +++ b/integrations/cdi/eclipselink-cdi/src/main/java/io/helidon/integrations/cdi/eclipselink/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides classes and interfaces for working with Eclipselink in CDI. + * + * @see io.helidon.integrations.cdi.eclipselink.CDISEPlatform + */ +package io.helidon.integrations.cdi.eclipselink; diff --git a/integrations/cdi/jpa-cdi/README.adoc b/integrations/cdi/jpa-cdi/README.adoc new file mode 100644 index 00000000000..e0c3dc8face --- /dev/null +++ b/integrations/cdi/jpa-cdi/README.adoc @@ -0,0 +1,30 @@ += Helidon JPA CDI Integration + +The Helidon JPA CDI Integration project performs the +provider-independent work of integrating JPA into standalone CDI +applications (including those based on Helidon MicroProfile). It is +one of several projects that together make up overall JPA support for +standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-specific counterpart, such as the `jpa-weld` project + found elsewhere in this git repository +* a JPA provider implementation, such as Eclipselink +* a JPA-provider-specific library to assist the JPA provider in + determining what kind of environment it is running in, such as the + `eclipselink-cdi` project found elsewhere in this git repository +* a library capable of integrating `DataSource`s into CDI, such as the + `datasource-hikaricp` project found elsewhere in this git repository +* a suitable JDBC-compliant database driver library + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JPA CDI Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file. diff --git a/integrations/cdi/jpa-cdi/pom.xml b/integrations/cdi/jpa-cdi/pom.xml new file mode 100644 index 00000000000..d6c6d3cb3ea --- /dev/null +++ b/integrations/cdi/jpa-cdi/pom.xml @@ -0,0 +1,159 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jpa + Helidon CDI Integrations JPA + + + -syntax + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.slf4j + slf4j-simple + test + + + + + org.jboss + jandex + runtime + + + + + jakarta.persistence + jakarta.persistence-api + provided + + + javax.annotation + javax.annotation-api + provided + + + javax.enterprise + cdi-api + provided + + + javax.transaction + javax.transaction-api + provided + + + + + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + + + Generate persistence.xml Java objects + + generate + + + io.helidon.integrations.cdi.jpa.jaxb + true + + + + jakarta.persistence + jakarta.persistence-api + javax/persistence/persistence_2_2.xsd + + + + false + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + + + + xmlbind + + [9,) + false + + + + javax.xml.bind + jaxb-api + compile + + + javax.activation + javax.activation-api + runtime + + + com.sun.xml.bind + jaxb-core + runtime + + + com.sun.xml.bind + jaxb-impl + runtime + + + + + diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java new file mode 100644 index 00000000000..8d87466f5be --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/BeanManagerBackedDataSourceProvider.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; +import javax.sql.DataSource; + +/** + * A {@link PersistenceUnitInfoBean.DataSourceProvider} implementation + * that uses a {@link BeanManager} to look up relevant {@link + * DataSource}s. + * + * @see PersistenceUnitInfoBean.DataSourceProvider + */ +@ApplicationScoped +class BeanManagerBackedDataSourceProvider implements PersistenceUnitInfoBean.DataSourceProvider { + + + /* + * Instance fields. + */ + + + /** + * The {@link BeanManager} to use to look up relevant {@link + * DataSource}s. + * + *

This field may be {@code null} in which case the {@link + * #getDataSource(boolean, boolean, String)} method will throw an + * {@link IllegalStateException}.

+ * + * @see #BeanManagerBackedDataSourceProvider(BeanManager) + */ + private final BeanManager beanManager; + + + /* + * Constructors. + */ + + + /** + * Creates a new nonfunctional {@link + * BeanManagerBackedDataSourceProvider}. + * + *

This constructor exists only to conform to section + * 3.15 of the CDI specification and for no other purpose.

+ * + * @deprecated Please use the {@link + * #BeanManagerBackedDataSourceProvider(BeanManager)} constructor + * instead. + */ + @Deprecated + BeanManagerBackedDataSourceProvider() { + this(null); + } + + /** + * Creates a new {@link BeanManagerBackedDataSourceProvider}. + * + * @param beanManager the {@link BeanManager} to use; may be + * {@code null}, but shouldn't, and if so the {@link + * #getDataSource(boolean, boolean, String)} method will throw an + * {@link IllegalStateException} + */ + @Inject + BeanManagerBackedDataSourceProvider(final BeanManager beanManager) { + super(); + this.beanManager = beanManager; + } + + + /* + * Instance methods. + */ + + + /** + * Supplies a {@link DataSource} according to rules defined by the + * JPA specification and portions of the Java EE specification. + * + *

Implementations of this method are permitted to return + * {@code null}.

+ * + * @param jta if {@code true}, the {@link DataSource} that is + * returned may be enrolled in JTA-compliant transactions; this + * implementation ignores this parameter + * + * @param useDefaultJta if {@code true}, and if the {@code jta} + * parameter value is {@code true}, the supplied {@code + * dataSourceName} may be ignored and a default {@link DataSource} + * eligible for enrolling in JTA-compliant transactions will be + * returned if possible + * + * @param dataSourceName the name of the {@link DataSource} to + * return; may be {@code null} + * + * @return an appropriate {@link DataSource}, or {@code null} + * + * @see PersistenceUnitInfoBean#getJtaDataSource() + * + * @see PersistenceUnitInfoBean#getNonJtaDataSource() + * + * @exception IllegalStateException if this {@link + * BeanManagerBackedDataSourceProvider} was created with a {@code + * null} {@link BeanManager} + */ + @Override + public DataSource getDataSource(final boolean jta, + final boolean useDefaultJta, + final String dataSourceName) { + if (this.beanManager == null) { + throw new IllegalStateException("beanManager == null"); + } + final Bean bean; + if (dataSourceName == null) { + if (useDefaultJta) { + bean = this.beanManager.resolve(this.beanManager.getBeans(DataSource.class)); + } else { + bean = null; + } + } else { + bean = this.beanManager.resolve(this.beanManager.getBeans(DataSource.class, NamedLiteral.of(dataSourceName))); + } + final DataSource returnValue; + if (bean == null) { + returnValue = null; + } else { + returnValue = + (DataSource) this.beanManager.getReference(bean, + DataSource.class, + this.beanManager.createCreationalContext(bean)); + } + return returnValue; + } +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java new file mode 100644 index 00000000000..0382d7583ba --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaExtension.java @@ -0,0 +1,803 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.CreationException; +import javax.enterprise.inject.Vetoed; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.Extension; +import javax.enterprise.inject.spi.ProcessAnnotatedType; +import javax.enterprise.inject.spi.WithAnnotations; +import javax.inject.Singleton; +import javax.persistence.Converter; +import javax.persistence.Embeddable; +import javax.persistence.Entity; +import javax.persistence.MappedSuperclass; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceProperty; +import javax.persistence.PersistenceUnit; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceProviderResolver; +import javax.persistence.spi.PersistenceProviderResolverHolder; +import javax.persistence.spi.PersistenceUnitInfo; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import io.helidon.integrations.cdi.jpa.PersistenceUnitInfoBean.DataSourceProvider; +import io.helidon.integrations.cdi.jpa.jaxb.Persistence; + +import static javax.interceptor.Interceptor.Priority.LIBRARY_AFTER; + +/** + * A {@linkplain Extension portable extension} normally instantiated + * by the Java {@linkplain java.util.ServiceLoader service provider + * infrastructure} that integrates the provider-independent parts of + * JPA into CDI. + * + * @see PersistenceUnitInfoBean + */ +public class JpaExtension implements Extension { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} for use by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(JpaExtension.class.getName(), "messages"); + + + /* + * Instance fields. + */ + + + /** + * A {@link Map} of {@link PersistenceUnitInfoBean} instances that + * were created by the {@link + * #gatherImplicitPersistenceUnits(ProcessAnnotatedType, + * BeanManager)} observer method, indexed by the names of + * persistence units. + * + *

This field is never {@code null}.

+ * + *

The contents of this field are used only when no explicit + * {@link PersistenceUnitInfo} beans are otherwise available in + * the container.

+ * + * @see #gatherImplicitPersistenceUnits(ProcessAnnotatedType, BeanManager) + * + * @see #afterBeanDiscovery(AfterBeanDiscovery, BeanManager) + */ + private final Map implicitPersistenceUnits; + + /** + * A {@link Map} of {@link Set}s of {@link Class}es whose keys are + * persistence unit names and whose values are {@link Set}s of + * {@link Class}es discovered by CDI (and hence consist of + * unlisted classes in the sense that they might not be found in + * any {@link PersistenceUnitInfo}). + * + *

Such {@link Class}es, of course, might not have been weaved + * appropriately by the relevant {@link PersistenceProvider}.

+ * + *

This field is never {@code null}.

+ */ + private final Map>> unlistedManagedClassesByPersistenceUnitNames; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link JpaExtension}. + */ + public JpaExtension() { + super(); + this.unlistedManagedClassesByPersistenceUnitNames = new HashMap<>(); + this.implicitPersistenceUnits = new HashMap<>(); + } + + + /* + * Instance methods. + */ + + + /** + * Looks for type-level {@link PersistenceContext} annotations + * that have at least one {@link PersistenceProperty} annotation + * {@linkplain PersistenceContext#properties() associated with} + * them and uses them to define persistence units, potentially + * preventing the need for {@code META-INF/persistence.xml} + * processing. + * + * @param event the {@link ProcessAnnotatedType} event occurring; + * may be {@code null} in which case no action will be taken + * + * @param beanManager the {@link BeanManager} in effect; may be + * {@code null} in which case no action will be taken + * + * @see PersistenceContext + * + * @see PersistenceProperty + * + * @see PersistenceUnitInfoBean + */ + private void gatherImplicitPersistenceUnits(@Observes + @WithAnnotations({ + PersistenceContext.class // yes, PersistenceContext, not PersistenceUnit + }) + final ProcessAnnotatedType event, + final BeanManager beanManager) { + final String cn = JpaExtension.class.getName(); + final String mn = "gatherImplicitPersistenceUnits"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager}); + } + + if (event != null && beanManager != null) { + final AnnotatedType annotatedType = event.getAnnotatedType(); + if (annotatedType != null + && !annotatedType.isAnnotationPresent(Vetoed.class)) { + final Set persistenceContexts = + annotatedType.getAnnotations(PersistenceContext.class); + if (persistenceContexts != null && !persistenceContexts.isEmpty()) { + for (final PersistenceContext persistenceContext : persistenceContexts) { + assert persistenceContext != null; + final PersistenceProperty[] persistenceProperties = persistenceContext.properties(); + if (persistenceProperties != null && persistenceProperties.length > 0) { + final String persistenceUnitName = persistenceContext.unitName(); + assert persistenceUnitName != null; + PersistenceUnitInfoBean persistenceUnit = this.implicitPersistenceUnits.get(persistenceUnitName); + if (persistenceUnit == null) { + final String jtaDataSourceName; + if (persistenceUnitName.isEmpty()) { + jtaDataSourceName = null; + } else { + jtaDataSourceName = persistenceUnitName; + } + final Class javaClass = annotatedType.getJavaClass(); + assert javaClass != null; + URL persistenceUnitRoot = null; + final ProtectionDomain pd = javaClass.getProtectionDomain(); + if (pd != null) { + final CodeSource cs = pd.getCodeSource(); + if (cs != null) { + persistenceUnitRoot = cs.getLocation(); + } + } + final Properties properties = new Properties(); + for (final PersistenceProperty persistenceProperty : persistenceProperties) { + assert persistenceProperty != null; + final String persistencePropertyName = persistenceProperty.name(); + assert persistencePropertyName != null; + if (!persistencePropertyName.isEmpty()) { + properties.setProperty(persistencePropertyName, persistenceProperty.value()); + } + } + persistenceUnit = + new PersistenceUnitInfoBean(persistenceUnitName, + persistenceUnitRoot, + null, + new BeanManagerBackedDataSourceProvider(beanManager), + properties); + this.implicitPersistenceUnits.put(persistenceUnitName, persistenceUnit); + } + } + } + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Tracks {@linkplain Converter converters}, {@linkplain Entity + * entities}, {@linkplain Embeddable embeddables} and {@linkplain + * MappedSuperclass mapped superclasses} that were auto-discovered + * by CDI bean discovery, and makes sure that they are not + * actually CDI beans, since according to the JPA specification + * they cannot be. + * + *

This method also keeps track of these classes as potential + * "unlisted classes" to be used by a {@linkplain + * PersistenceUnitInfo persistence unit} if its {@linkplain + * PersistenceUnitInfo#excludeUnlistedClasses()} method returns + * {@code false}.

+ * + * @param event the event describing the {@link AnnotatedType} + * being processed; may be {@code null} in which case no action + * will be taken + * + * @see Converter + * + * @see Embeddable + * + * @see Entity + * + * @see MappedSuperclass + * + * @see PersistenceUnitInfo#excludeUnlistedClasses() + */ + private void discoverManagedClasses(@Observes + @WithAnnotations({ + Converter.class, + Embeddable.class, + Entity.class, + MappedSuperclass.class + }) + final ProcessAnnotatedType event) { + final String cn = JpaExtension.class.getName(); + final String mn = "discoverManagedClasses"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, event); + } + + if (event != null) { + final AnnotatedType annotatedType = event.getAnnotatedType(); + if (annotatedType != null && !annotatedType.isAnnotationPresent(Vetoed.class)) { + this.assignManagedClassToPersistenceUnit(annotatedType.getAnnotations(PersistenceContext.class), + annotatedType.getAnnotations(PersistenceUnit.class), + annotatedType.getJavaClass()); + event.veto(); // managed classes can't be beans + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Given {@link Set}s of {@link PersistenceContext} and {@link + * PersistenceUnit} annotations that will be used for their {@code + * unitName} elements only, associates the supplied {@link Class} + * with the persistence units implied by the annotations. + * + * @param persistenceContexts a {@link Set} of {@link + * PersistenceContext}s whose {@link PersistenceContext#unitName() + * unitName} elements identify persistence units; may be {@code + * null} or {@linkplain Collection#isEmpty() empty} + * + * @param persistenceUnits a {@link Set} of {@link + * PersistenceUnit}s whose {@link PersistenceUnit#unitName() + * unitName} elements identify persistence units; may be {@code + * null} or {@linkplain Collection#isEmpty() empty} + * + * @param c the {@link Class} to associate; may be {@code null} in + * which case no action will be taken + * + * @see PersistenceContext + * + * @see PersistenceUnit + */ + private void assignManagedClassToPersistenceUnit(final Set persistenceContexts, + final Set persistenceUnits, + final Class c) { + final String cn = JpaExtension.class.getName(); + final String mn = "assignManagedClassToPersistenceUnit"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {persistenceContexts, persistenceUnits, c}); + } + + if (c != null) { + boolean processed = false; + if (persistenceContexts != null && !persistenceContexts.isEmpty()) { + for (final PersistenceContext persistenceContext : persistenceContexts) { + if (persistenceContext != null) { + final String name = persistenceContext.unitName(); // yes, unitName + assert name != null; + if (!name.isEmpty()) { + processed = true; + addUnlistedManagedClass(name, c); + } + } + } + } + if (persistenceUnits != null && !persistenceUnits.isEmpty()) { + for (final PersistenceUnit persistenceUnit : persistenceUnits) { + if (persistenceUnit != null) { + final String name = persistenceUnit.unitName(); // yes, unitName + assert name != null; + if (!name.isEmpty()) { + processed = true; + addUnlistedManagedClass(name, c); + } + } + } + } + if (!processed) { + addUnlistedManagedClass("", c); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Given a {@link Class} and a name of a persistence unit, + * associates the {@link Class} with that persistence unit as a + * member of its list of governed classes. + * + * @param name the name of the persistence unit in question; may + * be {@code null} in which case the empty string ({@code ""}) + * will be used instead + * + * @param managedClass the {@link Class} to associate; may be + * {@code null} in which case no action will be taken + * + * @see PersistenceUnitInfo#getManagedClassNames() + */ + private void addUnlistedManagedClass(String name, final Class managedClass) { + final String cn = JpaExtension.class.getName(); + final String mn = "addUnlistedManagedClass"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {name, managedClass}); + } + + if (managedClass != null) { + if (name == null) { + name = ""; + } + if (!name.isEmpty()) { + Set> unlistedManagedClasses = this.unlistedManagedClassesByPersistenceUnitNames.get(name); + if (unlistedManagedClasses == null) { + unlistedManagedClasses = new HashSet<>(); + this.unlistedManagedClassesByPersistenceUnitNames.put(name, unlistedManagedClasses); + } + unlistedManagedClasses.add(managedClass); + } + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Converts {@code META-INF/persistence.xml} resources into {@link + * PersistenceUnitInfo} objects and takes into account any other + * {@link PersistenceUnitInfo} objects that already exist and + * ensures that all of them are registered as CDI beans. + * + *

This allows other CDI-provider-specific mechanisms to use + * these {@link PersistenceUnitInfo} beans as inputs for creating + * {@link EntityManager} instances.

+ * + * @param event the {@link AfterBeanDiscovery} event describing + * the fact that bean discovery has been performed; must not be + * {@code null} + * + * @param beanManager the {@link BeanManager} currently in effect; + * must not be {@code null} + * + * @exception IOException if an input or output error occurs, + * typically because a {@code META-INF/persistence.xml} resource + * was found but could not be loaded for some reason + * + * @exception JAXBException if there was a problem {@linkplain + * Unmarshaller#unmarshal(Reader) unmarshalling} a {@code + * META-INF/persistence.xml} resource + * + * @exception NullPointerException if either {@code event} or + * {@code beanManager} is {@code null} + * + * @exception ReflectiveOperationException if reflection failed + * + * @exception XMLStreamException if there was a problem setting up + * JAXB + * + * @see PersistenceUnitInfo + */ + private void addPersistenceUnitInfoBeans(@Observes @Priority(LIBRARY_AFTER) + final AfterBeanDiscovery event, + final BeanManager beanManager) + throws IOException, JAXBException, ReflectiveOperationException, XMLStreamException { + final String cn = JpaExtension.class.getName(); + final String mn = "addPersistenceUnitInfoBeans"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager}); + } + + Objects.requireNonNull(event); + Objects.requireNonNull(beanManager); + + final Collection providers = addPersistenceProviderBeans(event); + + // Should we consider type-level @PersistenceContext + // definitions of persistence units ("implicits")? + boolean processImplicits = true; + + // Collect all pre-existing PersistenceUnitInfo beans + // (i.e. supplied by the end user) and make sure their + // associated PersistenceProviders are beanified. (Almost + // always this Set will be empty.) + final Set> preexistingPersistenceUnitInfoBeans = + beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); + if (preexistingPersistenceUnitInfoBeans != null && !preexistingPersistenceUnitInfoBeans.isEmpty()) { + processImplicits = false; + maybeAddPersistenceProviderBeans(event, beanManager, preexistingPersistenceUnitInfoBeans, providers); + } + + // Next, and most commonly, load all META-INF/persistence.xml + // resources with JAXB, and turn them into PersistenceUnitInfo + // instances, and add beans for all of them as well as their + // associated PersistenceProviders (if applicable). + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + assert classLoader != null; + final Enumeration urls = classLoader.getResources("META-INF/persistence.xml"); + if (urls != null && urls.hasMoreElements()) { + processImplicits = false; + this.processPersistenceXmls(event, beanManager, classLoader, urls, providers); + } + + // If we did not find any PersistenceUnitInfo instances via + // any other means, only then look at those defined "implicitly", + // i.e. via type-level @PersistenceContext annotations. + if (processImplicits) { + this.processImplicitPersistenceUnits(event, providers); + } + + this.unlistedManagedClassesByPersistenceUnitNames.clear(); + this.implicitPersistenceUnits.clear(); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + private static Collection addPersistenceProviderBeans(final AfterBeanDiscovery event) { + final String cn = JpaExtension.class.getName(); + final String mn = "addPersistenceProviderBeans"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, event); + } + + Objects.requireNonNull(event); + final PersistenceProviderResolver resolver = PersistenceProviderResolverHolder.getPersistenceProviderResolver(); + event.addBean() + .types(PersistenceProviderResolver.class) + .scope(Singleton.class) + .createWith(cc -> resolver); + final Collection providers = resolver.getPersistenceProviders(); + assert providers != null; + for (final PersistenceProvider provider : providers) { + event.addBean() + .addTransitiveTypeClosure(provider.getClass()) + .scope(Singleton.class) + .createWith(cc -> provider); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, providers); + } + return providers; + } + + private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, + final Collection providers) + throws ReflectiveOperationException { + final String cn = JpaExtension.class.getName(); + final String mn = "processImplicitPersistenceUnits"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, providers}); + } + + Objects.requireNonNull(event); + for (final PersistenceUnitInfoBean persistenceUnitInfoBean : this.implicitPersistenceUnits.values()) { + assert persistenceUnitInfoBean != null; + final String persistenceUnitName = persistenceUnitInfoBean.getPersistenceUnitName(); + if (!persistenceUnitInfoBean.excludeUnlistedClasses()) { + final Collection> unlistedManagedClasses = + this.unlistedManagedClassesByPersistenceUnitNames.get(persistenceUnitName); + if (unlistedManagedClasses != null && !unlistedManagedClasses.isEmpty()) { + for (final Class unlistedManagedClass : unlistedManagedClasses) { + assert unlistedManagedClass != null; + persistenceUnitInfoBean.addManagedClassName(unlistedManagedClass.getName()); + } + } + } + event.addBean() + .types(Collections.singleton(PersistenceUnitInfo.class)) + .scope(Singleton.class) + .addQualifiers(NamedLiteral.of(persistenceUnitName == null ? "" : persistenceUnitName)) + .createWith(cc -> persistenceUnitInfoBean); + maybeAddPersistenceProviderBean(event, persistenceUnitInfoBean, providers); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + private void processPersistenceXmls(final AfterBeanDiscovery event, + final BeanManager beanManager, + final ClassLoader classLoader, + final Enumeration urls, + final Collection providers) + throws IOException, JAXBException, ReflectiveOperationException, XMLStreamException { + final String cn = JpaExtension.class.getName(); + final String mn = "processPersistenceXmls"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager, classLoader, urls, providers}); + } + + Objects.requireNonNull(event); + if (urls != null && urls.hasMoreElements()) { + final Supplier tempClassLoaderSupplier; + if (classLoader instanceof URLClassLoader) { + tempClassLoaderSupplier = () -> new URLClassLoader(((URLClassLoader) classLoader).getURLs()); + } else { + tempClassLoaderSupplier = () -> classLoader; + } + // We use StAX for XML loading because it is the same + // strategy used by CDI implementations. If the end user + // wants to customize the StAX implementation then we want + // that customization to apply here as well. + // + // Note that XMLInputFactory is NOT deprecated in JDK 8: + // https://docs.oracle.com/javase/8/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory-- + // ...but IS deprecated in JDK 9: + // https://docs.oracle.com/javase/9/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory-- + // ...with an incorrect claim that it was deprecated since + // JDK 1.7. In JDK 7 it actually was *not* deprecated: + // https://docs.oracle.com/javase/7/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory() + // ...and now in JDK 10 it is NO LONGER deprecated: + // https://docs.oracle.com/javase/10/docs/api/javax/xml/stream/XMLInputFactory.html#newFactory() + // ...nor in JDK 11: + // https://docs.oracle.com/en/java/javase/11/docs/api/java.xml/javax/xml/stream/XMLInputFactory.html#newFactory() + // ...nor in JDK 12: + // https://docs.oracle.com/en/java/javase/12/docs/api/java.xml/javax/xml/stream/XMLInputFactory.html#newFactory() + // So we suppress deprecation warnings since deprecation + // in JDK 9 appears to have been a mistake. + @SuppressWarnings("deprecation") + final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + assert xmlInputFactory != null; + final Unmarshaller unmarshaller = + JAXBContext.newInstance(Persistence.class.getPackage().getName()).createUnmarshaller(); + assert unmarshaller != null; + final DataSourceProvider dataSourceProvider = new BeanManagerBackedDataSourceProvider(beanManager); + while (urls.hasMoreElements()) { + final URL url = urls.nextElement(); + assert url != null; + Collection persistenceUnitInfos = null; + try (InputStream inputStream = new BufferedInputStream(url.openStream())) { + final XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(inputStream); + assert reader != null; + persistenceUnitInfos = + PersistenceUnitInfoBean.fromPersistence((Persistence) unmarshaller.unmarshal(reader), + classLoader, + tempClassLoaderSupplier, + new URL(url, ".."), // e.g. META-INF/.. + this.unlistedManagedClassesByPersistenceUnitNames, + dataSourceProvider); + } + if (persistenceUnitInfos != null && !persistenceUnitInfos.isEmpty()) { + for (final PersistenceUnitInfo persistenceUnitInfo : persistenceUnitInfos) { + final String persistenceUnitName = persistenceUnitInfo.getPersistenceUnitName(); + event.addBean() + .types(Collections.singleton(PersistenceUnitInfo.class)) + .scope(Singleton.class) + .addQualifiers(NamedLiteral.of(persistenceUnitName == null ? "" : persistenceUnitName)) + .createWith(cc -> persistenceUnitInfo); + maybeAddPersistenceProviderBean(event, persistenceUnitInfo, providers); + } + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + private static void maybeAddPersistenceProviderBeans(final AfterBeanDiscovery event, + final BeanManager beanManager, + final Set> preexistingPersistenceUnitInfoBeans, + final Collection providers) + throws ReflectiveOperationException { + final String cn = JpaExtension.class.getName(); + final String mn = "maybeAddPersistenceProviderBeans"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager, preexistingPersistenceUnitInfoBeans, providers}); + } + + Objects.requireNonNull(event); + Objects.requireNonNull(beanManager); + Objects.requireNonNull(preexistingPersistenceUnitInfoBeans); + for (final Bean bean : preexistingPersistenceUnitInfoBeans) { + if (bean != null) { + assert bean.getTypes().contains(PersistenceUnitInfo.class); + @SuppressWarnings("unchecked") + final Bean preexistingPersistenceUnitInfoBean = (Bean) bean; + // We use Contextual#create() directly to create a + // PersistenceUnitInfo contextual instance (normally + // for this use case in CDI you would acquire a + // contextual reference via + // BeanManager#getReference(), but it is too early in + // the (spec-defined) lifecycle to do that here). We + // also deliberately do not use + // Context#get(Contextual, CreationalContext), since + // that might "install" the instance so acquired in + // whatever Context/scope it is defined in and we just + // need it transiently. + // + // Getting a contextual instance this way, via + // Contextual#create(), is normally frowned upon, + // since it bypasses CDI's Context mechansims and + // proxying and interception features (it is the + // foundation upon which they are built), but here we + // need the instance only for the return values of + // getPersistenceProviderClassName() and + // getClassLoader(). We then destroy the instance + // immediately so that everything behaves as though + // this contextual instance acquired by shady means + // never existed. + final CreationalContext cc = + beanManager.createCreationalContext(preexistingPersistenceUnitInfoBean); + final PersistenceUnitInfo pui = preexistingPersistenceUnitInfoBean.create(cc); + assert pui != null; + try { + maybeAddPersistenceProviderBean(event, pui, providers); + } finally { + preexistingPersistenceUnitInfoBean.destroy(pui, cc); + // Contextual#destroy() *should* release the + // CreationalContext, but it is an idempotent call + // and many Bean authors forget to do this. + cc.release(); + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Given a {@link PersistenceUnitInfo} and a {@link Collection} of + * {@link PersistenceProvider} instances representing already + * "beanified" {@link PersistenceProvider}s, adds a CDI bean for + * the {@linkplain + * PersistenceUnitInfo#getPersistenceProviderClassName() + * persistence provider referenced by the supplied + * PersistenceUnitInfo} if appropriate. + * + * @param event the {@link AfterBeanDiscovery} event that will do + * the actual bean addition; must not be {@code null} + * + * @param persistenceUnitInfo the {@link PersistenceUnitInfo} + * whose {@linkplain + * PersistenceUnitInfo#getPersistenceProviderClassName() + * associated persistence provider} will be beanified; must not be + * {@code null} + * + * @param providers a {@link Collection} of {@link + * PersistenceProvider} instances that represent {@link + * PersistenceProvider}s that have already had beans added for + * them; may be {@code null} + * + * @exception NullPointerException if {@code event} or {@code + * persistenceUnitInfo} is {@code null} + * + * @exception ReflectiveOperationException if an error occurs + * during reflection + */ + private static void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, + final PersistenceUnitInfo persistenceUnitInfo, + final Collection providers) + throws ReflectiveOperationException { + final String cn = JpaExtension.class.getName(); + final String mn = "maybeAddPersistenceProviderBean"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, persistenceUnitInfo, providers}); + } + + Objects.requireNonNull(event); + Objects.requireNonNull(persistenceUnitInfo); + final String providerClassName = persistenceUnitInfo.getPersistenceProviderClassName(); + if (providerClassName != null) { + + boolean add = true; + + if (providers != null && !providers.isEmpty()) { + for (final PersistenceProvider provider : providers) { + if (provider != null && provider.getClass().getName().equals(providerClassName)) { + add = false; + break; + } + } + } + + if (add) { + // The PersistenceProvider class in question is not one we + // already loaded. Add a bean for it too. + + final String persistenceUnitName = persistenceUnitInfo.getPersistenceUnitName(); + event.addBean() + .types(PersistenceProvider.class) + .scope(Singleton.class) + .addQualifiers(NamedLiteral.of(persistenceUnitName == null ? "" : persistenceUnitName)) + .createWith(cc -> { + try { + ClassLoader classLoader = persistenceUnitInfo.getClassLoader(); + if (classLoader == null) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + assert classLoader != null; + @SuppressWarnings("unchecked") + final Class c = + (Class) Class.forName(providerClassName, true, classLoader); + return c.getDeclaredConstructor().newInstance(); + } catch (final ReflectiveOperationException reflectiveOperationException) { + throw new CreationException(reflectiveOperationException.getMessage(), + reflectiveOperationException); + } + }); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java new file mode 100644 index 00000000000..ebf8252b0ca --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceUnitInfoBean.java @@ -0,0 +1,871 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.persistence.SharedCacheMode; +import javax.persistence.ValidationMode; +import javax.persistence.spi.ClassTransformer; +import javax.persistence.spi.PersistenceProvider; // for javadoc only +import javax.persistence.spi.PersistenceUnitInfo; +import javax.persistence.spi.PersistenceUnitTransactionType; +import javax.sql.DataSource; + +import io.helidon.integrations.cdi.jpa.jaxb.Persistence; +import io.helidon.integrations.cdi.jpa.jaxb.Persistence.PersistenceUnit; +import io.helidon.integrations.cdi.jpa.jaxb.PersistenceUnitCachingType; +import io.helidon.integrations.cdi.jpa.jaxb.PersistenceUnitValidationModeType; + +/** + * A {@link PersistenceUnitInfo} implementation that can be + * constructed by hand. + * + * @see PersistenceUnitInfo + */ +public class PersistenceUnitInfoBean implements PersistenceUnitInfo { + + + /* + * Instance fields. + */ + + + private final ClassLoader classLoader; + + private final ClassLoader originalClassLoader; + + private final boolean excludeUnlistedClasses; + + private final List jarFileUrls; + + private final Set managedClassNames; + + private final List managedClassNamesView; + + private final List mappingFileNames; + + private final String jtaDataSourceName; + + private final String nonJtaDataSourceName; + + private final DataSourceProvider dataSourceProvider; + + private final String persistenceProviderClassName; + + private final String persistenceUnitName; + + private final URL persistenceUnitRootUrl; + + private final String persistenceXMLSchemaVersion; + + private final Properties properties; + + private final SharedCacheMode sharedCacheMode; + + private final Consumer classTransformerConsumer; + + private final Supplier tempClassLoaderSupplier; + + private final PersistenceUnitTransactionType transactionType; + + private final ValidationMode validationMode; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link PersistenceUnitInfoBean} using as many + * defaults as reasonably possible. + * + * @param persistenceUnitName the name of the persistence unit + * this {@link PersistenceUnitInfoBean} represents; must not be + * {@code null} + * + * @param persistenceUnitRootUrl the {@link URL} identifying the + * root of the persistence unit this {@link + * PersistenceUnitInfoBean} represents; must not be {@code null} + * + * @param managedClassNames a {@link Collection} of + * fully-qualified class names identifying JPA-managed classes + * (such as entity classes, mapped superclasses and the like); may + * be {@code null}. The {@link Collection} is copied and no + * reference to it is retained. + * + * @param dataSourceProvider a {@link DataSourceProvider} capable + * of supplying {@link DataSource} instances; must not be {@code + * null} + * + * @param properties a {@link Properties} object representing the + * properties of the persistence unit represented by this {@link + * PersistenceUnitInfoBean}; may be {@code null}. A reference is + * retained to this object. + * + * @exception NullPointerException if {@code persistenceUnitName}, + * {@code persistenceUnitRootUrl} or {@code dataSourceProvider} is + * {@code null} + * + * @see #PersistenceUnitInfoBean(String, URL, String, String, + * ClassLoader, Supplier, Consumer, boolean, Collection, + * Collection, Collection, String, String, DataSourceProvider, + * Properties, SharedCacheMode, PersistenceUnitTransactionType, + * ValidationMode) + */ + public PersistenceUnitInfoBean(final String persistenceUnitName, + final URL persistenceUnitRootUrl, + final Collection managedClassNames, + final DataSourceProvider dataSourceProvider, + final Properties properties) { + this(persistenceUnitName, + persistenceUnitRootUrl, + null, + null, + Thread.currentThread().getContextClassLoader(), + null, + null, + managedClassNames != null && !managedClassNames.isEmpty(), + null, + managedClassNames, + null, + persistenceUnitName, + null, + dataSourceProvider, + properties, + SharedCacheMode.UNSPECIFIED, + PersistenceUnitTransactionType.JTA, + ValidationMode.AUTO); + } + + /** + * Creates a new {@link PersistenceUnitInfoBean}. + * + * @param persistenceUnitName the name of the persistence unit + * this {@link PersistenceUnitInfoBean} represents; must not be + * {@code null} + * + * @param persistenceUnitRootUrl the {@link URL} identifying the + * root of the persistence unit this {@link + * PersistenceUnitInfoBean} represents; must not be {@code null} + * + * @param persistenceXMLSchemaVersion a {@link String} + * representation of the version of JPA being supported; may be + * {@code null} in which case "{@code 2.2}" will be used instead + * + * @param persistenceProviderClassName the fully-qualified class + * name of a {@link PersistenceProvider} implementation; may be + * {@code null} in which case a default will be used + * + * @param classLoader a {@link ClassLoader} to be returned by the + * {@link #getClassLoader()} method; may be {@code null} + * + * @param tempClassLoaderSupplier a {@link Supplier} of {@link + * ClassLoader} instances to be used by the {@link + * #getNewTempClassLoader()} method; may be {@code null} + * + * @param classTransformerConsumer a {@link Consumer} of any + * {@link ClassTransformer}s that may be added via a JPA + * provider's invocation of the {@link + * #addTransformer(ClassTransformer)} method; may be {@code null} + * in which case no action will be taken + * + * @param excludeUnlistedClasses if {@code true}, then any + * automatically discovered managed classes not explicitly + * contained in {@code managedClassNames} will be excluded from + * consideration + * + * @param jarFileUrls a {@link Collection} of {@link URL}s + * identifying {@code .jar} files containing managed classes; may + * be {@code null}. The {@link Collection} is copied and no + * reference to it is retained. + * + * @param managedClassNames a {@link Collection} of + * fully-qualified class names identifying JPA-managed classes + * (such as entity classes, mapped superclasses and the like); may + * be {@code null}. The {@link Collection} is copied and no + * reference to it is retained. + * + * @param mappingFileNames a {@link Collection} of classpath + * resource names identifying JPA mapping files; may be {@code + * null}. The {@link Collection} is copied and no reference to it + * is retained. + * + * @param jtaDataSourceName the name of a data source that may be + * enrolled in JTA-compliant transactions; may be {@code null} + * + * @param nonJtaDataSourceName the name of a data source that + * should not be enrolled in JTA-compliant transactions; may be + * {@code null} + * + * @param dataSourceProvider a {@link DataSourceProvider} capable + * of supplying {@link DataSource} instances; must not be {@code + * null} + * + * @param properties a {@link Properties} object representing the + * properties of the persistence unit represented by this {@link + * PersistenceUnitInfoBean}; may be {@code null}. A reference is + * retained to this object. + * + * @param sharedCacheMode the {@link SharedCacheMode} this {@link + * PersistenceUnitInfoBean} will use; may be {@code null} in which + * case {@link SharedCacheMode#UNSPECIFIED} will be used instead + * + * @param transactionType the {@link + * PersistenceUnitTransactionType} this {@link + * PersistenceUnitInfoBean} will use; may be {@code null} in which + * case {@link PersistenceUnitTransactionType#JTA} will be used + * instead + * + * @param validationMode the {@link ValidationMode} this {@link + * PersistenceUnitInfoBean} will use; may be {@code null} in which + * case {@link ValidationMode#AUTO} will be used instead + * + * @exception NullPointerException if {@code persistenceUnitName}, + * {@code persistenceUnitRootUrl} or {@code dataSourceProvider} is + * {@code null} + * + * @see #getPersistenceUnitName() + * + * @see #getPersistenceUnitRootUrl() + * + * @see #getPersistenceXMLSchemaVersion() + * + * @see #getPersistenceProviderClassName() + * + * @see #getClassLoader() + * + * @see #getNewTempClassLoader() + * + * @see #excludeUnlistedClasses() + * + * @see #getJarFileUrls() + * + * @see #getManagedClassNames() + * + * @see #getMappingFileNames() + * + * @see #getJtaDataSource() + * + * @see #getNonJtaDataSource() + * + * @see #getProperties() + * + * @see #getSharedCacheMode() + * + * @see #getTransactionType() + * + * @see #getValidationMode() + */ + public PersistenceUnitInfoBean(final String persistenceUnitName, + final URL persistenceUnitRootUrl, + final String persistenceXMLSchemaVersion, + final String persistenceProviderClassName, + final ClassLoader classLoader, + final Supplier tempClassLoaderSupplier, + final Consumer classTransformerConsumer, + final boolean excludeUnlistedClasses, + final Collection jarFileUrls, + final Collection managedClassNames, + final Collection mappingFileNames, + final String jtaDataSourceName, + final String nonJtaDataSourceName, + final DataSourceProvider dataSourceProvider, + final Properties properties, + final SharedCacheMode sharedCacheMode, + final PersistenceUnitTransactionType transactionType, + final ValidationMode validationMode) { + super(); + Objects.requireNonNull(persistenceUnitName); + Objects.requireNonNull(persistenceUnitRootUrl); + Objects.requireNonNull(dataSourceProvider); + Objects.requireNonNull(transactionType); + + this.persistenceUnitName = persistenceUnitName; + this.persistenceUnitRootUrl = persistenceUnitRootUrl; + this.persistenceProviderClassName = persistenceProviderClassName; + this.persistenceXMLSchemaVersion = persistenceXMLSchemaVersion == null ? "2.2" : persistenceXMLSchemaVersion; + this.originalClassLoader = classLoader; + this.classLoader = classLoader; + this.tempClassLoaderSupplier = tempClassLoaderSupplier; + this.classTransformerConsumer = classTransformerConsumer; + this.excludeUnlistedClasses = excludeUnlistedClasses; + + if (jarFileUrls == null || jarFileUrls.isEmpty()) { + this.jarFileUrls = Collections.emptyList(); + } else { + this.jarFileUrls = Collections.unmodifiableList(new ArrayList<>(jarFileUrls)); + } + + if (managedClassNames == null || managedClassNames.isEmpty()) { + this.managedClassNames = new LinkedHashSet<>(); + } else { + this.managedClassNames = new LinkedHashSet<>(managedClassNames); + } + this.managedClassNamesView = new AbstractList() { + @Override + public boolean isEmpty() { + return PersistenceUnitInfoBean.this.managedClassNames.isEmpty(); + } + @Override + public int size() { + return PersistenceUnitInfoBean.this.managedClassNames.size(); + } + @Override + public Iterator iterator() { + return PersistenceUnitInfoBean.this.managedClassNames.iterator(); + } + @Override + public String get(final int index) { + final Iterator iterator = this.iterator(); + assert iterator != null; + for (int i = 0; i < index; i++) { + iterator.next(); + } + return iterator.next(); + } + }; + + if (mappingFileNames == null || mappingFileNames.isEmpty()) { + this.mappingFileNames = Collections.emptyList(); + } else { + this.mappingFileNames = Collections.unmodifiableList(new ArrayList<>(mappingFileNames)); + } + + if (properties == null) { + this.properties = new Properties(); + } else { + this.properties = properties; + } + + if (jtaDataSourceName == null || jtaDataSourceName.isEmpty()) { + this.jtaDataSourceName = null; + } else { + this.jtaDataSourceName = jtaDataSourceName; + } + this.nonJtaDataSourceName = nonJtaDataSourceName; + this.dataSourceProvider = dataSourceProvider; + + if (sharedCacheMode == null) { + this.sharedCacheMode = SharedCacheMode.UNSPECIFIED; + } else { + this.sharedCacheMode = sharedCacheMode; + } + this.transactionType = transactionType; + if (validationMode == null) { + this.validationMode = ValidationMode.AUTO; + } else { + this.validationMode = validationMode; + } + } + + + /* + * Instance methods. + */ + + + boolean addManagedClassName(final String className) { + return className != null && this.managedClassNames.add(className); + } + + @Override + public List getJarFileUrls() { + return this.jarFileUrls; + } + + @Override + public URL getPersistenceUnitRootUrl() { + return this.persistenceUnitRootUrl; + } + + @Override + public List getManagedClassNames() { + return this.managedClassNamesView; + } + + @Override + public boolean excludeUnlistedClasses() { + return this.excludeUnlistedClasses; + } + + @Override + public SharedCacheMode getSharedCacheMode() { + return this.sharedCacheMode; + } + + @Override + public ValidationMode getValidationMode() { + return this.validationMode; + } + + @Override + public Properties getProperties() { + return this.properties; + } + + @Override + public ClassLoader getClassLoader() { + return this.classLoader; + } + + @Override + public String getPersistenceXMLSchemaVersion() { + return this.persistenceXMLSchemaVersion; + } + + @Override + public ClassLoader getNewTempClassLoader() { + ClassLoader cl = null; + if (this.tempClassLoaderSupplier != null) { + cl = this.tempClassLoaderSupplier.get(); + } + if (cl == null) { + cl = this.originalClassLoader; + if (cl == null) { + cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = this.getClass().getClassLoader(); + } + } + } + return cl; + } + + @Override + public void addTransformer(final ClassTransformer classTransformer) { + if (this.classTransformerConsumer != null) { + this.classTransformerConsumer.accept(classTransformer); + } + } + + @Override + public String getPersistenceUnitName() { + return this.persistenceUnitName; + } + + @Override + public String getPersistenceProviderClassName() { + return this.persistenceProviderClassName; + } + + @Override + public PersistenceUnitTransactionType getTransactionType() { + return this.transactionType; + } + + @Override + public final DataSource getJtaDataSource() { + return this.dataSourceProvider.getDataSource(true, this.nonJtaDataSourceName == null, this.jtaDataSourceName); + } + + @Override + public final DataSource getNonJtaDataSource() { + return this.dataSourceProvider.getDataSource(false, false, this.nonJtaDataSourceName); + } + + @Override + public List getMappingFileNames() { + return this.mappingFileNames; + } + + /** + * Given a {@link Persistence} (a Java object representation of a + * {@code META-INF/persistence.xml} resource), a {@link URL} + * representing the root of all persistence units, a {@link Map} + * of unlisted managed classes (entity classes, mapped + * superclasses and so on) indexed by persistence unit name, and a + * {@link DataSourceProvider} that can provide {@link DataSource} + * instances, returns a {@link Collection} of {@link + * PersistenceUnitInfoBean} instances representing all the + * persistence units in play. + * + *

This method never returns {@code null}.

+ * + * @param persistence a {@link Persistence} containing bootstrap + * information from which persistence units and their + * configuration may be deduced; may be {@code null} in which case + * an {@linkplain Collection#isEmpty() empty} {@link Collection} + * will be returned + * + * @param classLoader a {@link ClassLoader} that the resulting + * {@link PersistenceUnitInfoBean} instances will use; may be + * {@code null} + * + * @param tempClassLoaderSupplier a {@link Supplier} of a {@link + * ClassLoader} that will be used to implement the {@link + * PersistenceUnitInfo#getNewTempClassLoader()} method; may be + * {@code null} + * + * @param rootUrl the {@link URL} representing the root of all + * persistence units; must not be {@code null} + * + * @param unlistedClasses a {@link Map} of managed classes indexed + * by persistence unit name whose values might not be explicitly + * listed in a {@link PersistenceUnit}; may be {@code null} + * + * @param dataSourceProvider a {@link DataSourceProvider}; must + * not be {@code null} + * + * @return a non-{@code null} {@link Collection} of {@link + * PersistenceUnitInfoBean} instances + * + * @exception MalformedURLException if a {@link URL} could not be + * constructed + * + * @exception NullPointerException if {@code rootUrl} or {@code + * dataSourceProvider} is {@code null} + * + * @see #fromPersistenceUnit(Persistence.PersistenceUnit, + * ClassLoader, Supplier, URL, Map, DataSourceProvider) + * + * @see PersistenceUnitInfo + */ + public static final Collection + fromPersistence(final Persistence persistence, + final ClassLoader classLoader, + final Supplier tempClassLoaderSupplier, + final URL rootUrl, + Map>> unlistedClasses, + final DataSourceProvider dataSourceProvider) + throws MalformedURLException { + Objects.requireNonNull(rootUrl); + if (unlistedClasses == null) { + unlistedClasses = Collections.emptyMap(); + } + final Collection returnValue; + if (persistence == null) { + returnValue = Collections.emptySet(); + } else { + final Collection persistenceUnits = persistence.getPersistenceUnit(); + if (persistenceUnits == null || persistenceUnits.isEmpty()) { + returnValue = Collections.emptySet(); + } else { + returnValue = new ArrayList<>(); + for (final PersistenceUnit persistenceUnit : persistenceUnits) { + assert persistenceUnit != null; + returnValue.add(fromPersistenceUnit(persistenceUnit, + classLoader, + tempClassLoaderSupplier, + rootUrl, + unlistedClasses, + dataSourceProvider)); + } + } + } + return returnValue; + } + + /** + * Given a {@link PersistenceUnit} (a Java object representation + * of a {@code } element in a {@code + * META-INF/persistence.xml} resource), a {@link URL} representing + * the persistence unit's root, a {@link Map} of unlisted managed + * classes (entity classes, mapped superclasses and so on) indexed + * by persistence unit name, and a {@link DataSourceProvider} that + * can supply {@link DataSource} instances, returns a {@link + * PersistenceUnitInfoBean} representing the persistence unit in + * question. + * + *

This method never returns {@code null}.

+ * + *

This method calls the {@link + * #fromPersistenceUnit(Persistence.PersistenceUnit, ClassLoader, + * Supplier, URL, Map, DataSourceProvider)} method using the + * return value of the {@link Thread#getContextClassLoader()} + * method as the {@link ClassLoader}.

+ * + * @param persistenceUnit a {@link PersistenceUnit}; must not be + * {@code null} + * + * @param rootUrl the {@link URL} representing the root of the + * persistence unit; must not be {@code null} + * + * @param unlistedClasses a {@link Map} of managed classes indexed + * by persistence unit name whose values might not be explicitly + * listed in the supplied {@link PersistenceUnit}; may be {@code + * null} + * + * @param dataSourceProvider a {@link DataSourceProvider}; must not + * be {@code null} + * + * @return a non-{@code null} {@link PersistenceUnitInfoBean} + * + * @exception MalformedURLException if a {@link URL} could not be + * constructed + * + * @exception NullPointerException if {@code persistenceUnit}, + * {@code rootUrl} or {@code dataSourceProvider} is {@code null} + * + * @see #fromPersistenceUnit(Persistence.PersistenceUnit, + * ClassLoader, Supplier, URL, Map, DataSourceProvider) + * + * @see PersistenceUnit + * + * @see PersistenceUnitInfo + */ + public static final PersistenceUnitInfoBean + fromPersistenceUnit(final PersistenceUnit persistenceUnit, + final URL rootUrl, + final Map>> unlistedClasses, + final DataSourceProvider dataSourceProvider) + throws MalformedURLException { + final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + return fromPersistenceUnit(persistenceUnit, + classLoader, + () -> classLoader, + rootUrl, + unlistedClasses, + dataSourceProvider); + } + + /** + * Given a {@link PersistenceUnit} (a Java object representation + * of a {@code } element in a {@code + * META-INF/persistence.xml} resource), a {@link ClassLoader} for + * loading JPA classes and resources, a {@link Supplier} of {@link + * ClassLoader} instances for helping to implement the {@link + * PersistenceUnitInfo#getNewTempClassLoader()} method, a {@link + * URL} representing the persistence unit's root, a {@link Map} of + * unlisted managed classes (entity classes, mapped superclasses + * and so on) indexed by persistence unit name, and a {@link + * DataSourceProvider} that can provide {@link DataSource} + * instances, returns a {@link PersistenceUnitInfoBean} + * representing the persistence unit in question. + * + *

This method never returns {@code null}.

+ * + * @param persistenceUnit a {@link PersistenceUnit}; must not be + * {@code null} + * + * @param classLoader a {@link ClassLoader} that the resulting + * {@link PersistenceUnitInfoBean} will use; may be {@code null} + * + * @param tempClassLoaderSupplier a {@link Supplier} of a {@link + * ClassLoader} that will be used to implement the {@link + * PersistenceUnitInfo#getNewTempClassLoader()} method; may be + * {@code null} + * + * @param rootUrl the {@link URL} representing the root of the + * persistence unit; must not be {@code null} + * + * @param unlistedClasses a {@link Map} of managed classes indexed + * by persistence unit name whose values might not be explicitly + * listed in the supplied {@link PersistenceUnit}; may be {@code + * null} + * + * @param dataSourceProvider a {@link DataSourceProvider}; must + * not be {@code null} + * + * @return a non-{@code null} {@link PersistenceUnitInfoBean} + * + * @exception MalformedURLException if a {@link URL} could not be + * constructed + * + * @exception NullPointerException if {@code persistenceUnit} or + * {@code rootUrl} is {@code null} + * + * @see PersistenceUnit + * + * @see PersistenceUnitInfo + */ + public static final PersistenceUnitInfoBean + fromPersistenceUnit(final PersistenceUnit persistenceUnit, + final ClassLoader classLoader, + Supplier tempClassLoaderSupplier, + final URL rootUrl, + final Map>> unlistedClasses, + final DataSourceProvider dataSourceProvider) + throws MalformedURLException { + Objects.requireNonNull(persistenceUnit); + Objects.requireNonNull(rootUrl); + Objects.requireNonNull(dataSourceProvider); + + final Collection jarFiles = persistenceUnit.getJarFile(); + final List jarFileUrls = new ArrayList<>(); + for (final String jarFile : jarFiles) { + if (jarFile != null) { + jarFileUrls.add(createJarFileURL(rootUrl, jarFile)); + } + } + + final Collection mappingFiles = persistenceUnit.getMappingFile(); + + final Properties properties = new Properties(); + final PersistenceUnit.Properties persistenceUnitProperties = persistenceUnit.getProperties(); + if (persistenceUnitProperties != null) { + final Collection propertyInstances = + persistenceUnitProperties.getProperty(); + if (propertyInstances != null && !propertyInstances.isEmpty()) { + for (final PersistenceUnit.Properties.Property property : propertyInstances) { + assert property != null; + properties.setProperty(property.getName(), property.getValue()); + } + } + } + + final Collection managedClasses = persistenceUnit.getClazz(); + assert managedClasses != null; + String name = persistenceUnit.getName(); + if (name == null) { + name = ""; + } + final Boolean excludeUnlistedClasses = persistenceUnit.isExcludeUnlistedClasses(); + if (!Boolean.TRUE.equals(excludeUnlistedClasses)) { + if (unlistedClasses != null && !unlistedClasses.isEmpty()) { + Collection> myUnlistedClasses = unlistedClasses.get(name); + if (myUnlistedClasses != null && !myUnlistedClasses.isEmpty()) { + for (final Class unlistedClass : myUnlistedClasses) { + if (unlistedClass != null) { + managedClasses.add(unlistedClass.getName()); + } + } + } + // Also add "default" ones + if (!name.isEmpty()) { + myUnlistedClasses = unlistedClasses.get(""); + if (myUnlistedClasses != null && !myUnlistedClasses.isEmpty()) { + for (final Class unlistedClass : myUnlistedClasses) { + if (unlistedClass != null) { + managedClasses.add(unlistedClass.getName()); + } + } + } + } + } + } + + final SharedCacheMode sharedCacheMode; + final PersistenceUnitCachingType persistenceUnitCachingType = persistenceUnit.getSharedCacheMode(); + if (persistenceUnitCachingType == null) { + sharedCacheMode = SharedCacheMode.UNSPECIFIED; + } else { + sharedCacheMode = SharedCacheMode.valueOf(persistenceUnitCachingType.name()); + } + + final PersistenceUnitTransactionType transactionType; + final io.helidon.integrations.cdi.jpa.jaxb.PersistenceUnitTransactionType persistenceUnitTransactionType = + persistenceUnit.getTransactionType(); + if (persistenceUnitTransactionType == null) { + transactionType = PersistenceUnitTransactionType.JTA; // I guess + } else { + transactionType = PersistenceUnitTransactionType.valueOf(persistenceUnitTransactionType.name()); + } + + final ValidationMode validationMode; + final PersistenceUnitValidationModeType validationModeType = persistenceUnit.getValidationMode(); + if (validationModeType == null) { + validationMode = ValidationMode.AUTO; + } else { + validationMode = ValidationMode.valueOf(validationModeType.name()); + } + + if (tempClassLoaderSupplier == null) { + if (classLoader instanceof URLClassLoader) { + tempClassLoaderSupplier = () -> new URLClassLoader(((URLClassLoader) classLoader).getURLs()); + } else { + tempClassLoaderSupplier = () -> classLoader; + } + } + + final PersistenceUnitInfoBean returnValue = + new PersistenceUnitInfoBean(name, + rootUrl, + "2.2", + persistenceUnit.getProvider(), + classLoader, + tempClassLoaderSupplier, + null, // no consuming of ClassTransformer for now + excludeUnlistedClasses == null ? true : excludeUnlistedClasses, + jarFileUrls, + managedClasses, + mappingFiles, + persistenceUnit.getJtaDataSource(), + persistenceUnit.getNonJtaDataSource(), + dataSourceProvider, + properties, + sharedCacheMode, + transactionType, + validationMode); + return returnValue; + } + + private static URL createJarFileURL(final URL persistenceUnitRootUrl, final String jarFileUrlString) + throws MalformedURLException { + Objects.requireNonNull(persistenceUnitRootUrl); + Objects.requireNonNull(jarFileUrlString); + // Revisit: probably won't work if persistenceUnitRootUrl is, say, a jar URL + final URL returnValue = new URL(persistenceUnitRootUrl, jarFileUrlString); + return returnValue; + } + + + /** + * A {@linkplain FunctionalInterface functional interface} + * indicating that its implementations can supply {@link + * DataSource}s. + * + * @see #getDataSource(boolean, boolean, String) + */ + @FunctionalInterface + public interface DataSourceProvider { + + /** + * Supplies a {@link DataSource}. + * + *

Implementations of this method are permitted to return + * {@code null}.

+ * + * @param jta if {@code true}, the {@link DataSource} that is + * returned may be enrolled in JTA-compliant transactions + * + * @param useDefaultJta if {@code true}, and if the {@code + * jta} parameter value is {@code true}, the supplied {@code + * dataSourceName} may be ignored and a default {@link + * DataSource} eligible for enrolling in JTA-compliant + * transactions will be returned if possible + * + * @param dataSourceName the name of the {@link DataSource} to + * return; may be {@code null}; ignored if both {@code jta} + * and {@code useDefaultJta} are {@code true} + * + * @return an appropriate {@link DataSource}, or {@code null} + * + * @see PersistenceUnitInfoBean#getJtaDataSource() + * + * @see PersistenceUnitInfoBean#getNonJtaDataSource() + */ + DataSource getDataSource(boolean jta, boolean useDefaultJta, String dataSourceName); + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java new file mode 100644 index 00000000000..95831872e8b --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides classes and interfaces that integrate the + * provider-independent parts of JPA into CDI. + * + * @see io.helidon.integrations.cdi.jpa.JpaExtension + * + * @see io.helidon.integrations.cdi.jpa.PersistenceUnitInfoBean + */ +package io.helidon.integrations.cdi.jpa; diff --git a/integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html b/integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html new file mode 100644 index 00000000000..5c361b80409 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/javadoc/io/helidon/integrations/cdi/jpa/jaxb/package.html @@ -0,0 +1,30 @@ + + + Provides classes and interfaces generated from + the JPA 2.2 {@code persistence} XML schema; other + documentation in this package is generated indirectly by + the {@code xjc} tool, which is solely responsible + for its contents and omissions. + + @author Laird Nelson + + @see io.helidon.integrations.cdi.jpa.jaxb.Persistence + diff --git a/integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..89f9c163080 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,25 @@ + + + + diff --git a/integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..caae586014f --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +io.helidon.integrations.cdi.jpa.JpaExtension diff --git a/integrations/cdi/jpa-cdi/src/main/resources/messages.properties b/integrations/cdi/jpa-cdi/src/main/resources/messages.properties new file mode 100644 index 00000000000..fdb1c0f5f01 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/resources/messages.properties @@ -0,0 +1,15 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# diff --git a/integrations/cdi/jpa-weld/README.adoc b/integrations/cdi/jpa-weld/README.adoc new file mode 100644 index 00000000000..0c6f4f4d7ed --- /dev/null +++ b/integrations/cdi/jpa-weld/README.adoc @@ -0,0 +1,30 @@ += Helidon JPA Weld Integration + +The Helidon JPA Weld Integration project performs the +CDI-provider-specific work of integrating JPA into standalone CDI +applications (including those based on Helidon MicroProfile). It is +one of several projects that together make up overall JPA support for +standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-agnostic counterpart, such as the `jpa-cdi` project + found elsewhere in this git repository +* a JPA provider implementation, such as Eclipselink +* a JPA-provider-specific library to assist the JPA provider in + determining what kind of environment it is running in, such as the + `eclipselink-cdi` project found elsewhere in this git repository +* a library capable of integrating `DataSource`s into CDI, such as the + `datasource-hikaricp` project found elsewhere in this git repository +* a suitable JDBC-compliant database driver library + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JPA Weld Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file. diff --git a/integrations/cdi/jpa-weld/pom.xml b/integrations/cdi/jpa-weld/pom.xml new file mode 100644 index 00000000000..200c5ebff82 --- /dev/null +++ b/integrations/cdi/jpa-weld/pom.xml @@ -0,0 +1,162 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jpa-weld + Helidon CDI Integrations JPA Weld + + + package + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.jboss.weld.se + weld-se-core + test + + + org.slf4j + slf4j-jdk14 + test + + + com.h2database + h2 + test + + + ${project.groupId} + helidon-integrations-cdi-jta-weld + ${project.version} + test + + + ${project.groupId} + helidon-integrations-cdi-datasource-hikaricp + ${project.version} + test + + + ${project.groupId} + helidon-integrations-cdi-eclipselink + ${project.version} + test + + + io.helidon.microprofile.config + helidon-microprofile-config-cdi + ${project.version} + test + + + + + ${project.groupId} + helidon-integrations-cdi-jpa + ${project.version} + runtime + + + org.jboss + jandex + runtime + + + + + javax.annotation + javax.annotation-api + provided + + + javax.enterprise + cdi-api + provided + + + jakarta.persistence + jakarta.persistence-api + provided + + + javax.transaction + javax.transaction-api + provided + + + javax.validation + validation-api + provided + + + + + org.jboss.weld + weld-spi + compile + + + + + + + maven-surefire-plugin + + + ${basedir}/src/test/java/logging.properties + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + + diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java new file mode 100644 index 00000000000..5469b85c762 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/TransactionObserver.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa.weld; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.transaction.TransactionScoped; + +/** + * A bean housing an observer method that alerts a {@link + * WeldJpaInjectionServices} instance when a JTA transaction is + * available. + * + *

Design Notes

+ * + *

This class is excluded by this bean archive's {@code + * META-INF/beans.xml} resource if the {@link TransactionScoped + * javax.transaction.TransactionScoped} class is not available. This + * has the effect of fully decoupling the rest of this bean archive + * (most notably the {@link WeldJpaInjectionServices} class) from + * transactional concerns if a JTA implementation is not present at + * runtime.

+ * + * @see WeldJpaInjectionServices + * + * @see Initialized + * + * @see TransactionScoped + */ +@ApplicationScoped +final class TransactionObserver { + + + /* + * Static fields. + */ + private static final Logger LOGGER = Logger.getLogger(TransactionObserver.class.getName(), "messages"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link TransactionObserver}. + */ + private TransactionObserver() { + super(); + final String cn = TransactionObserver.class.getName(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, ""); + LOGGER.exiting(cn, ""); + } + } + + + /* + * Static methods. + */ + + + /** + * Observes the {@linkplain Initialized initialization} of the + * {@link TransactionScoped} scope and calls the {@link + * WeldJpaInjectionServices#jtaTransactionBegun()} method on + * the supplied {@link WeldJpaInjectionServices} instance. + * + * @param event the opaque event that represents the + * initialization of the {@link TransactionScoped} scope; may be + * {@code null}; ignored + * + * @param services the {@link WeldJpaInjectionServices} to + * notify; may be {@code null} in which case no action will be + * taken + */ + private static void jtaTransactionBegun(@Observes @Initialized(TransactionScoped.class) final Object event, + final WeldJpaInjectionServices services) { + final String cn = TransactionObserver.class.getName(); + final String mn = "jtaTransactionBegun"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, services}); + } + + if (services != null) { + services.jtaTransactionBegun(); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Observes the {@linkplain Destroyed destruction} of the + * {@link TransactionScoped} scope and calls the {@link + * WeldJpaInjectionServices#jtaTransactionEnded()} method on + * the supplied {@link WeldJpaInjectionServices} instance. + * + * @param event the opaque event that represents the + * destruction of the {@link TransactionScoped} scope; may be + * {@code null}; ignored + * + * @param services the {@link WeldJpaInjectionServices} to + * notify; may be {@code null} in which case no action will be + * taken + */ + private static void jtaTransactionEnded(@Observes @Destroyed(TransactionScoped.class) final Object event, + final WeldJpaInjectionServices services) { + final String cn = TransactionObserver.class.getName(); + final String mn = "jtaTransactionEnded"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, services}); + } + + if (services != null) { + services.jtaTransactionEnded(); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java new file mode 100644 index 00000000000..e57285e3e3c --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServices.java @@ -0,0 +1,1121 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa.weld; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.context.spi.CreationalContext; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.literal.NamedLiteral; +import javax.enterprise.inject.spi.Annotated; +import javax.enterprise.inject.spi.AnnotatedField; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Named; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.PersistenceContext; +import javax.persistence.PersistenceException; +import javax.persistence.PersistenceUnit; +import javax.persistence.SynchronizationType; +import javax.persistence.spi.PersistenceProvider; +import javax.persistence.spi.PersistenceUnitInfo; + +import org.jboss.weld.injection.spi.JpaInjectionServices; +import org.jboss.weld.injection.spi.ResourceReference; +import org.jboss.weld.injection.spi.ResourceReferenceFactory; +import org.jboss.weld.manager.api.ExecutorServices; +import org.jboss.weld.manager.api.WeldManager; + +import static javax.persistence.spi.PersistenceUnitTransactionType.JTA; +import static javax.persistence.spi.PersistenceUnitTransactionType.RESOURCE_LOCAL; + +/** + * A {@link JpaInjectionServices} implementation that integrates JPA + * functionality into Weld-based CDI environments. + * + * @see JpaInjectionServices + */ +final class WeldJpaInjectionServices implements JpaInjectionServices { + + + /* + * Static fields. + */ + + + /* + * Weld instantiates this class exactly three (!) times during + * normal execution (see https://issues.jboss.org/browse/WELD-2563 + * for details). Only one of those instances (the first) is + * actually used to produce EntityManagers and + * EntityManagerFactories; the other two are discarded. The + * static instance and underway fields ensure that truly only one + * instance processes all incoming calls, and that it is the one + * that is actually tracked and stored by Weld itself in the + * return value of the WeldManager#getServices() method. + * + * See the underway() method as well. + */ + + /** + * The single officially sanctioned instance of this class. + * + *

This field may be {@code null}.

+ * + * @see WELD-2563 + * + * @see #getInstance() + */ + private static volatile WeldJpaInjectionServices instance; + + /** + * Whether a "business" method of this class has been invoked or + * not. + * + * @see WELD-2563 + */ + private static volatile boolean underway; + + /** + * The {@link Logger} for use by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(WeldJpaInjectionServices.class.getName(), "messages"); + + + /* + * Instance fields. + */ + + + /** + * A {@link Set} of {@link EntityManager}s that have been created + * as container-managed {@link EntityManager}s, i.e. not + * application-managed ones. + * + *

This field is never {@code null}.

+ * + *

The {@link Set} assigned to this field is safe for + * concurrent usage by multiple threads.

+ */ + private final Set containerManagedEntityManagers; + + /** + * A {@link Map} of {@link EntityManagerFactory} instances indexed + * by names of persistence units. + * + *

This field is never {@code null}.

+ * + *

Synchronization on this field is required for concurrent + * thread access.

+ */ + // @GuardedBy("this") + private volatile Map emfs; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link WeldJpaInjectionServices}. + * + *

Oddly, the fact that this constructor is {@code private} + * does not prevent Weld from loading it as a service. This is an + * unexpected bonus as nothing about this class should be {@code + * public}.

+ */ + private WeldJpaInjectionServices() { + super(); + synchronized (WeldJpaInjectionServices.class) { + // See https://issues.jboss.org/browse/WELD-2563. Make sure + // only the first instance is "kept" as it's the one tracked by + // WeldManager's ServiceRegistry. The others are discarded. + if (instance == null) { + assert !underway; + instance = this; + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.logp(Level.WARNING, + WeldJpaInjectionServices.class.getName(), + "", + "experimental"); + } + } else if (underway) { + throw new IllegalStateException(); + } + } + this.containerManagedEntityManagers = ConcurrentHashMap.newKeySet(); + } + + /** + * Records the fact that a significant method has been invoked. + * + * @see WELD-2563 + */ + private static synchronized void underway() { + underway = true; + } + + /** + * Returns the only instance of this class. + * + *

This method never returns {@code null}.

+ * + * @return the same non-{@code null} {@link WeldJpaInjectionServices} + * when invoked + * + * @see WELD-2563 + */ + static synchronized WeldJpaInjectionServices getInstance() { + return instance; + } + + /** + * Called by the ({@code private}) {@code TransactionObserver} + * class when a JTA transaction is begun. + * + *

The Narayana CDI integration this class is often deployed + * with will fire such events. These events serve as an + * indication that a call to {@link + * javax.transaction.TransactionManager#begin()} has been + * made.

+ * + *

{@link EntityManager}s created by this class will have their + * {@link EntityManager#joinTransaction()} methods called if the + * supplied object is non-{@code null}.

+ */ + void jtaTransactionBegun() { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "jtaTransactionBegun"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + assert this == instance; + for (final EntityManager containerManagedEntityManager : this.containerManagedEntityManagers) { + assert containerManagedEntityManager != null; + final Map properties = containerManagedEntityManager.getProperties(); + final Object synchronizationType; + if (properties == null) { + synchronizationType = null; + } else { + synchronizationType = properties.get(SynchronizationType.class.getName()); + } + if (SynchronizationType.SYNCHRONIZED.equals(synchronizationType)) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "{0} joining transaction", containerManagedEntityManager); + } + containerManagedEntityManager.joinTransaction(); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Called by the ({@code private}) {@code TransactionObserver} + * class when a JTA transaction has ended, either successfully or + * unsuccessfully. + * + *

The Narayana CDI integration this class is often deployed + * with will fire such events. These events serve as an + * indication that a call to {@link + * javax.transaction.TransactionManager#commit()} or {@link + * javax.transaction.TransactionManager#rollback()} has been + * made.

+ */ + void jtaTransactionEnded() { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "jtaTransactionEnded"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + // This method is reserved for future use. + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Returns a {@link ResourceReferenceFactory} whose {@link + * ResourceReferenceFactory#createResource()} method will be + * invoked appropriately by Weld later. + * + *

This method never returns {@code null}.

+ * + * @param injectionPoint the {@link InjectionPoint} annotated with + * {@link PersistenceContext}; must not be {@code null} + * + * @return a non-{@code null} {@link ResourceReferenceFactory} + * whose {@link ResourceReferenceFactory#createResource()} method + * will create {@link EntityManager} instances + * + * @exception NullPointerException if {@code injectionPoint} is + * {@code null} + * + * @see ResourceReferenceFactory#createResource() + */ + @Override + public ResourceReferenceFactory registerPersistenceContextInjectionPoint(final InjectionPoint injectionPoint) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "registerPersistenceContextInjectionPoint"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, injectionPoint); + } + underway(); + assert this == instance; + final ResourceReferenceFactory returnValue; + Objects.requireNonNull(injectionPoint); + final Annotated annotatedMember = injectionPoint.getAnnotated(); + assert annotatedMember != null; + final PersistenceContext persistenceContextAnnotation = annotatedMember.getAnnotation(PersistenceContext.class); + if (persistenceContextAnnotation == null) { + throw new IllegalArgumentException("injectionPoint.getAnnotated().getAnnotation(PersistenceContext.class) == null"); + } + final String name; + final String n = persistenceContextAnnotation.unitName(); + if (n.isEmpty()) { + if (annotatedMember instanceof AnnotatedField) { + name = ((AnnotatedField) annotatedMember).getJavaMember().getName(); + } else { + name = n; + } + } else { + name = n; + } + final SynchronizationType synchronizationType = persistenceContextAnnotation.synchronization(); + assert synchronizationType != null; + synchronized (this) { + if (this.emfs == null) { + this.emfs = new ConcurrentHashMap<>(); + } + } + returnValue = () -> new EntityManagerResourceReference(name, synchronizationType); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns a {@link ResourceReferenceFactory} whose {@link + * ResourceReferenceFactory#createResource()} method will be + * invoked appropriately by Weld later. + * + *

This method never returns {@code null}.

+ * + * @param ip the {@link InjectionPoint} annotated with {@link + * PersistenceUnit}; must not be {@code null} + * + * @return a non-{@code null} {@link ResourceReferenceFactory} + * whose {@link ResourceReferenceFactory#createResource()} method + * will create {@link EntityManagerFactory} instances + * + * @exception NullPointerException if {@code ip} is + * {@code null} + * + * @see ResourceReferenceFactory#createResource() + */ + @Override + public ResourceReferenceFactory registerPersistenceUnitInjectionPoint(final InjectionPoint ip) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "registerPersistenceUnitInjectionPoint"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, ip); + } + + underway(); + assert this == instance; + final ResourceReferenceFactory returnValue; + Objects.requireNonNull(ip); + final Annotated annotatedMember = ip.getAnnotated(); + assert annotatedMember != null; + final PersistenceUnit persistenceUnitAnnotation = annotatedMember.getAnnotation(PersistenceUnit.class); + if (persistenceUnitAnnotation == null) { + throw new IllegalArgumentException("ip.getAnnotated().getAnnotation(PersistenceUnit.class) == null"); + } + final String name; + final String n = persistenceUnitAnnotation.unitName(); + if (n.isEmpty()) { + if (annotatedMember instanceof AnnotatedField) { + name = ((AnnotatedField) annotatedMember).getJavaMember().getName(); + } else { + name = n; + } + } else { + name = n; + } + synchronized (this) { + if (this.emfs == null) { + this.emfs = new ConcurrentHashMap<>(); + } + } + returnValue = () -> new EntityManagerFactoryResourceReference(this.emfs, name); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Invoked by Weld automatically to clean up any resources held by + * this class. + */ + @Override + public void cleanup() { + // cleanup() can get invoked multiple times at will by Weld. + // Specifically, the same Service instance can be stored in + // multiple BeanManagerImpls, and each one can call its + // cleanup() method, so it must be idempotent. + // + // See + // https://github.com/weld/core/blob/06fcaf4a6f625f101be5804208c1eb3a32884773/impl/src/main/java/org/jboss/weld/Container.java#L143-L145 + // and + // https://github.com/weld/core/blob/06fcaf4a6f625f101be5804208c1eb3a32884773/impl/src/main/java/org/jboss/weld/manager/BeanManagerImpl.java#L1173. + if (underway) { + assert this == instance; + + // this.containerManagedEntityManagers should be empty + // already. If for some reason it is not, we just clear() + // it (rather than, say, calling em.close() on each + // element). This is for two reasons: one, we're being + // cleaned up so the whole container is going down anyway. + // Two, we're going to close() all EntityManagerFactories + // which should also do the job since an + // EntityManagerFactory's EntityManagers are supposed to + // be closed when the EntityManagerFactory is closed. + this.containerManagedEntityManagers.clear(); + + final Map emfs = this.emfs; + if (emfs != null && !emfs.isEmpty()) { + final Collection values = emfs.values(); + assert values != null; + assert !values.isEmpty(); + final Iterator iterator = values.iterator(); + assert iterator != null; + assert iterator.hasNext(); + while (iterator.hasNext()) { + final EntityManagerFactory emf = iterator.next(); + assert emf != null; + if (emf.isOpen()) { + emf.close(); + } + iterator.remove(); + } + } + } + assert this.containerManagedEntityManagers.isEmpty(); + assert this.emfs == null || this.emfs.isEmpty(); + synchronized (WeldJpaInjectionServices.class) { + underway = false; + instance = null; + } + } + + /** + * Calls the {@link + * #registerPersistenceContextInjectionPoint(InjectionPoint)} + * method and invokes {@link ResourceReference#getInstance()} on + * its return value and returns the result. + * + *

This method never returns {@code null}.

+ * + * @param injectionPoint an {@link InjectionPoint} annotated with + * {@link PersistenceContext}; must not be {@code null} + * + * @return a non-{@code null} {@link EntityManager} + * + * @see #registerPersistenceContextInjectionPoint(InjectionPoint) + * + * @deprecated See the documentation for the {@link + * JpaInjectionServices#resolvePersistenceContext(InjectionPoint)} + * method. + */ + @Deprecated + @Override + public EntityManager resolvePersistenceContext(final InjectionPoint injectionPoint) { + return this.registerPersistenceContextInjectionPoint(injectionPoint).createResource().getInstance(); + } + + /** + * Calls the {@link + * #registerPersistenceUnitInjectionPoint(InjectionPoint)} method + * and invokes {@link ResourceReference#getInstance()} on its + * return value and returns the result. + * + *

This method never returns {@code null}.

+ * + * @param injectionPoint an {@link InjectionPoint} annotated with + * {@link PersistenceUnit}; must not be {@code null} + * + * @return a non-{@code null} {@link EntityManagerFactory} + * + * @see #registerPersistenceUnitInjectionPoint(InjectionPoint) + * + * @deprecated See the documentation for the {@link + * JpaInjectionServices#resolvePersistenceUnit(InjectionPoint)} + * method. + */ + @Deprecated + @Override + public EntityManagerFactory resolvePersistenceUnit(final InjectionPoint injectionPoint) { + return this.registerPersistenceUnitInjectionPoint(injectionPoint).createResource().getInstance(); + } + + + /* + * Static methods. + */ + + + /** + * Returns a {@link PersistenceProvider} for the supplied {@link + * PersistenceUnitInfo}. + * + *

This method never returns {@code null}.

+ * + * @param persistenceUnitInfo the {@link PersistenceUnitInfo} in + * question; must not be {@code null} + * + * @return a non-{@code null} {@link PersistenceProvider} + * + * @exception NullPointerException if {@code persistenceUnitInfo} + * was {@code null} + * + * @exception + * javax.enterprise.inject.UnsatisfiedResolutionException if no + * {@link PersistenceProvider} could be found + * + * @exception javax.enterprise.inject.AmbiguousResolutionException + * if there were many possible {@link PersistenceProvider}s that + * could be returned + * + * @exception ReflectiveOperationException if there was a + * reflection-related error + */ + private static PersistenceProvider getPersistenceProvider(final PersistenceUnitInfo persistenceUnitInfo) + throws ReflectiveOperationException { + final String providerClassName = Objects.requireNonNull(persistenceUnitInfo).getPersistenceProviderClassName(); + final PersistenceProvider persistenceProvider; + final CDI cdi = CDI.current(); + assert cdi != null; + if (providerClassName == null) { + persistenceProvider = cdi.select(PersistenceProvider.class).get(); + } else { + persistenceProvider = + (PersistenceProvider) cdi.select(Class.forName(providerClassName, + true, + Thread.currentThread().getContextClassLoader())).get(); + } + return persistenceProvider; + } + + /** + * Given the name of a persistence unit, uses a {@link + * BeanManager} internally to locate a {@link PersistenceUnitInfo} + * qualified with a {@link Named} annotation that {@linkplain + * Named#value() has the same name} as the supplied {@code name}, + * and returns it. + * + *

This method never returns {@code null}.

+ * + *

If there is only one {@link PersistenceUnitInfo} present in + * the CDI container, then it will be returned by this method when + * it is invoked, regardless of the value of the {@code name} + * parameter.

+ * + * @param name the name of the {@link PersistenceUnitInfo} to + * return; may be effectively ignored in some cases; must not be + * {@code null} + * + * @return a non-{@code null} {@link PersistenceUnitInfo}, which + * may not have the same name as that which was requested if it + * was the only such {@link PersistenceUnitInfo} in the CDI + * container + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @exception javax.enterprise.inject.AmbiguousResolutionException + * if there somehow was more than one {@link PersistenceUnitInfo} + * available + * + * @exception + * javax.enterprise.inject.UnsatisfiedResolutionException if there + * were no {@link PersistenceUnitInfo} instances available + */ + private static PersistenceUnitInfo getPersistenceUnitInfo(final String name) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "getPersistenceUnitInfo"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, name); + } + + Objects.requireNonNull(name); + final CDI cdi = CDI.current(); + assert cdi != null; + final BeanManager beanManager = cdi.getBeanManager(); + assert beanManager != null; + final Named named = NamedLiteral.of(name); + assert named != null; + Set> beans = beanManager.getBeans(PersistenceUnitInfo.class, named); + final boolean warn; + if (beans == null || beans.isEmpty()) { + beans = beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); + warn = LOGGER.isLoggable(Level.WARNING) && beans != null && !beans.isEmpty(); + } else { + warn = false; + } + if (beans == null || beans.isEmpty()) { + // Let CDI blow up in whatever way it does here. + cdi.select(PersistenceUnitInfo.class, named).get(); + throw new AssertionError(); + } + Bean bean = null; + final int size = beans.size(); + assert size > 0; + switch (size) { + case 1: + // We either got the explicit one we asked for + // (e.g. "dev"), or the only one there was (we asked for + // "dev"; the only one that was there was "test"). We may + // need to revisit this; this may be *too* convenient. + bean = beans.iterator().next(); + break; + default: + bean = beanManager.resolve(beans); + break; + } + final PersistenceUnitInfo returnValue = + (PersistenceUnitInfo) beanManager.getReference(bean, + PersistenceUnitInfo.class, + beanManager.createCreationalContext(bean)); + if (warn) { + LOGGER.logp(Level.WARNING, cn, mn, + "persistenceUnitNameMismatch", + new Object[] {returnValue, returnValue.getPersistenceUnitName(), name}); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Given a {@link Map} of {@link EntityManagerFactory} instances + * indexed by their persistence unit names, an optional {@link + * PersistenceUnitInfo} and the name of a persistence unit, + * returns a suitable {@link EntityManagerFactory} for the implied + * persistence unit, creating it if necessary. + * + *

This method never returns {@code null}.

+ * + *

The contents of the supplied {@link Map} may be altered by + * this method.

+ * + * @param emfs a {@link Map} of {@link EntityManagerFactory} + * instances indexed by their persistence unit names; must not be + * {@code null} but may be {@linkplain Map#isEmpty() empty} + * + * @param info a {@link PersistenceUnitInfo}; may be {@code null} + * in which case the supplied {@code name} must not be {@code + * null} + * + * @param name the name of the persistence unit; must not be + * {@code null}; if the supplied {@link PersistenceUnitInfo} is + * not {@code null} then {@linkplain + * PersistenceUnitInfo#getPersistenceUnitName() its name} should + * be equal to this value, but is not required to be + * + * @return a non-{@code null} {@link EntityManagerFactory} + * + * @exception NullPointerException if either {@code emfs} or + * {@code name} is {@code null} + * + * @see #computeEntityManagerFactory(PersistenceUnitInfo, String, + * EntityManagerFactory) + */ + private static EntityManagerFactory computeEntityManagerFactory(final Map emfs, + final PersistenceUnitInfo info, + final String name) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "computeEntityManagerFactory"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {emfs, info, name}); + } + + Objects.requireNonNull(emfs); + Objects.requireNonNull(name); + + final EntityManagerFactory returnValue = emfs.compute(name, (n, emf) -> computeEntityManagerFactory(info, n, emf)); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns the supplied {@link EntityManagerFactory}, if it is + * non-{@code null} and {@linkplain EntityManagerFactory#isOpen() + * open}, or creates a new one and returns it. + * + *

This method never returns {@code null}.

+ * + *

If creation is called for, then the supplied {@link + * PersistenceUnitInfo}'s {@linkplain + * PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} is checked to see + * if it is {@link + * javax.persistence.spi.PersistenceUnitTransactionType#RESOURCE_LOCAL + * RESOURCE_LOCAL}. If it is, then creation occurs by an + * invocation of the {@link + * Persistence#createEntityManagerFactory(String)} method. + * Otherwise, it occurs by an invocation of the {@link + * #createContainerManagedEntityManagerFactory(PersistenceUnitInfo)} + * method.

+ * + * @param info a {@link PersistenceUnitInfo} describing a + * persistence unit; may be {@code null} + * + * @param name the name of the persistence unit; must not be + * {@code null} + * + * @param existing an {@link EntityManagerFactory} that was + * already associated with the supplied {@code name}; may be + * {@code null} + * + * @return the supplied {@link EntityManagerFactory} if it is + * non-{@code null}, or a new one; never {@code null} + * + * @exception NullPointerException if {@code name} is {@code null} + * + * @exception PersistenceException if a persistence-related error + * occurs + * + * @see #createContainerManagedEntityManagerFactory(PersistenceUnitInfo) + */ + private static EntityManagerFactory computeEntityManagerFactory(final PersistenceUnitInfo info, + final String name, + final EntityManagerFactory existing) { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "computeEntityManagerFactory"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {info, name, existing}); + } + + final EntityManagerFactory returnValue; + if (existing == null) { + if (isResourceLocal(info)) { + returnValue = Persistence.createEntityManagerFactory(name); + } else { + EntityManagerFactory temp = null; + try { + temp = createContainerManagedEntityManagerFactory(info); + } catch (final ReflectiveOperationException reflectiveOperationException) { + throw new PersistenceException(reflectiveOperationException.getMessage(), reflectiveOperationException); + } finally { + returnValue = temp; + } + } + } else { + returnValue = existing; + if (LOGGER.isLoggable(Level.WARNING) && !isResourceLocal(info)) { + final Map properties = existing.getProperties(); + if (properties == null + || !Boolean.TRUE.equals(properties.get("io.helidon.integrations.cdi.jpa.weld.containerManaged"))) { + LOGGER.logp(Level.WARNING, cn, mn, + "transactionTypeMismatch", + new Object[] {name, returnValue}); + } + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Creates an {@link EntityManagerFactory} suitable for the + * supplied {@link PersistenceUnitInfo}, {@linkplain + * PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, + * Map) following the JPA 2.2 specification}. + * + *

This method returns a new {@link EntityManagerFactory} each + * time it is invoked.

+ * + *

This method never returns {@code null}.

+ * + * @param info the {@link PersistenceUnitInfo} describing the + * persistence unit; must not be {@code null}; should have an + * {@linkplain PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} equal to {@link + * javax.persistence.spi.PersistenceUnitTransactionType#JTA JTA} + * + * @return a new {@link EntityManagerFactory}; never {@code null} + * + * @exception NullPointerException if {@code info} is {@code null} + * + * @exception PersistenceException if a persistence-related error + * occurs + * + * @exception ReflectiveOperationException if a reflection-related + * error occurs + * + * @see + * PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, + * Map) + */ + private static EntityManagerFactory createContainerManagedEntityManagerFactory(final PersistenceUnitInfo info) + throws ReflectiveOperationException { + final String cn = WeldJpaInjectionServices.class.getName(); + final String mn = "createContainerManagedEntityManagerFactory"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, info); + } + + Objects.requireNonNull(info); + final PersistenceProvider persistenceProvider = getPersistenceProvider(info); + assert persistenceProvider != null; + final CDI cdi = CDI.current(); + assert cdi != null; + final BeanManager beanManager = cdi.getBeanManager(); + assert beanManager != null; + final Map properties = new HashMap<>(); + properties.put("io.helidon.integrations.cdi.jpa.weld.containerManaged", Boolean.TRUE); + properties.put("javax.persistence.bean.manager", beanManager); + Class validatorFactoryClass = null; + try { + validatorFactoryClass = Class.forName("javax.validation.ValidatorFactory"); + } catch (final ClassNotFoundException classNotFoundException) { + + } + if (validatorFactoryClass != null) { + final Bean vfb = getValidatorFactoryBean(beanManager, validatorFactoryClass); + if (vfb != null) { + final CreationalContext cc = beanManager.createCreationalContext(vfb); + properties.put("javax.persistence.validation.factory", beanManager.getReference(vfb, validatorFactoryClass, cc)); + } + } + final EntityManagerFactory returnValue = persistenceProvider.createContainerEntityManagerFactory(info, properties); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns a {@link Bean} that can {@linkplain + * Bean#create(CreationalContext) create} a {@link + * javax.validation.ValidatorFactory}, or {@code null} if no such + * {@link Bean} is available. + * + *

This method may return {@code null}.

+ * + * @param beanManager the {@link BeanManager} in effect; may be + * {@code null} in which case {@code null} will be returned + * + * @param validatorFactoryClass a {@link Class}; may be {@code + * null}; if not {@linkplain Class#getName() named} {@code + * javax.validation.ValidatorFactory} then {@code null} will be + * returned + * + * @return a {@link Bean} that can {@linkplain + * Bean#create(CreationalContext) create} a {@link + * javax.validation.ValidatorFactory}, or {@code null} + */ + private static Bean getValidatorFactoryBean(final BeanManager beanManager, + final Class validatorFactoryClass) { + Bean returnValue = null; + if (beanManager != null + && validatorFactoryClass != null + && "javax.validation.ValidatorFactory".equals(validatorFactoryClass.getName())) { + final Set> beans = beanManager.getBeans(validatorFactoryClass); + if (beans != null && !beans.isEmpty()) { + returnValue = beanManager.resolve(beans); + } + } + return returnValue; + } + + /** + * Returns {@code true} if and only if the supplied {@link + * PersistenceUnitInfo} is {@code null} or has an {@linkplain + * PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} equal to {@link + * javax.persistence.spi.PersistenceUnitTransactionType#RESOURCE_LOCAL + * RESOURCE_LOCAL}. + * + * @param persistenceUnitInfo the {@link PersistenceUnitInfo} to + * test; may be {@code null} in which case {@code true} will be + * returned + * + * @return {@code true} if and only if the supplied {@link + * PersistenceUnitInfo} is {@code null} or has an {@linkplain + * PersistenceUnitInfo#getTransactionType() affiliated + * PersistenceUnitTransactionType} equal to {@link + * javax.persistence.spi.PersistenceUnitTransactionType#RESOURCE_LOCAL + * RESOURCE_LOCAL} + */ + private static boolean isResourceLocal(final PersistenceUnitInfo persistenceUnitInfo) { + return persistenceUnitInfo == null || RESOURCE_LOCAL.equals(persistenceUnitInfo.getTransactionType()); + } + + + /* + * Inner and nested classes. + */ + + + private static final class EntityManagerFactoryResourceReference implements ResourceReference { + + private final Map emfs; + + private final String name; + + private final PersistenceUnitInfo persistenceUnitInfo; + + private EntityManagerFactoryResourceReference(final Map emfs, + final String name) { + super(); + final String cn = EntityManagerFactoryResourceReference.class.getName(); + final String mn = ""; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {emfs, name}); + } + + this.emfs = Objects.requireNonNull(emfs); + this.name = Objects.requireNonNull(name); + this.persistenceUnitInfo = getPersistenceUnitInfo(name); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + @Override + public EntityManagerFactory getInstance() { + final String cn = EntityManagerFactoryResourceReference.class.getName(); + final String mn = "getInstance"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + // See https://developer.jboss.org/message/984489#984489; + // there is no contract governing whether, for example, an + // EntityManagerFactory should be created from within + // ResourceReference#getInstance() or from within + // ResourceReferenceFactory#createResource(). The + // maintainers of Weld and CDI suggest following what + // Wildfly does, as it is most likely (!) to be correct. + // So that's what we do. + final EntityManagerFactory returnValue = computeEntityManagerFactory(emfs, this.persistenceUnitInfo, this.name); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + @Override + public void release() { + final String cn = EntityManagerFactoryResourceReference.class.getName(); + final String mn = "release"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final EntityManagerFactory emf = this.emfs.remove(this.name); + if (emf != null && emf.isOpen()) { + emf.close(); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + } + + private final class EntityManagerResourceReference implements ResourceReference { + + // @GuardedBy("this") + private EntityManager em; + + private final Future emfFuture; + + private final Supplier emSupplier; + + private EntityManagerResourceReference(final String name, + final SynchronizationType synchronizationType) { + super(); + final String cn = EntityManagerResourceReference.class.getName(); + final String mn = ""; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {name, synchronizationType}); + } + + Objects.requireNonNull(name); + Objects.requireNonNull(synchronizationType); + + // Kick off the lengthy process of setting up an + // EntityManagerFactory in the background with the + // optimistic assumption, possibly incorrect, that someone + // will call getInstance() at some point. + final ExecutorService taskExecutorService = + ((WeldManager) CDI.current().getBeanManager()).getServices().get(ExecutorServices.class).getTaskExecutor(); + assert taskExecutorService != null; + final PersistenceUnitInfo persistenceUnitInfo = getPersistenceUnitInfo(name); + this.emfFuture = + taskExecutorService.submit(() -> computeEntityManagerFactory(emfs, persistenceUnitInfo, name)); + + if (isResourceLocal(persistenceUnitInfo)) { + this.emSupplier = () -> { + try { + return emfFuture.get().createEntityManager(); + } catch (final ExecutionException executionException) { + final Throwable cause = executionException.getCause(); + assert cause != null; + throw new PersistenceException(cause.getMessage(), cause); + } catch (final InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RuntimeException(interruptedException.getMessage(), interruptedException); + } + }; + } else { + assert JTA.equals(persistenceUnitInfo.getTransactionType()); + this.emSupplier = () -> { + try { + final EntityManager em = emfFuture.get().createEntityManager(synchronizationType); + assert em != null; + em.setProperty(SynchronizationType.class.getName(), synchronizationType); + WeldJpaInjectionServices.this.containerManagedEntityManagers.add(em); + return em; + } catch (final ExecutionException executionException) { + final Throwable cause = executionException.getCause(); + assert cause != null; + throw new PersistenceException(cause.getMessage(), cause); + } catch (final InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RuntimeException(interruptedException.getMessage(), interruptedException); + } + }; + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + @Override + public EntityManager getInstance() { + final String cn = EntityManagerResourceReference.class.getName(); + final String mn = "getInstance"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final EntityManager em; + synchronized (this) { + // See + // https://developer.jboss.org/message/984489#984489; + // there is no contract governing whether, for + // example, an EntityManager should be created from + // within ResourceReference#getInstance() or from + // within ResourceReferenceFactory#createResource(). + // The maintainers of Weld and CDI suggest following + // what Wildfly does, as it is most likely (!) to be + // correct. So that's what we do. + // + // We also ensure that this + // EntityManagerResourceReference, no matter what, + // vends a non-null EntityManager whose isOpen() + // method returns true. + if (this.em == null || !this.em.isOpen()) { + this.em = this.emSupplier.get(); + } + em = this.em; + } + assert em != null; + assert em.isOpen(); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, em); + } + return em; + } + + @Override + public void release() { + final String cn = EntityManagerResourceReference.class.getName(); + final String mn = "release"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final EntityManager em; + synchronized (this) { + em = this.em; + this.em = null; + } + + if (em != null) { + WeldJpaInjectionServices.this.containerManagedEntityManagers.remove(em); + if (em.isOpen()) { + em.close(); + } + } + if (!this.emfFuture.isDone()) { + this.emfFuture.cancel(true); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + } + +} diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java new file mode 100644 index 00000000000..3ec49e349c7 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/WeldJpaInjectionServicesExtension.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa.weld; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Extension; +import javax.inject.Singleton; + +/** + * An {@link Extension} that exists solely to make the {@link + * WeldJpaInjectionServices} class become a bean in {@link Singleton} + * scope. + * + * @see WeldJpaInjectionServices + * + * @see TransactionObserver + */ +final class WeldJpaInjectionServicesExtension implements Extension { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} for use by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(WeldJpaInjectionServicesExtension.class.getName(), "messages"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link WeldJpaInjectionServicesExtension}. + * + *

Oddly, the fact that this constructor is {@code private} + * does not prevent Weld from loading it as a service. This is an + * unexpected bonus as nothing about this class should be {@code + * public}.

+ */ + private WeldJpaInjectionServicesExtension() { + super(); + final String cn = WeldJpaInjectionServicesExtension.class.getName(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, ""); + LOGGER.exiting(cn, ""); + } + } + + + /* + * Instance methods. + */ + + + /** + * Creates a bean deliberately in {@link Singleton} scope to + * represent the {@link WeldJpaInjectionServices} class. + * + *

Weld often creates multiple copies of {@link + * WeldJpaInjectionServices} by virtue of the way it loads its + * bootstrap services (see + * https://issues.jboss.org/browse/WELD-2563 for details). We + * want to ensure there's just one that can be injected into + * observer methods. See the {@link TransactionObserver} class, + * which houses one such observer method.

+ * + * @param event the {@link AfterBeanDiscovery} event; may be + * {@code null} in which case no action will be taken + * + * @see WeldJpaInjectionServices + * + * @see TransactionObserver + */ + private void afterBeanDiscovery(@Observes final AfterBeanDiscovery event) { + final String cn = WeldJpaInjectionServicesExtension.class.getName(); + final String mn = "afterBeanDiscovery"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, event); + } + + if (event != null) { + event.addBean() + .addTransitiveTypeClosure(WeldJpaInjectionServices.class) + .scope(Singleton.class) + .createWith(ignored -> { + return WeldJpaInjectionServices.getInstance(); + }); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java new file mode 100644 index 00000000000..b93d33e515b --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/java/io/helidon/integrations/cdi/jpa/weld/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides classes and interfaces that help to integrate JPA into Weld-based CDI 2.0 SE environments. + * + * @see + * io.helidon.integrations.cdi.jpa.weld.WeldJpaInjectionServices + */ +package io.helidon.integrations.cdi.jpa.weld; diff --git a/integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml b/integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..fe2feaf624d --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/META-INF/beans.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..aa9fb997467 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +io.helidon.integrations.cdi.jpa.weld.WeldJpaInjectionServicesExtension diff --git a/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service new file mode 100644 index 00000000000..ca8a18d0a1e --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +io.helidon.integrations.cdi.jpa.weld.WeldJpaInjectionServices diff --git a/integrations/cdi/jpa-weld/src/main/resources/messages.properties b/integrations/cdi/jpa-weld/src/main/resources/messages.properties new file mode 100644 index 00000000000..012fe3f670a --- /dev/null +++ b/integrations/cdi/jpa-weld/src/main/resources/messages.properties @@ -0,0 +1,25 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +transactionTypeMismatch=A resource-local EntityManagerFactory was requested for \ + the persistence unit named "{0}", but a JTA \ + EntityManagerFactory, {1}, was previously associated \ + with the persistence unit. +persistenceUnitNameMismatch=The sole PersistenceUnitInfo, {0}, representing \ + the persistence unit with name "{1}", will be used \ + instead of the requested persistence unit named \ + "{2}" +experimental=JPA support is currently experimental and not suitable for \ + production use. diff --git a/integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java b/integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java new file mode 100644 index 00000000000..38e58e7d87a --- /dev/null +++ b/integrations/cdi/jpa-weld/src/test/java/io/helidon/integrations/cdi/jpa/weld/TestIntegration.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.cdi.jpa.weld; + +import javax.annotation.sql.DataSourceDefinition; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ApplicationScoped +@DataSourceDefinition( + name = "test", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:test", + serverName = "", + properties = { + "user=sa" + } +) +public class TestIntegration { + + private SeContainer cdiContainer; + + @Inject + private Transaction transaction; + + @PersistenceContext + private EntityManager entityManager; + + TestIntegration() { + super(); + } + + @BeforeEach + void startCdiContainer() { + shutDownCdiContainer(); + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(TestIntegration.class); + assertNotNull(initializer); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + this.cdiContainer = null; + } + } + + private static void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event, + final TestIntegration self) + throws SystemException { + assertNotNull(event); + assertNotNull(self); + self.doSomethingTransactional(); + self.doSomethingNonTransactional(); + } + + @Transactional(Transactional.TxType.REQUIRED) + void doSomethingTransactional() throws SystemException { + assertNotNull(this.transaction); + assertEquals(Status.STATUS_ACTIVE, this.transaction.getStatus()); + assertNotNull(this.entityManager); + assertTrue(this.entityManager.isOpen()); + assertTrue(this.entityManager.isJoinedToTransaction()); + } + + void doSomethingNonTransactional() { + assertNotNull(this.transaction); // ...but the scope won't be active + try { + this.transaction.toString(); + fail("The TransactionScoped scope was active when it should not have been"); + } catch (final ContextNotActiveException expected) { + + } + assertNotNull(this.entityManager); + assertTrue(this.entityManager.isOpen()); + + } + + @Test + void testIntegration() { + } + +} diff --git a/integrations/cdi/jpa-weld/src/test/java/logging.properties b/integrations/cdi/jpa-weld/src/test/java/logging.properties new file mode 100644 index 00000000000..fc7b373c15f --- /dev/null +++ b/integrations/cdi/jpa-weld/src/test/java/logging.properties @@ -0,0 +1,24 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +.level=WARNING +com.arjuna.level=WARNING +com.zaxxer.hikari.level=WARNING +handlers=java.util.logging.ConsoleHandler +io.helidon.integrations.cdi.jpa.level=WARNING +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level=FINEST +org.eclipse.persistence.level=WARNING +org.jboss.weld.level=WARNING diff --git a/integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml b/integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml new file mode 100644 index 00000000000..afb0d0093f7 --- /dev/null +++ b/integrations/cdi/jpa-weld/src/test/resources/META-INF/persistence.xml @@ -0,0 +1,48 @@ + + + + + test + + + + + + + + + + + + + + + + diff --git a/integrations/cdi/jta-cdi/README.adoc b/integrations/cdi/jta-cdi/README.adoc new file mode 100644 index 00000000000..050ef87511c --- /dev/null +++ b/integrations/cdi/jta-cdi/README.adoc @@ -0,0 +1,23 @@ += Helidon JTA CDI Integration + +The Helidon JTA CDI Integration project performs the +CDI-provider-agnostic work of integrating a JTA implementation into standalone CDI +applications (including those based on Helidon MicroProfile). It is +one of two projects that together make up overall JTA support for +standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-specific counterpart, such as the `jta-weld` project + found elsewhere in this git repository + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JTA CDI Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file that uses this project. diff --git a/integrations/cdi/jta-cdi/pom.xml b/integrations/cdi/jta-cdi/pom.xml new file mode 100644 index 00000000000..6d7f0ebbd65 --- /dev/null +++ b/integrations/cdi/jta-cdi/pom.xml @@ -0,0 +1,109 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jta + Helidon CDI Integrations JTA + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.jboss.weld.se + weld-se-core + test + + + org.slf4j + slf4j-simple + test + + + + + org.jboss + jandex + runtime + + + org.jboss + jboss-transaction-spi + runtime + + + + + javax.annotation + javax.annotation-api + provided + + + javax.transaction + javax.transaction-api + provided + + + javax.enterprise + cdi-api + provided + + + + + org.jboss.narayana.jta + cdi + compile + + + + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + process-classes + + + + + + diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java new file mode 100644 index 00000000000..af1bba75d93 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionManager.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.cdi; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.InvalidTransactionException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.Status; // for javadoc only +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; + +/** + * An {@code abstract} {@link TransactionManager} implementation that + * delegates all method invocations to another {@link + * TransactionManager}. + * + *

Design Notes

+ * + *

This class is {@code public} for convenience. It is extended by + * other non-{@code public} internal classes.

+ * + * @see TransactionManager + */ +public abstract class DelegatingTransactionManager implements TransactionManager { + + private final TransactionManager delegate; + + /** + * Creates a new {@link DelegatingTransactionManager}. + * + * @param delegate the {@link TransactionManager} to which all + * method invocations will be delegated; may be {@code null}, but + * then a {@link SystemException} will be thrown by every method + * in this class when invoked + */ + protected DelegatingTransactionManager(final TransactionManager delegate) { + super(); + this.delegate = delegate; + } + + /** + * Creates a new transaction and associates it with the current + * thread. + * + * @exception NotSupportedException if the thread is already + * associated with a transaction and this {@link + * TransactionManager} implementation does not support nested + * transactions + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void begin() throws NotSupportedException, SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.begin(); + } + + /** + * Completes the transaction associated with the current thread. + * + *

When this method completes, the thread is no longer + * associated with a transaction.

+ * + * @exception RollbackException if the transaction has been rolled + * back rather than committed + * + * @exception HeuristicMixedException if a heuristic decision was + * made and that some relevant updates have been committed while + * others have been rolled back + * + * @exception HeuristicRollbackException if a heuristic decision + * was made and all relevant updates have been rolled back + * + * @exception SecurityException if the thread is not allowed to + * commit the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.commit(); + } + + /** + * Returns the status of the transaction associated with the + * current thread. + * + * @return the transaction status expressed as the value of one of + * the {@code int} constants in the {@link Status} class; if no + * transaction is associated with the current thread, this method + * returns {@link Status#STATUS_NO_TRANSACTION} + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see Status + */ + @Override + public int getStatus() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + return this.delegate.getStatus(); + } + + /** + * Returns the {@link Transaction} object that represents the + * transaction context of the calling thread. + * + *

This method never returns {@code null}.

+ * + * @return the {@link Transaction} object representing the + * transaction associated with the calling thread; never {@code + * null} + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public Transaction getTransaction() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + return this.delegate.getTransaction(); + } + + /** + * Resumes the transaction context association of the calling + * thread with the transaction represented by the supplied {@link + * Transaction} object. + * + *

When this method returns, the calling thread is associated + * with the transaction context specified.

+ * + * @param transaction the {@link Transaction} representing the + * transaction to be resumed; must not be {@code null} + * + * @exception InvalidTransactionException if {@code transaction} + * is invalid + * + * @exception IllegalStateException if the thread is already + * associated with another transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void resume(final Transaction transaction) throws InvalidTransactionException, SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.resume(transaction); + } + + /** + * Rolls back the transaction associated with the current thread. + * + *

When this method completes, the thread is no longer + * associated with a transaction.

+ * + * @exception SecurityException if the thread is not allowed to + * roll back the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void rollback() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.rollback(); + } + + /** + * Irrevocably modifies the transaction associated with the + * current thread such that the only possible outcome is for it to + * {@linkplain #rollback() roll back}. + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public void setRollbackOnly() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.setRollbackOnly(); + } + + /** + * Sets the timeout value that is associated with transactions + * started by the current thread with the {@link #begin()} method. + * + *

If an application has not called this method, the + * transaction service uses some default value for the transaction + * timeout.

+ * + * @param seconds the timeout in seconds; if the value is zero, + * the transaction service restores the default value; if the + * value is negative a {@link SystemException} is thrown + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition or if {@code seconds} + * is less than zero + */ + @Override + public void setTransactionTimeout(final int seconds) throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + this.delegate.setTransactionTimeout(seconds); + } + + /** + * Suspends the transaction currently associated with the calling + * thread and returns a {@link Transaction} that represents the + * transaction context being suspended, or {@code null} if the + * calling thread is not associated with a transaction. + * + *

This method may return {@code null}.

+ * + *

When this method returns, the calling thread is no longer + * associated with a transaction.

+ * + * @return a {@link Transaction} representing the suspended + * transaction, or {@code null} + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + */ + @Override + public Transaction suspend() throws SystemException { + if (this.delegate == null) { + throw new SystemException("delegate == null"); + } + return this.delegate.suspend(); + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java new file mode 100644 index 00000000000..526bd094ed7 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/DelegatingTransactionSynchronizationRegistry.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.cdi; + +import java.util.Map; // for javadoc only + +import javax.transaction.Status; // for javadoc only +import javax.transaction.Synchronization; +import javax.transaction.Transaction; // for javadoc only +import javax.transaction.TransactionManager; // for javadoc only +import javax.transaction.TransactionSynchronizationRegistry; + +/** + * An {@code abstract} {@link TransactionSynchronizationRegistry} + * implementation that delegates all method invocations to another + * {@link TransactionSynchronizationRegistry}. + * + *

Design Notes

+ * + *

This class is {@code public} for convenience. It is extended by + * other non-{@code public} internal classes.

+ * + * @see TransactionSynchronizationRegistry + */ +public abstract class DelegatingTransactionSynchronizationRegistry implements TransactionSynchronizationRegistry { + + private final TransactionSynchronizationRegistry delegate; + + /** + * Creates a new {@link + * DelegatingTransactionSynchronizationRegistry}. + * + * @param delegate the {@link TransactionSynchronizationRegistry} + * to which all method invocations will be delegated; may be + * {@code null} in which case every method in this class will + * throw an {@link IllegalStateException} when invoked + * + */ + protected DelegatingTransactionSynchronizationRegistry(final TransactionSynchronizationRegistry delegate) { + super(); + this.delegate = delegate; + } + + /** + * Return an opaque object to represent the transaction bound to + * the current thread at the time this method is called. + * + *

This method may return {@code null}.

+ * + *

This object overrides {@link Object#hashCode()} and {@link + * Object#equals(Object)} to allow its use as the key in a {@link + * Map} for use by the caller. If there is no transaction + * currently active, this method will return {@code null}.

+ * + *

The {@link Object} returned will return the same hashCode + * and compare equal to all other objects returned by calling this + * method from any component executing in the same transaction + * context in the same application server.

+ * + *

The {@link Object#toString()} method returns a {@link + * String} that might be usable by a human reader to usefully + * understand the transaction context. The {@link + * Object#toString()} result is otherwise not + * defined. Specifically, there is no forward or backward + * compatibility guarantee of the results of the returned {@link + * Object}'s {@link Object#toString()} override.

+ * + *

The object is not necessarily serializable, and has no + * defined behavior outside the virtual machine whence it was + * obtained.

+ * + * @return an opaque object representing the transaction bound to + * the current thread at the time this method is called, or {@code + * null} + * + * @exception IllegalStateException if a {@code null} {@code + * delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + */ + @Override + public Object getTransactionKey() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getTransactionKey(); + } + + /** + * Adds or replaces an object in the {@link Map} of resources + * being managed for the transaction bound to the current thread + * at the time this method is called. + * + *

The supplied key should be of an caller-defined class so as + * not to conflict with other users. The class of the key must + * guarantee that the {@link Object#hashCode() hashCode()} and + * {@link Object#equals(Object) equals(Object)} methods are + * suitable for use as keys in a {@link Map}. The key and value + * are not examined or used by the implementation. The general + * contract of this method is that of {@link Map#put(Object, + * Object)} for a {@link Map} that supports non-{@code null} keys + * and null values. For example, if there is already an value + * associated with the key, it is replaced by the {@code value} + * parameter.

+ * + * @param key the key for the {@link Map} entry; must not be + * {@code null} + * + * @param value the value for the {@link Map} entry + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @exception NullPointerException if the parameter {@code key} is + * {@code null} + */ + @Override + public void putResource(final Object key, final Object value) { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + this.delegate.putResource(key, value); + } + + /** + * Gets an object from the {@link Map} of resources being managed + * for the transaction bound to the current thread at the time + * this method is called. + * + *

The key should have been supplied earlier by a call to + * {@link #putResource(Object, Object)} in the same + * transaction. If the key cannot be found in the current resource + * {@link Map}, {@code null} is returned. The general contract of + * this method is that of {@link Map#get(Object)} for a {@link + * Map} that supports non-{@code null} keys and null values. For + * example, the returned value is null if there is no entry for + * the parameter {@code key} or if the value associated with the + * key is actually {@code null}.

+ * + * @param key the key for the {@link Map} entry + * + * @return the value associated with the supplied {@code key}; may + * be {@code null} + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @exception NullPointerException if the parameter {@code key} is + * {@code null} + */ + @Override + public Object getResource(final Object key) { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getResource(key); + } + + /** + * Registers a {@link Synchronization} instance with special + * ordering semantics. + * + *

The supplied {@link Synchronization}'s {@link + * Synchronization#beforeCompletion()} method will be called after + * all {@code SessionSynchronization#beforeCompletion()} callbacks + * and callbacks registered directly with the {@link Transaction}, + * but before the 2-phase commit process starts. Similarly, the + * {@link Synchronization#afterCompletion(int)} callback will be + * called after 2-phase commit completes but before any {@code + * SessionSynchronization} and {@link Transaction} {@code + * afterCompletion(int)} callbacks.

+ * + *

The {@link Synchronization#beforeCompletion()} callback will + * be invoked in the transaction context of the transaction bound + * to the current thread at the time this method is + * called. Allowable methods include access to resources, + * e.g. connectors. No access is allowed to "user components" + * (e.g. timer services or bean methods), as these might change + * the state of data being managed by the caller, and might change + * the state of data that has already been flushed by another + * caller of {@link + * #registerInterposedSynchronization(Synchronization)}. The + * general context is the component context of the caller of + * {@link + * #registerInterposedSynchronization(Synchronization)}.

+ * + *

The {@link Synchronization#afterCompletion(int)} callback + * will be invoked in an undefined context. No access is permitted + * to "user components" as defined above. Resources can be closed + * but no transactional work can be performed with them.

+ * + *

If this method is invoked without an active transaction + * context, an {@link IllegalStateException} is thrown.

+ * + *

If this method is invoked after the two-phase commit + * processing has started, an {@link IllegalStateException} is + * thrown.

+ * + * @param synchronization the {@link Synchronization} to register; + * must not be {@code null} + * + * @exception IllegalStateException if no transaction is active or + * two-phase commit processing has started or if a {@code null} + * {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @see Synchronization + * + * @see Synchronization#beforeCompletion() + * + * @see Synchronization#afterCompletion(int) + */ + @Override + public void registerInterposedSynchronization(final Synchronization synchronization) { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + this.delegate.registerInterposedSynchronization(synchronization); + } + + /** + * Return the status of the transaction bound to the current + * thread at the time this method is called. + * + *

This is the result of executing {@link + * TransactionManager#getStatus()} in the context of the + * transaction bound to the current thread at the time this method + * is called.

+ * + * @return the status of the transaction bound to the current + * thread at the time this method is called; will be equal the + * value of one of the constants defined in the {@link Status} + * class + * + * @exception IllegalStateException if a {@code null} {@code + * delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + * + * @see TransactionManager#getStatus() + * + * @see Status + */ + @Override + public int getTransactionStatus() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getTransactionStatus(); + } + + /** + * Sets the {@code rollbackOnly} status of the transaction bound + * to the current thread at the time this method is called. + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + */ + @Override + public void setRollbackOnly() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + this.delegate.setRollbackOnly(); + } + + /** + * Get the {@code rollbackOnly} status of the transaction bound to + * the current thread at the time this method is called. + * + * @return the {@code rollbackOnly} status + * + * @exception IllegalStateException if no transaction is active or + * if a {@code null} {@code delegate} was supplied at {@linkplain + * #DelegatingTransactionSynchronizationRegistry(TransactionSynchronizationRegistry) + * construction time} + */ + @Override + public boolean getRollbackOnly() { + if (this.delegate == null) { + throw new IllegalStateException("delegate == null"); + } + return this.delegate.getRollbackOnly(); + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java new file mode 100644 index 00000000000..3765aea85a9 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaExtension.java @@ -0,0 +1,234 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.cdi; + +import java.util.Collection; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Any; +import javax.enterprise.inject.CreationException; +import javax.enterprise.inject.Default; +import javax.enterprise.inject.spi.AfterBeanDiscovery; +import javax.enterprise.inject.spi.Bean; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; +import javax.enterprise.inject.spi.Extension; +import javax.inject.Singleton; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionScoped; +import javax.transaction.UserTransaction; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; +import com.arjuna.common.internal.util.propertyservice.BeanPopulator; + +import static javax.interceptor.Interceptor.Priority.LIBRARY_BEFORE; + +/** + * A CDI 2.0 + * portable extension that adapts the Narayana transaction engine to a CDI + * 2.0 SE environment. + */ +public final class NarayanaExtension implements Extension { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} for use by all instances of {@link + * NarayanaExtension}. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(NarayanaExtension.class.getName(), "messages"); + + /** + * The default {@link JTAEnvironmentBean} used throughout the + * Narayana transaction engine as configured via the {@code + * BeanPopulator} mechanism. + * + *

This field is never {@code null}.

+ */ + private static final JTAEnvironmentBean DEFAULT_JTA_ENVIRONMENT_BEAN = + BeanPopulator.getDefaultInstance(JTAEnvironmentBean.class); + + + /* + * Instance fields. + */ + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link NarayanaExtension}. + */ + public NarayanaExtension() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Adds a synthetic bean that creates a {@link Transaction} in + * {@linkplain TransactionScoped transaction scope}. + * + * @param event the {@link AfterBeanDiscovery} event fired by the + * CDI container; may be {@code null} in which case no action will + * be taken + * + * @param beanManager the {@link BeanManager} in effect; may be + * {@code null} in which case no action will be taken + */ + private void afterBeanDiscovery(@Observes final AfterBeanDiscovery event, final BeanManager beanManager) { + final String cn = NarayanaExtension.class.getName(); + final String mn = "afterBeanDiscovery"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, beanManager}); + } + + if (event != null && beanManager != null) { + + // Weld registers a UserTransaction bean well before this + // observer method fires. OpenWebBeans does not. We need + // to handle both cases since this is not part of the CDI + // specification. + Collection> beans = beanManager.getBeans(UserTransaction.class); + if (beans == null || beans.isEmpty()) { + event.addBean() + .types(UserTransaction.class) + // OpenWebBeans does not add these qualifiers; + // Weld does automatically: + .addQualifiers(Any.Literal.INSTANCE, Default.Literal.INSTANCE) + // see + // e.g. https://docs.oracle.com/javaee/6/tutorial/doc/gmgli.html + // which reads in part: "Predefined beans are + // injected with **dependent scope** [emphasis + // mine] and the predefined default + // qualifier @Default." This scope restriction is + // not specified in the CDI specification but + // seems reasonable and widely expected. + .scope(Dependent.class) + .createWith(cc -> com.arjuna.ats.jta.UserTransaction.userTransaction()); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "addedUserTransactionBean"); + } + } + + event.addBean() + .id(Transaction.class.getName()) + .types(Transaction.class) + .addQualifiers(Any.Literal.INSTANCE, Default.Literal.INSTANCE) // OpenWebBeans does not add these + .scope(TransactionScoped.class) + .createWith(cc -> { + try { + return CDI.current().select(TransactionManager.class).get().getTransaction(); + } catch (final SystemException systemException) { + throw new CreationException(systemException.getMessage(), systemException); + } + }); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "addedTransactionBean"); + } + + beans = beanManager.getBeans(JTAEnvironmentBean.class); + if (beans == null || beans.isEmpty()) { + event.addBean() + .addTransitiveTypeClosure(JTAEnvironmentBean.class) + // OpenWebBeans does not add these qualifiers; + // Weld does automatically: + .addQualifiers(Any.Literal.INSTANCE, Default.Literal.INSTANCE) + .scope(Singleton.class) + .createWith(cc -> DEFAULT_JTA_ENVIRONMENT_BEAN); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "addedJtaEnvironmentBeanBean"); + } + } + + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Observes the startup of the CDI container (by observing the + * {@linkplain Initialized initialization} of the {@linkplain + * ApplicationScoped application scope}) and reacts by {@linkplain + * Event#fire(Object) firing an event} consisting of the {@link + * JTAEnvironmentBean} singleton initialized by the Narayana + * transaction engine and preconfigured through its {@code + * BeanPopulator} proprietary mechanism. + * + *

This allows other portable extensions to further configure + * the default {@link JTAEnvironmentBean} in whatever manner they + * see fit.

+ * + * @param event the event representing the {@linkplain + * ApplicationScoped application scope} {@linkplain Initialized + * initialization}; may be {@code null}; ignored + * + * @param broadcaster an {@link Event} capable of {@linkplain + * Event#fire(Object) firing} a {@link JTAEnvironmentBean} + * + * @see JTAEnvironmentBean + */ + private static void onStartup(@Observes + @Initialized(ApplicationScoped.class) + @Priority(LIBRARY_BEFORE) + final Object event, + final Event broadcaster) { + final String cn = NarayanaExtension.class.getName(); + final String mn = "onStartup"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, new Object[] {event, broadcaster}); + } + if (broadcaster != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, cn, mn, "firingJtaEnvironmentBean", DEFAULT_JTA_ENVIRONMENT_BEAN); + } + broadcaster.fire(DEFAULT_JTA_ENVIRONMENT_BEAN); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java new file mode 100644 index 00000000000..0a06e76d3b1 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionManager.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.inject.Inject; +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; // for javadoc only +import javax.transaction.TransactionScoped; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; + +/** + * A {@link DelegatingTransactionManager} in {@linkplain + * ApplicationScoped application scope} that uses the return value + * that results from invoking the {@link + * JTAEnvironmentBean#getTransactionManager()} method as its backing + * implementation. + * + * @see com.arjuna.ats.jta.common.JTAEnvironmentBean#getTransactionManager() + */ +@ApplicationScoped +class NarayanaTransactionManager extends DelegatingTransactionManager { + + + /* + * Instance fields. + */ + + + /** + * An {@link Event} capable of {@linkplain Event#fire(Object) + * firing} a {@link Transaction} when {@linkplain + * TransactionScoped transaction scope} has begun. + * + *

This field may be {@code null}.

+ */ + private final Event transactionScopeInitializedBroadcaster; + + /** + * An {@link Event} capable of {@linkplain Event#fire(Object) + * firing} an {@link Object} when {@linkplain TransactionScoped + * transaction scope} has ended. + * + *

This field may be {@code null}.

+ */ + private final Event transactionScopeDestroyedBroadcaster; + + + /* + * Constructors. + */ + + + /** + * Creates a new, nonfunctional {@link + * NarayanaTransactionManager}. + * + *

This constructor exists only to conform with section + * 3.15 of the CDI specification.

+ * + * @deprecated This constructor exists only to conform with section + * 3.15 of the CDI specification. + * + * @see Section + * 3.15 of the CDI specification + */ + @Deprecated + NarayanaTransactionManager() { + this(null, null, null); + } + + /** + * Creates a new {@link NarayanaTransactionManager}. + * + * @param jtaEnvironmentBean a {@link JTAEnvironmentBean} used to + * acquire this {@link NarayanaTransactionManager}'s delegate; may + * be {@code null} but then a {@link SystemException} will be + * thrown by every method in this class when invoked + * + * @param transactionScopeInitializedBroadcaster an {@link Event} + * capable of {@linkplain Event#fire(Object) firing} {@link + * Transaction} instances; may be {@code null} + * + * @param transactionScopeDestroyedBroadcaster an {@link Event} + * capable of {@linkplain Event#fire(Object) firing} {@link + * Object} instances; may be {@code null} + * + * @see #begin() + * + * @see #commit() + * + * @see #rollback() + */ + @Inject + private NarayanaTransactionManager(final JTAEnvironmentBean jtaEnvironmentBean, + @Initialized(TransactionScoped.class) + final Event transactionScopeInitializedBroadcaster, + @Destroyed(TransactionScoped.class) + final Event transactionScopeDestroyedBroadcaster) { + super(jtaEnvironmentBean == null ? null : jtaEnvironmentBean.getTransactionManager()); + this.transactionScopeInitializedBroadcaster = transactionScopeInitializedBroadcaster; + this.transactionScopeDestroyedBroadcaster = transactionScopeDestroyedBroadcaster; + } + + + /* + * Instance methods. + */ + + + /** + * Overrides {@link DelegatingTransactionManager#begin()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link + * Object} representing the {@linkplain Initialized + * initialization} of the {@linkplain TransactionScoped + * transaction scope}. + * + * @exception NotSupportedException if the thread is already + * associated with a transaction and this {@link + * TransactionManager} implementation does not support nested + * transactions + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see DelegatingTransactionManager#begin() + * + * @see Event#fire(Object) + * + * @see Initialized + * + * @see TransactionScoped + */ + @Override + public void begin() throws NotSupportedException, SystemException { + super.begin(); + if (this.transactionScopeInitializedBroadcaster != null) { + this.transactionScopeInitializedBroadcaster.fire(this.getTransaction()); + } + } + + /** + * Overrides {@link DelegatingTransactionManager#commit()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link + * Object} representing the {@linkplain Destroyed destruction} of + * the {@linkplain TransactionScoped transaction scope}. + * + * @exception RollbackException if the transaction has been rolled + * back rather than committed + * + * @exception HeuristicMixedException if a heuristic decision was + * made and that some relevant updates have been committed while + * others have been rolled back + * + * @exception HeuristicRollbackException if a heuristic decision + * was made and all relevant updates have been rolled back + * + * @exception SecurityException if the thread is not allowed to + * commit the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see DelegatingTransactionManager#commit() + * + * @see Event#fire(Object) + * + * @see Destroyed + * + * @see TransactionScoped + */ + @Override + public void commit() throws HeuristicMixedException, HeuristicRollbackException, RollbackException, SystemException { + try { + super.commit(); + } finally { + if (this.transactionScopeDestroyedBroadcaster != null) { + this.transactionScopeDestroyedBroadcaster.fire(this.toString()); + } + } + } + + /** + * Overrides {@link DelegatingTransactionManager#rollback()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link + * Object} representing the {@linkplain Destroyed destruction} of + * the {@linkplain TransactionScoped transaction scope}. + * + * @exception SecurityException if the thread is not allowed to + * roll back the transaction + * + * @exception IllegalStateException if the current thread is not + * associated with a transaction + * + * @exception SystemException if this {@link TransactionManager} + * encounters an unexpected error condition + * + * @see DelegatingTransactionManager#rollback() + * + * @see Event#fire(Object) + * + * @see Destroyed + * + * @see TransactionScoped + */ + @Override + public void rollback() throws SystemException { + try { + super.rollback(); + } finally { + if (this.transactionScopeDestroyedBroadcaster != null) { + this.transactionScopeDestroyedBroadcaster.fire(this.toString()); + } + } + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java new file mode 100644 index 00000000000..da18bd6dafc --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/NarayanaTransactionSynchronizationRegistry.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; + +/** + * A {@link DelegatingTransactionSynchronizationRegistry} in + * {@linkplain ApplicationScoped application scope} that uses the + * return value that results from invoking the {@link + * JTAEnvironmentBean#getTransactionSynchronizationRegistry()} method + * as its backing implementation. + * + * @see JTAEnvironmentBean#getTransactionSynchronizationRegistry() + */ +@ApplicationScoped +class NarayanaTransactionSynchronizationRegistry extends DelegatingTransactionSynchronizationRegistry { + + /** + * Creates a new, nonfunctional {@link + * NarayanaTransactionSynchronizationRegistry}. + * + *

This constructor exists only to conform with section 3.15 of + * the CDI specification.

+ * + * @deprecated This constructor exists only to conform with + * section 3.15 of the CDI specification; please use the {@link + * #NarayanaTransactionSynchronizationRegistry(JTAEnvironmentBean)} + * constructor instead. + * + * @see + * #NarayanaTransactionSynchronizationRegistry(JTAEnvironmentBean) + * + * @see Section + * 3.15 of the CDI 2.0 specification + */ + @Deprecated + NarayanaTransactionSynchronizationRegistry() { + this(null); + } + + /** + * Creates a new {@link + * NarayanaTransactionSynchronizationRegistry}. + * + * @param jtaEnvironmentBean the {@link JTAEnvironmentBean} + * describing the environment in which transaction processing will + * take place + * + * @see JTAEnvironmentBean#getTransactionSynchronizationRegistry() + */ + @Inject + private NarayanaTransactionSynchronizationRegistry(final JTAEnvironmentBean jtaEnvironmentBean) { + super(jtaEnvironmentBean == null ? null : jtaEnvironmentBean.getTransactionSynchronizationRegistry()); + } + +} diff --git a/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java new file mode 100644 index 00000000000..94c152073fb --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/java/io/helidon/integrations/jta/cdi/package-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides classes and interfaces that integrate JTA version 1.2 + * into CDI + * version 2.0 using Narayana as the + * underlying implementation. + */ +package io.helidon.integrations.jta.cdi; diff --git a/integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml b/integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..70f5bd0fe91 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/META-INF/beans.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension b/integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension new file mode 100644 index 00000000000..b3534bb1ff7 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/META-INF/services/javax.enterprise.inject.spi.Extension @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +io.helidon.integrations.jta.cdi.NarayanaExtension diff --git a/integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml b/integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml new file mode 100644 index 00000000000..e9297874125 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/default-jbossts-properties.xml @@ -0,0 +1,153 @@ + + + + + + + + YES + + + PutObjectStoreDirHere + + + ON + + + 1 + + + 1 + + + com.arjuna.ats.internal.jta.recovery.arjunacore.JTATransactionLogXAResourceOrphanFilter + com.arjuna.ats.internal.jta.recovery.arjunacore.JTANodeNameXAResourceOrphanFilter + com.arjuna.ats.internal.jta.recovery.arjunacore.SubordinationManagerXAResourceOrphanFilter + + + + 0 + + + + com.arjuna.ats.internal.arjuna.recovery.AtomicActionRecoveryModule + com.arjuna.ats.internal.txoj.recovery.TORecoveryModule + com.arjuna.ats.internal.jta.recovery.arjunacore.SubordinateAtomicActionRecoveryModule + com.arjuna.ats.internal.jta.recovery.arjunacore.XARecoveryModule + + + + + com.arjuna.ats.internal.arjuna.recovery.ExpiredTransactionStatusManagerScanner + + + + + + 4712 + + + + + 0 + + + + + + NO + + diff --git a/integrations/cdi/jta-cdi/src/main/resources/messages.properties b/integrations/cdi/jta-cdi/src/main/resources/messages.properties new file mode 100644 index 00000000000..76dc0a59ee3 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/main/resources/messages.properties @@ -0,0 +1,19 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +addedJtaEnvironmentBeanBean=Added JTAEnvironmentBean bean +addedTransactionBean=Added Transaction bean +addedUserTransactionBean=Added UserTransaction bean +firingJtaEnvironmentBean=Firing {0} diff --git a/integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java b/integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java new file mode 100644 index 00000000000..fc2a4804c60 --- /dev/null +++ b/integrations/cdi/jta-cdi/src/test/java/io/helidon/integrations/jta/cdi/TestTransactionalAnnotationSupport.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.cdi; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; +import javax.inject.Inject; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.TransactionManager; +import javax.transaction.Transactional; +import javax.transaction.Transaction; +import javax.transaction.TransactionScoped; +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ApplicationScoped +public class TestTransactionalAnnotationSupport { + + private SeContainer cdiContainer; + + private boolean transactionScopeStarted; + + @Inject + private Transaction transaction; + + @Inject + private UserTransaction userTransaction; + + TestTransactionalAnnotationSupport() { + super(); + } + + @BeforeEach + void startCdiContainer() { + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(TestTransactionalAnnotationSupport.class); + assertNotNull(initializer); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + } + + private static void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event, + final TestTransactionalAnnotationSupport self) + throws SystemException { + assertNotNull(event); + assertNotNull(self); + self.doSomethingTransactional(); + } + + private void onBeginningOfTransactionScope(@Observes @Initialized(TransactionScoped.class) final Object event) { + assertTrue(event instanceof Transaction); + this.transactionScopeStarted = true; + } + + @Transactional(Transactional.TxType.REQUIRED) + void doSomethingTransactional() throws SystemException { + assertTrue(this.transactionScopeStarted); + assertNotNull(this.userTransaction); + assertEquals(Status.STATUS_ACTIVE, this.userTransaction.getStatus()); + assertNotNull(this.transaction); + assertEquals(Status.STATUS_ACTIVE, this.transaction.getStatus()); + } + + @Test + void testTransactionalAnnotationSupport() { + + } + +} diff --git a/integrations/cdi/jta-weld/README.adoc b/integrations/cdi/jta-weld/README.adoc new file mode 100644 index 00000000000..66906d522f4 --- /dev/null +++ b/integrations/cdi/jta-weld/README.adoc @@ -0,0 +1,23 @@ += Helidon JTA Weld Integration + +The Helidon JTA Weld Integration project performs the +CDI-provider-specific work of integrating a JTA implementation into +standalone CDI applications (including those based on Helidon +MicroProfile). It is one of two projects that together make up +overall JTA support for standalone CDI applications. + +To function properly, this project also requires: + +* a CDI-provider-agnostic counterpart, such as the `jta-cdi` project + found elsewhere in this git repository + +IMPORTANT: Please note that this feature is currently experimental and + not suitable for production use. + +== Installation + +Ensure that the Helidon JTA Weld Integration project and its runtime +dependencies are present on your application's runtime classpath. + +Please see the `examples/integrations/cdi/jpa` project found elsewhere +in this git repository for a working `pom.xml` file that uses this project. diff --git a/integrations/cdi/jta-weld/pom.xml b/integrations/cdi/jta-weld/pom.xml new file mode 100644 index 00000000000..a336f4cd3c5 --- /dev/null +++ b/integrations/cdi/jta-weld/pom.xml @@ -0,0 +1,104 @@ + + + + 4.0.0 + + io.helidon.integrations.cdi + helidon-integrations-cdi-project + 1.0.4-SNAPSHOT + + helidon-integrations-cdi-jta-weld + Helidon CDI Integrations JTA Weld + + + + package + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.hamcrest + hamcrest-all + test + + + org.jboss.weld.se + weld-se-core + test + + + org.slf4j + slf4j-simple + test + + + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta + ${project.version} + runtime + + + + org.jboss.weld.module + weld-jta + runtime + + + + + javax.transaction + javax.transaction-api + provided + + + javax.enterprise + cdi-api + provided + + + + + org.jboss.narayana.jta + cdi + compile + + + org.jboss.weld + weld-spi + compile + + + + diff --git a/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java new file mode 100644 index 00000000000..eb15b2e7df7 --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/NarayanaTransactionServices.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.weld; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.spi.CDI; +import javax.transaction.RollbackException; +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; +import org.jboss.weld.transaction.spi.TransactionServices; + +/** + * A {@link TransactionServices} implementation that uses the Narayana transaction + * engine and does not use JNDI. + * + *

{@link TransactionServices} implementations are used by Weld for transactional observer notification as well as + * for providing the implementation backing the built-in {@code UserTransaction} CDI bean.

+ * + * @see TransactionServices + */ +final class NarayanaTransactionServices implements TransactionServices { + + + /* + * Static fields. + */ + + + /** + * The {@link Logger} used by all instances of this class. + * + *

This field is never {@code null}.

+ */ + private static final Logger LOGGER = Logger.getLogger(NarayanaTransactionServices.class.getName(), "messages"); + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link NarayanaTransactionServices}. + */ + private NarayanaTransactionServices() { + super(); + final String cn = NarayanaTransactionServices.class.getName(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, ""); + LOGGER.exiting(cn, ""); + } + } + + + /* + * Instance methods. + */ + + + /** + * Returns the {@link UserTransaction} present in this environment + * by invoking the {@link + * com.arjuna.ats.jta.UserTransaction#userTransaction()} method + * and returning its result. + * + *

This method never returns {@code null}.

+ * + *

The return value of this method is used as the backing + * implementation of the built-in {@code UserTransaction} CDI + * bean.

+ * + * @return the non-{@code null} {@link UserTransaction} present in + * this environment + * + * @see com.arjuna.ats.jta.UserTransaction#userTransaction() + */ + @Override + public UserTransaction getUserTransaction() { + // We don't want to use, e.g., + // CDI.current().select(UserTransaction.class).get() here + // because CDI containers like Weld are obliged per the + // specification to automatically provide a bean for + // UserTransaction. Weld uses the return value of this method + // to create such a bean and we obviously need to avoid the + // infinite loop. + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "getUserTransaction"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final Instance jtaEnvironmentBeans = CDI.current().select(JTAEnvironmentBean.class); + assert jtaEnvironmentBeans != null; + final JTAEnvironmentBean jtaEnvironmentBean; + if (jtaEnvironmentBeans.isUnsatisfied()) { + jtaEnvironmentBean = com.arjuna.ats.jta.common.jtaPropertyManager.getJTAEnvironmentBean(); + } else { + jtaEnvironmentBean = jtaEnvironmentBeans.get(); + } + assert jtaEnvironmentBean != null; + final UserTransaction returnValue = jtaEnvironmentBean.getUserTransaction(); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, returnValue); + } + return returnValue; + } + + /** + * Returns {@code true} if the current {@link Transaction} + * {@linkplain Transaction#getStatus() has a status} indicating + * that it is active. + * + *

This method returns {@code true} if the current {@link + * Transaction} {@linkplain Transaction#getStatus() has a status} + * equal to one of the following values:

+ * + *
    + * + *
  • {@link Status#STATUS_ACTIVE}
  • + * + *
  • {@link Status#STATUS_COMMITTING}
  • + * + *
  • {@link Status#STATUS_MARKED_ROLLBACK}
  • + * + *
  • {@link Status#STATUS_PREPARED}
  • + * + *
  • {@link Status#STATUS_PREPARING}
  • + * + *
  • {@link Status#STATUS_ROLLING_BACK}
  • + * + *
+ * + * @return {@code true} if the current {@link Transaction} + * {@linkplain Transaction#getStatus() has a status} indicating + * that it is active; {@code false} otherwise + * + * @exception RuntimeException if an invocation of the {@link + * Transaction#getStatus()} method resulted in a {@link + * SystemException} + * + * @see Status + */ + @Override + public boolean isTransactionActive() { + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "isTransactionActive"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + } + + final boolean returnValue; + final Instance transactions = CDI.current().select(Transaction.class); + assert transactions != null; + if (!transactions.isUnsatisfied()) { + final Transaction transaction = transactions.get(); + assert transaction != null; + boolean temp = false; + try { + final int status = transaction.getStatus(); + temp = + status == Status.STATUS_ACTIVE + || status == Status.STATUS_COMMITTING + || status == Status.STATUS_MARKED_ROLLBACK + || status == Status.STATUS_PREPARED + || status == Status.STATUS_PREPARING + || status == Status.STATUS_ROLLING_BACK; + } catch (final SystemException e) { + throw new RuntimeException(e.getMessage(), e); + } finally { + returnValue = temp; + } + } else { + returnValue = false; + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn, Boolean.valueOf(returnValue)); + } + return returnValue; + } + + /** + * Registers the supplied {@link Synchronization} with the current + * {@link Transaction}. + * + * @exception RuntimeException if an invocation of the {@link + * TransactionManager#getTransaction()} method resulted in a + * {@link SystemException}, or if an invocation of the {@link + * Transaction#registerSynchronization(Synchronization)} method + * resulted in either a {@link SystemException} or a {@link + * RollbackException} + * + * @see Transaction#registerSynchronization(Synchronization) + */ + @Override + public void registerSynchronization(final Synchronization synchronization) { + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "registerSynchronization"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn, synchronization); + } + + final CDI cdi = CDI.current(); + final Instance transactionInstance = cdi.select(Transaction.class); + Transaction transaction = null; + if (transactionInstance.isUnsatisfied()) { + Instance transactionManagerInstance = cdi.select(TransactionManager.class); + assert transactionManagerInstance != null; + final TransactionManager transactionManager; + if (transactionManagerInstance.isUnsatisfied()) { + transactionManager = com.arjuna.ats.jta.TransactionManager.transactionManager(); + } else { + transactionManager = transactionManagerInstance.get(); + } + if (transactionManager != null) { + try { + transaction = transactionManager.getTransaction(); + } catch (final SystemException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + } else { + transaction = transactionInstance.get(); + } + if (transaction != null) { + try { + transaction.registerSynchronization(synchronization); + } catch (final SystemException | RollbackException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + } + + /** + * Releases any internal resources acquired during the lifespan of + * this object. + */ + @Override + public synchronized void cleanup() { + final String cn = NarayanaTransactionServices.class.getName(); + final String mn = "cleanup"; + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(cn, mn); + LOGGER.exiting(cn, mn); + } + } + +} diff --git a/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java new file mode 100644 index 00000000000..a7ff4eb8da5 --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/java/io/helidon/integrations/jta/weld/package-info.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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. + */ + +/** + * Provides classes and interfaces that enable transactional + * observer methods in Weld-backed CDI 2.0 + * SE implementations using the Narayana engine. + */ +package io.helidon.integrations.jta.weld; diff --git a/integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service b/integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service new file mode 100644 index 00000000000..be88c7afaba --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/resources/META-INF/services/org.jboss.weld.bootstrap.api.Service @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# +io.helidon.integrations.jta.weld.NarayanaTransactionServices diff --git a/integrations/cdi/jta-weld/src/main/resources/messages.properties b/integrations/cdi/jta-weld/src/main/resources/messages.properties new file mode 100644 index 00000000000..e4c231448fb --- /dev/null +++ b/integrations/cdi/jta-weld/src/main/resources/messages.properties @@ -0,0 +1,16 @@ +# +# Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. +# +# 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. +# + diff --git a/integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java b/integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java new file mode 100644 index 00000000000..451871f1b27 --- /dev/null +++ b/integrations/cdi/jta-weld/src/test/java/io/helidon/integrations/jta/weld/TestAutomaticUserTransactionInjection.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * 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 io.helidon.integrations.jta.weld; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.se.SeContainer; +import javax.enterprise.inject.se.SeContainerInitializer; +import javax.enterprise.inject.spi.CDI; +import javax.inject.Inject; +import javax.transaction.NotSupportedException; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import com.arjuna.ats.jta.common.JTAEnvironmentBean; + +import io.helidon.integrations.jta.cdi.NarayanaExtension; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +@ApplicationScoped +public class TestAutomaticUserTransactionInjection { + + private SeContainer cdiContainer; + + TestAutomaticUserTransactionInjection() { + super(); + } + + @BeforeEach + @SuppressWarnings("unchecked") + void startCdiContainer() { + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .disableDiscovery() + .addBeanClasses(TestAutomaticUserTransactionInjection.class) + .addExtensions(NarayanaExtension.class); + assertNotNull(initializer); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + } + + private void onJtaEnvironmentBeanLoad(@Observes final JTAEnvironmentBean instance) { + assertNotNull(instance); + } + + private void onStartup(@Observes @Initialized(ApplicationScoped.class) final Object event, + final UserTransaction userTransaction) + throws NotSupportedException, SystemException { + assertNotNull(userTransaction); + assertEquals("Transaction: unknown", userTransaction.toString()); + assertEquals(Status.STATUS_NO_TRANSACTION, userTransaction.getStatus()); + try { + userTransaction.begin(); + assertEquals(Status.STATUS_ACTIVE, userTransaction.getStatus()); + } finally { + userTransaction.rollback(); + assertEquals(Status.STATUS_NO_TRANSACTION, userTransaction.getStatus()); + } + } + + @Test + void testSpike() { + + } + +} diff --git a/integrations/cdi/pom.xml b/integrations/cdi/pom.xml index c7cb632c9b0..e7ac7d412d0 100644 --- a/integrations/cdi/pom.xml +++ b/integrations/cdi/pom.xml @@ -33,7 +33,12 @@ datasource-hikaricp + eclipselink-cdi jedis-cdi + jpa-cdi + jpa-weld + jta-cdi + jta-weld oci-objectstorage-cdi diff --git a/javadocs/pom.xml b/javadocs/pom.xml index 578f2dbcdae..ba294f519a0 100644 --- a/javadocs/pom.xml +++ b/javadocs/pom.xml @@ -356,6 +356,11 @@ helidon-integrations-cdi-datasource-hikaricp ${project.version} + + io.helidon.integrations.cdi + helidon-integrations-cdi-eclipselink + ${project.version} + io.helidon.integrations.cdi helidon-integrations-cdi-jedis @@ -366,6 +371,20 @@ helidon-integrations-cdi-oci-objectstorage ${project.version} + + javax.transaction + javax.transaction-api + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta + ${project.version} + + + io.helidon.integrations.cdi + helidon-integrations-cdi-jta-weld + ${project.version} + diff --git a/pom.xml b/pom.xml index b9bbaeaaa41..d5d822afae7 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ Oracle Corporation - Laird Jarrett Nelson + Laird Nelson laird.nelson@oracle.com Oracle Corporation @@ -86,6 +86,7 @@ + all UTF-8 UTF-8 @@ -111,6 +112,7 @@ 0.31.0 2.0 8.18 + 2.7.4 3.0.0 3.0.0 2.16.0 @@ -122,11 +124,16 @@ 1.4.197 1.3 2.7.8 + 1 2.9.8 0.34.0 + 2.2.2 2.0.4.Final 2.3.0 + 2.3.0.1 + 2.3.2 2.1 + 7.6.0.Final 2.9.0 2.26 4.9.0.201710071750-r @@ -144,10 +151,12 @@ 2.0 2.23.4 8.0.11 + 5.9.3.Final 4.1.34.Final 1.2.44 12.2.0.1 0.31.0 + 2.2 0.4.0 3.5.1 1.0.2 @@ -159,7 +168,9 @@ 1.24 3.1.3 6.13.1 + 1.2 1.3.3 + 2.0.1.Final 3.0.3.Final 2.6.0 1.5.18 @@ -187,6 +198,7 @@ 1.0.5 3.0.2 3.0.0 + 0.14.0 1.16 1.6.7 1.5.0.Final @@ -243,6 +255,7 @@ http://docs.jboss.org/cdi/api/${version.lib.cdi-api} + http://javax-inject.github.io/javax-inject/api/ https://fasterxml.github.io/jackson-annotations/javadoc/2.9/ https://fasterxml.github.io/jackson-core/javadoc/2.9/ https://fasterxml.github.io/jackson-databind/javadoc/2.9/ @@ -254,13 +267,20 @@ https://static.javadoc.io/io.opentracing/opentracing-api/${version.lib.opentracing} https://static.javadoc.io/io.prometheus/simpleclient/${version.lib.prometheus} https://static.javadoc.io/io.zipkin.reporter2/zipkin-reporter/${version.lib.zipkin} - https://static.javadoc.io/javax.json/javax.json-api/${version.lib.jsonp-api} https://static.javadoc.io/jakarta.json.bind/jakarta.json.bind-api/${version.lib.jsonb-api} + https://static.javadoc.io/jakarta.persistence/jakarta.persistence-api/${version.lib.jakarta-persistence-api} + https://static.javadoc.io/javax.json/javax.json-api/${version.lib.jsonp-api} + https://static.javadoc.io/javax.json.bind/javax.json.bind-api/${version.lib.jsonb-api} + https://static.javadoc.io/javax.persistence/javax.persistence-api/${version.lib.persistence-api} + https://static.javadoc.io/javax.transaction/javax.transaction-api/${version.lib.transaction-api} + https://static.javadoc.io/javax.xml.bind/jaxb-api/${version.lib.jaxb-api}/ https://static.javadoc.io/org.eclipse.microprofile.config/microprofile-config-api/${version.lib.microprofile-config-api} https://static.javadoc.io/org.eclipse.microprofile.health/microprofile-health-api/${version.lib.microprofile-health-api} https://static.javadoc.io/org.eclipse.microprofile.metrics/microprofile-metrics-api/${version.lib.microprofile-metrics-api} + https://docs.jboss.org/weld/javadoc/3.0/weld-spi/ + http://narayana.io/docs/api/ + https://www.eclipse.org/eclipselink/api/2.7/ - all -J-Dhttp.agent=maven-javadoc-plugin @@ -507,6 +527,11 @@ ${maven.deploy.skip} + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + ${version.plugin.jaxb} + org.codehaus.mojo buildnumber-maven-plugin @@ -645,6 +670,16 @@ jaxb-api ${version.lib.jaxb-api} + + com.sun.xml.bind + jaxb-core + ${version.lib.jaxb-core} + + + com.sun.xml.bind + jaxb-impl + ${version.lib.jaxb-impl} + javax.ws.rs javax.ws.rs-api @@ -665,6 +700,11 @@ javax.annotation-api ${version.lib.annotation-api} + + javax.inject + javax.inject + ${version.lib.inject} + javax.json javax.json-api @@ -863,6 +903,13 @@ + + org.jboss.weld + weld-core-bom + ${version.lib.weld} + pom + import + org.jboss.weld.se weld-se-core @@ -1005,6 +1052,26 @@ + + jakarta.persistence + jakarta.persistence-api + ${version.lib.jakarta-persistence-api} + + + javax.persistence + javax.persistence-api + ${version.lib.persistence-api} + + + javax.transaction + javax.transaction-api + ${version.lib.transaction-api} + + + javax.validation + validation-api + ${version.lib.validation-api} + com.h2database h2 @@ -1039,6 +1106,37 @@ jedis ${version.lib.jedis} + + org.eclipse.persistence + org.eclipse.persistence.jpa + ${version.lib.eclipselink} + + + org.jboss.narayana.jta + cdi + ${version.lib.narayana} + + + org.jboss.spec.javax.resource + jboss-connector-api_1.7_spec + + + sun.jdk + jconsole + + + + + org.jboss + jboss-transaction-spi + ${version.lib.jboss-transaction-spi} + + + org.jboss.spec.javax.resource + jboss-connector-api_1.7_spec + + + org.graalvm.sdk graal-sdk