From 7626254e2f63922d14fd7c57428ffa1aeabfd247 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Fri, 25 Aug 2023 10:55:57 -0700 Subject: [PATCH] 4.x: Forward ports all 3.x JDBC/JPA/JTA work (#7437) * Improves integrations/jdbc/jdbc to better support future JPA improvements; initial work (#5654) Signed-off-by: Laird Nelson * Squashable commit; initial work (#5716) Lays some groundwork with deprecation and cleanup and isolated improvements to support ongoing JPA improvements. Signed-off-by: Laird Nelson * Introduces LocalXAResource and a few support classes in jta/jdbc. (#5733) Signed-off-by: Laird Nelson * Adds connection unwrapping abilities to CDISEPlatform.java (#5790) Signed-off-by: Laird Nelson * Introduces JtaConnection.java (#5905) Signed-off-by: Laird Nelson * Fixes erroneous closing behavior in JtaConnection.java (#6321) * Fixes erroneous closing behavior in JtaConnection.java Signed-off-by: Laird Nelson * Minor JPA cleanups; part of overall refactoring effort (#6435) Signed-off-by: Laird Nelson * Improving JPA pom.xml as part of overall JPA refactoring (#6508) Signed-off-by: Laird Nelson * Fixes merge conflicts etc. from cherry-pick of c9a849e187ac262cb8a1215c427d4ae83aeb807c Signed-off-by: Laird Nelson * Adds an enabled flag to JpaExtension to permit subsequent refactoring and replacement (#6512) Adds an enabled flag to JpaExtension to permit subsequent refactoring and replacement Signed-off-by: Laird Nelson * Adds more classes as part of overall JPA refactoring effort (#6584) Signed-off-by: Laird Nelson * Lets unit tests validating JpaExtension and unit tests validating PersistenceExtension run side-by-side; continuation of overall fix for nested transaction problems (#7118) * Lets unit tests validating JpaExtension and unit tests validating PersistenceExtension run side-by-side; continuation of overall fix for nested transaction problems Signed-off-by: Laird Nelson * Resolves issue 7316, which features some intermittent database-related tests (#7317) Signed-off-by: Laird Nelson * Addresses copyright plugin complaints after lots of cherry-picking from old 3.x commits Signed-off-by: Laird Nelson --------- Signed-off-by: Laird Nelson --- dependencies/pom.xml | 22 + .../delegates/src/main/java/module-info.java | 6 +- .../ReferenceCounted.java | 5 +- .../ReferenceCountedContext.java | 5 +- .../ReferenceCountedExtension.java | 5 +- .../referencecountedcontext/package-info.java | 8 +- .../src/main/java/module-info.java | 6 +- .../ucp/cdi/UCPBackedDataSourceExtension.java | 160 +- integrations/cdi/eclipselink-cdi/pom.xml | 5 + .../cdi/eclipselink/CDISEPlatform.java | 30 +- .../src/main/java/module-info.java | 4 +- .../cdi/jpa-cdi/etc/spotbugs/exclude.xml | 28 +- integrations/cdi/jpa-cdi/pom.xml | 228 ++- .../jpa/AfterCompletionSynchronization.java | 47 + .../cdi/jpa/DelegatingEntityManager.java | 152 +- .../integrations/cdi/jpa/DelegatingQuery.java | 55 +- .../jpa/DelegatingStoredProcedureQuery.java | 74 +- .../cdi/jpa/DelegatingTypedQuery.java | 85 +- .../integrations/cdi/jpa/JpaExtension.java | 1193 +++++------- .../JpaTransactionScopedEntityManager.java | 9 +- .../cdi/jpa/JtaAbsentDataSourceProvider.java | 71 + .../jpa/JtaAdaptingDataSourceProvider.java | 194 ++ .../cdi/jpa/JtaEntityManager.java | 934 +++++++++ .../cdi/jpa/JtaExtendedEntityManager.java | 90 + .../cdi/jpa/JtaTransactionRegistry.java | 67 + .../cdi/jpa/JtaTransactionScoped.java | 61 + .../cdi/jpa/NoTransactionRegistry.java | 51 + .../cdi/jpa/PersistenceExtension.java | 1705 +++++++++++++++++ .../cdi/jpa/TransactionRegistry.java | 37 + .../jpa-cdi/src/main/java/module-info.java | 3 +- .../integrations/cdi/jpa/Messages.properties | 4 +- .../cdi/jpa/TestPersistenceExtension.java | 137 ++ .../integrations/cdi/jpa/chirp/Microblog.java | 3 +- .../integrations/cdi/jpa/chirp2/Author.java | 100 + .../integrations/cdi/jpa/chirp2/Chirp.java | 145 ++ .../cdi/jpa/chirp2/Microblog.java | 183 ++ .../cdi/jpa/chirp2/TestCascadePersist2.java | 293 +++ ...estExtendedSynchronizedEntityManager2.java | 235 +++ ...tExtendedUnsynchronizedEntityManager2.java | 240 +++ ...ctionScopedSynchronizedEntityManager2.java | 442 +++++ ...ionScopedUnsynchronizedEntityManager2.java | 237 +++ .../jpa/chirp2/TestRollbackScenarios2.java | 352 ++++ .../TestWithTransactionalInterceptors2.java | 322 ++++ .../cdi/jpa-cdi/src/test/logging.properties | 7 +- .../test/resources/META-INF/persistence.xml | 22 + .../cdi/jpa-cdi/src/test/resources/chirp2.ddl | 19 + .../jdbc/jdbc/etc/spotbugs/exclude.xml | 10 +- integrations/jdbc/jdbc/pom.xml | 26 +- .../jdbc/AbstractCommonDataSource.java | 13 +- .../integrations/jdbc/AbstractDataSource.java | 12 +- .../ConditionallyCloseableConnection.java | 918 ++++++++- .../jdbc/DelegatingCallableStatement.java | 808 ++++++++ .../jdbc/DelegatingConnection.java | 295 +-- .../jdbc/DelegatingDatabaseMetaData.java | 968 ++++++++++ .../jdbc/DelegatingPreparedStatement.java | 436 +++++ .../jdbc/DelegatingResultSet.java | 1349 +++++++++++++ .../jdbc/DelegatingStatement.java | 534 ++++++ .../integrations/jdbc/SQLBooleanSupplier.java | 37 + .../integrations/jdbc/SQLRunnable.java | 35 + .../integrations/jdbc/SQLSupplier.java | 39 + .../jdbc/UncheckedSQLException.java | 48 + .../jdbc/jdbc/src/main/java/module-info.java | 5 +- .../TestConditionallyCloseableConnection.java | 182 ++ .../jdbc/TestH2IsolationLevelCases.java | 126 ++ .../jta/jdbc/etc/spotbugs/exclude.xml | 35 + integrations/jta/jdbc/pom.xml | 68 +- .../jta/jdbc/ExceptionConverter.java | 204 ++ .../jta/jdbc/JtaAdaptingDataSource.java | 250 +++ .../integrations/jta/jdbc/JtaConnection.java | 1201 ++++++++++++ .../integrations/jta/jdbc/JtaDataSource.java | 449 ++--- .../jta/jdbc/LocalXAResource.java | 1031 ++++++++++ .../jta/jdbc/TransactionSupplier.java | 42 + .../jta/jdbc/UncheckedXAException.java | 33 + .../jdbc/XADataSourceWrappingDataSource.java | 88 +- .../integrations/jta/jdbc/package-info.java | 2 +- .../jta/jdbc/src/main/java/module-info.java | 4 +- .../integrations/jta/jdbc/NoOpXAResource.java | 153 ++ .../jta/jdbc/TestJtaConnection.java | 406 ++++ .../TestTransactionSpecificConnection.java | 165 ++ .../jta/jdbc/TestXAStartCommitEnd.java | 276 +++ .../src/test/resources/logging.properties | 23 + 81 files changed, 16788 insertions(+), 1564 deletions(-) create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/AfterCompletionSynchronization.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAbsentDataSourceProvider.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAdaptingDataSourceProvider.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaEntityManager.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaExtendedEntityManager.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionRegistry.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionScoped.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/NoTransactionRegistry.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java create mode 100644 integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/TransactionRegistry.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Author.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Chirp.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Microblog.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestCascadePersist2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedSynchronizedEntityManager2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedUnsynchronizedEntityManager2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedSynchronizedEntityManager2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedUnsynchronizedEntityManager2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestRollbackScenarios2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestWithTransactionalInterceptors2.java create mode 100644 integrations/cdi/jpa-cdi/src/test/resources/chirp2.ddl create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingCallableStatement.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingDatabaseMetaData.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingPreparedStatement.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingResultSet.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingStatement.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLBooleanSupplier.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLRunnable.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLSupplier.java create mode 100644 integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/UncheckedSQLException.java create mode 100644 integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestConditionallyCloseableConnection.java create mode 100644 integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestH2IsolationLevelCases.java create mode 100644 integrations/jta/jdbc/etc/spotbugs/exclude.xml create mode 100644 integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/ExceptionConverter.java create mode 100644 integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaAdaptingDataSource.java create mode 100644 integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaConnection.java create mode 100644 integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/LocalXAResource.java create mode 100644 integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/TransactionSupplier.java create mode 100644 integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/UncheckedXAException.java create mode 100644 integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/NoOpXAResource.java create mode 100644 integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestJtaConnection.java create mode 100644 integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestTransactionSpecificConnection.java create mode 100644 integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestXAStartCommitEnd.java create mode 100644 integrations/jta/jdbc/src/test/resources/logging.properties diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 51fcc55dc70..a306017911c 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -82,6 +82,7 @@ 2.0.0 3.0.0 2.1.0 + 4.0.0 3.1.2 4.0.3 @@ -285,21 +286,42 @@ opentracing-tracerresolver ${version.lib.opentracing.tracerresolver} + jakarta.xml.bind jakarta.xml.bind-api ${version.lib.jakarta.xml.bind-api} + com.sun.xml.bind jaxb-core ${version.lib.jaxb-core} + com.sun.xml.bind jaxb-impl ${version.lib.jaxb-impl} + + + org.glassfish.jaxb + jaxb-runtime + ${version.lib.jaxb-runtime} + jakarta.ws.rs jakarta.ws.rs-api diff --git a/integrations/cdi/common-cdi/delegates/src/main/java/module-info.java b/integrations/cdi/common-cdi/delegates/src/main/java/module-info.java index ba68a5919c5..33a3d6ed338 100644 --- a/integrations/cdi/common-cdi/delegates/src/main/java/module-info.java +++ b/integrations/cdi/common-cdi/delegates/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,8 +17,10 @@ /** * Provides classes and interfaces that wrap existing CDI constructs. */ +@SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.integrations.cdi.delegates { - requires jakarta.cdi; + + requires transitive jakarta.cdi; exports io.helidon.integrations.cdi.delegates; } diff --git a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCounted.java b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCounted.java index 4afcdf6db6b..041374b1a34 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCounted.java +++ b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCounted.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,10 @@ * notional request starting upon first instantiation.

* * @see ReferenceCountedContext + * + * @deprecated This annotation is slated for removal. */ +@Deprecated(forRemoval = true, since = "3.0.3") @Documented @NormalScope(passivating = false) @Retention(RetentionPolicy.RUNTIME) diff --git a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedContext.java b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedContext.java index 95176530944..0b42468b96b 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedContext.java +++ b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,10 @@ * @see #get(Contextual, CreationalContext) * * @see ReferenceCounted + * + * @deprecated This class is slated for removal. */ +@Deprecated(forRemoval = true, since = "3.0.3") public final class ReferenceCountedContext implements AlterableContext { diff --git a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedExtension.java b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedExtension.java index 970dc023360..8b34d155a05 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedExtension.java +++ b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/ReferenceCountedExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,10 @@ * @see ReferenceCountedContext * * @see ReferenceCounted + * + * @deprecated This class is slated for removal. */ +@Deprecated(forRemoval = true, since = "3.0.3") public class ReferenceCountedExtension implements Extension { diff --git a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/package-info.java b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/package-info.java index 727f65280f2..e1a6e882cf5 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/package-info.java +++ b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/io/helidon/integrations/cdi/referencecountedcontext/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,10 @@ * Provides classes and interfaces that support contextual reference * counting. * - * @see io.helidon.integrations.cdi.referencecountedcontext.ReferenceCountedExtension + * @see + * io.helidon.integrations.cdi.referencecountedcontext.ReferenceCountedExtension + * + * @deprecated This package is slated for removal with no replacement. */ +@Deprecated(forRemoval = true, since = "3.0.3") package io.helidon.integrations.cdi.referencecountedcontext; diff --git a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/module-info.java b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/module-info.java index 0fd58df2d7b..e7e4958c395 100644 --- a/integrations/cdi/common-cdi/reference-counted-context/src/main/java/module-info.java +++ b/integrations/cdi/common-cdi/reference-counted-context/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2021 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,11 @@ * * @see io.helidon.integrations.cdi.referencecountedcontext.ReferenceCountedExtension */ +@Deprecated(forRemoval = true, since = "3.0.3") +@SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.integrations.cdi.referencecountedcontext { requires io.helidon.integrations.cdi.delegates; - requires jakarta.cdi; + requires transitive jakarta.cdi; exports io.helidon.integrations.cdi.referencecountedcontext; diff --git a/integrations/cdi/datasource-ucp/src/main/java/io/helidon/integrations/datasource/ucp/cdi/UCPBackedDataSourceExtension.java b/integrations/cdi/datasource-ucp/src/main/java/io/helidon/integrations/datasource/ucp/cdi/UCPBackedDataSourceExtension.java index 298f2dd60d7..a2918203ede 100644 --- a/integrations/cdi/datasource-ucp/src/main/java/io/helidon/integrations/datasource/ucp/cdi/UCPBackedDataSourceExtension.java +++ b/integrations/cdi/datasource-ucp/src/main/java/io/helidon/integrations/datasource/ucp/cdi/UCPBackedDataSourceExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,10 +48,12 @@ import oracle.ucp.jdbc.PoolXADataSourceImpl; /** - * An {@link Extension} that arranges for named {@link DataSource} - * injection points to be satisfied by the Oracle - * Universal Connection Pool. + * An {@link Extension} that arranges for named {@link DataSource} injection points to be satisfied by the Oracle Universal Connection + * Pool. + * + *

In accordance with the CDI specification, instances of this class are not necessarily safe for concurrent use by + * multiple threads.

*/ public class UCPBackedDataSourceExtension extends AbstractDataSourceExtension { @@ -71,46 +73,33 @@ public UCPBackedDataSourceExtension() { } @Override - protected final Matcher getDataSourcePropertyPatternMatcher(final String configPropertyName) { - final Matcher returnValue; - if (configPropertyName == null) { - returnValue = null; - } else { - returnValue = DATASOURCE_NAME_PATTERN.matcher(configPropertyName); - } - return returnValue; + protected final Matcher getDataSourcePropertyPatternMatcher(String configPropertyName) { + return configPropertyName == null ? null : DATASOURCE_NAME_PATTERN.matcher(configPropertyName); } @Override - protected final String getDataSourceName(final Matcher dataSourcePropertyPatternMatcher) { - final String returnValue; + protected final String getDataSourceName(Matcher dataSourcePropertyPatternMatcher) { + String returnValue; if (dataSourcePropertyPatternMatcher == null) { returnValue = null; } else { returnValue = dataSourcePropertyPatternMatcher.group(2); - // While we have the Matcher available, store whether this - // is XA or not. + // While we have the Matcher available, store whether this is XA or not. this.xa.put(returnValue, dataSourcePropertyPatternMatcher.group(1) != null); } return returnValue; } @Override - protected final String getDataSourcePropertyName(final Matcher dataSourcePropertyPatternMatcher) { - final String returnValue; - if (dataSourcePropertyPatternMatcher == null) { - returnValue = null; - } else { - returnValue = dataSourcePropertyPatternMatcher.group(3); - } - return returnValue; + protected final String getDataSourcePropertyName(Matcher dataSourcePropertyPatternMatcher) { + return dataSourcePropertyPatternMatcher == null ? null : dataSourcePropertyPatternMatcher.group(3); } @Override - protected final void addBean(final BeanConfigurator beanConfigurator, - final Named dataSourceName, - final Properties dataSourceProperties) { - final boolean xa = this.xa.get(dataSourceName.value()); + protected final void addBean(BeanConfigurator beanConfigurator, + Named dataSourceName, + Properties dataSourceProperties) { + boolean xa = this.xa.get(dataSourceName.value()); beanConfigurator .addQualifier(dataSourceName) .addTransitiveTypeClosure(xa ? PoolXADataSourceImpl.class : PoolDataSourceImpl.class) @@ -118,54 +107,53 @@ protected final void addBean(final BeanConfigurator beanConfigurator .produceWith(instance -> { try { return createDataSource(instance, dataSourceName, xa, dataSourceProperties); - } catch (final IntrospectionException | ReflectiveOperationException | SQLException exception) { + } catch (IntrospectionException | ReflectiveOperationException | SQLException exception) { throw new CreationException(exception.getMessage(), exception); } }) .disposeWith((dataSource, ignored) -> { - if (dataSource instanceof AutoCloseable) { + if (dataSource instanceof AutoCloseable autoCloseable) { try { - ((AutoCloseable) dataSource).close(); - } catch (final RuntimeException runtimeException) { + autoCloseable.close(); + } catch (RuntimeException runtimeException) { throw runtimeException; - } catch (final Exception exception) { + } catch (Exception exception) { throw new CreationException(exception.getMessage(), exception); } } }); } - private static PoolDataSource createDataSource(final Instance instance, - final Named dataSourceName, - final boolean xa, - final Properties properties) + private static PoolDataSource createDataSource(Instance instance, + Named dataSourceName, + boolean xa, + Properties properties) throws IntrospectionException, ReflectiveOperationException, SQLException { // See // https://docs.oracle.com/en/database/oracle/oracle-database/19/jjucp/get-started.html#GUID-2CC8D6EC-483F-4942-88BA-C0A1A1B68226 // for the general pattern. - final PoolDataSource returnValue = + PoolDataSource returnValue = xa ? PoolDataSourceFactory.getPoolXADataSource() : PoolDataSourceFactory.getPoolDataSource(); - final Set propertyNames = properties.stringPropertyNames(); + Set propertyNames = properties.stringPropertyNames(); if (!propertyNames.isEmpty()) { - final Properties connectionFactoryProperties = new Properties(); - final BeanInfo beanInfo = Introspector.getBeanInfo(returnValue.getClass()); - final PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); - for (final String propertyName : propertyNames) { + Properties connectionFactoryProperties = new Properties(); + BeanInfo beanInfo = Introspector.getBeanInfo(returnValue.getClass()); + PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors(); + for (String propertyName : propertyNames) { if (propertyName != null) { boolean handled = false; - for (final PropertyDescriptor pd : pds) { + for (PropertyDescriptor pd : pds) { if (propertyName.equals(pd.getName())) { - // We have matched a Java Beans property on the PoolDataSource implementation - // class. Set it if we can. Note that these properties are NOT those of the - // PoolDataSource's *underlying* "real" connection factory (usually a - // DataSource that provides the actual connections ultimately pooled by the - // Universal Connection Pool). Those are handled in a manner unfortunately - // restricted by the limited configuration mechanism belonging to the - // PoolDataSource implementation itself via the connectionFactoryProperties - // object. See below. - final Method writeMethod = pd.getWriteMethod(); + // We have matched a Java Beans property on the PoolDataSource implementation class. Set it + // if we can. Note that these properties are NOT those of the PoolDataSource's *underlying* + // "real" connection factory (usually a DataSource that provides the actual connections + // ultimately pooled by the Universal Connection Pool). Those are handled in a manner + // unfortunately restricted by the limited configuration mechanism belonging to the + // PoolDataSource implementation itself via the connectionFactoryProperties object. See + // below. + Method writeMethod = pd.getWriteMethod(); if (writeMethod != null) { - final Class type = pd.getPropertyType(); + Class type = pd.getPropertyType(); if (type.equals(String.class)) { writeMethod.invoke(returnValue, properties.getProperty(propertyName)); handled = true; @@ -183,35 +171,33 @@ private static PoolDataSource createDataSource(final Instance instance, } } if (!handled) { - // We have found a property that is not a Java Beans property of the PoolDataSource, but - // is supposed to be a property of the connection factory that it wraps. + // We have found a property that is not a Java Beans property of the PoolDataSource, but is + // supposed to be a property of the connection factory that it wraps. // - // (Sadly, "serviceName" and "pdbRoles" are special properties that have significance - // to certain connection factories (such as Oracle database-oriented DataSources), and - // to the oracle.ucp.jdbc.UCPConnectionBuilder class, which underlies getConnection(user, - // password) calls, but which sadly cannot be set on a PoolDataSource except by means of - // some irrelevant XML configuration. We work around this design and special case it - // below, not here.) + // (Sadly, "serviceName" and "pdbRoles" are special properties that have significance to certain + // connection factories (such as Oracle database-oriented DataSources), and to the + // oracle.ucp.jdbc.UCPConnectionBuilder class, which underlies getConnection(user, password) + // calls, but which sadly cannot be set on a PoolDataSource except by means of some irrelevant + // XML configuration. We work around this design and special case it below, not here.) // // Sadly, the Universal Connection Pool lacks a mechanism to tunnel arbitrary Java - // Beans-conformant property values destined for the underlying connection factory - // (which is usually a DataSource or ConnectionPoolDataSource implementation, - // but may be other things) through to that underlying connection factory with - // arbitrary type information set properly. Because the PoolDataSource is in charge of - // instantiating the connection factory (the underlying DataSource), you can't pass a - // fully configured DataSource into it, nor can you access an unconfigured instance of - // it that you can work with. The only configuration the Universal Connection Pool - // supports is via a Properties object, whose values are retrieved by the PoolDataSource - // implementation, as Strings. This limits the kinds of underlying connection - // factories (DataSource implementations, usually) that can be fully configured with - // the Universal Connection Pool to Strings and those Strings which can be converted by - // the PoolDataSourceImpl#toBasicType(String, String) method. + // Beans-conformant property values destined for the underlying connection factory (which is + // usually a DataSource or ConnectionPoolDataSource implementation, but may be other things) + // through to that underlying connection factory with arbitrary type information set + // properly. Because the PoolDataSource is in charge of instantiating the connection factory + // (the underlying DataSource), you can't pass a fully configured DataSource into it, nor can + // you access an unconfigured instance of it that you can work with. The only configuration the + // Universal Connection Pool supports is via a Properties object, whose values are retrieved by + // the PoolDataSource implementation, as Strings. This limits the kinds of underlying + // connection factories (DataSource implementations, usually) that can be fully configured with + // the Universal Connection Pool to Strings and those Strings which can be converted by the + // PoolDataSourceImpl#toBasicType(String, String) method. connectionFactoryProperties.setProperty(propertyName, properties.getProperty(propertyName)); } } } - final Object serviceName = connectionFactoryProperties.remove("serviceName"); - final Object pdbRoles = connectionFactoryProperties.remove("pdbRoles"); + Object serviceName = connectionFactoryProperties.remove("serviceName"); + Object pdbRoles = connectionFactoryProperties.remove("pdbRoles"); // Used for OCI ATP Integration // Removing this so that it is not set on connectionFactoryProperties, // Else we get exception with getConnection using this DS, if its set. @@ -221,8 +207,8 @@ private static PoolDataSource createDataSource(final Instance instance, // hopefully fully configure it. Apply them here. returnValue.setConnectionFactoryProperties(connectionFactoryProperties); } - // Set the PoolDataSource's serviceName property so that it appears to the PoolDataSource to have been set via - // the undocumented XML configuration that the PoolDataSource can apparently be configured with in + // Set the PoolDataSource's serviceName property so that it appears to the PoolDataSource to have been set + // via the undocumented XML configuration that the PoolDataSource can apparently be configured with in // certain (irrelevant for Helidon) application server cases. if (serviceName instanceof String) { try { @@ -230,12 +216,12 @@ private static PoolDataSource createDataSource(final Instance instance, if (m.trySetAccessible()) { m.invoke(returnValue, serviceName); } - } catch (final NoSuchMethodException ignoreOnPurpose) { + } catch (NoSuchMethodException ignoreOnPurpose) { } } - // Set the PoolDataSource's pdbRoles property so that it appears to the PoolDataSource to have been set via the - // undocumented XML configuration that the PoolDataSource can apparently be configured with in certain + // Set the PoolDataSource's pdbRoles property so that it appears to the PoolDataSource to have been set via + // the undocumented XML configuration that the PoolDataSource can apparently be configured with in certain // (irrelevant for Helidon) application server cases. if (pdbRoles instanceof Properties) { try { @@ -243,24 +229,22 @@ private static PoolDataSource createDataSource(final Instance instance, if (m.trySetAccessible()) { m.invoke(returnValue, pdbRoles); } - } catch (final NoSuchMethodException ignoreOnPurpose) { + } catch (NoSuchMethodException ignoreOnPurpose) { } } } - final Instance sslContextInstance = instance.select(SSLContext.class, dataSourceName); + Instance sslContextInstance = instance.select(SSLContext.class, dataSourceName); if (!sslContextInstance.isUnsatisfied()) { returnValue.setSSLContext(sslContextInstance.get()); } // Permit further customization before the bean is actually created if (xa) { - instance.select(new TypeLiteral>() {}, - dataSourceName) + instance.select(new TypeLiteral>() {}, dataSourceName) .get() .fire((PoolXADataSource) returnValue); } else { - instance.select(new TypeLiteral>() {}, - dataSourceName) + instance.select(new TypeLiteral>() {}, dataSourceName) .get() .fire(returnValue); } diff --git a/integrations/cdi/eclipselink-cdi/pom.xml b/integrations/cdi/eclipselink-cdi/pom.xml index 4700f09d89e..b078f50bcb2 100644 --- a/integrations/cdi/eclipselink-cdi/pom.xml +++ b/integrations/cdi/eclipselink-cdi/pom.xml @@ -94,6 +94,11 @@ org.eclipse.persistence.jpa compile + + io.helidon.integrations.jdbc + helidon-integrations-jdbc + compile + 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 index a8707191b4b..358ee6ebe29 100644 --- 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 @@ -20,12 +20,15 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.sql.Connection; import java.util.Objects; import java.util.concurrent.Executor; import javax.management.MBeanServer; import javax.sql.DataSource; +import io.helidon.integrations.jdbc.DelegatingConnection; + import jakarta.enterprise.inject.InjectionException; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.literal.NamedLiteral; @@ -50,9 +53,8 @@ * *

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 + * eclipselink.target-server} Eclipselink JPA extension property in a {@code * META-INF/persistence.xml} file.

* @@ -276,6 +278,28 @@ public final int getJNDIConnectorLookupType() { return JNDIConnector.UNDEFINED_LOOKUP; } + /** + * Overrides the {@link ServerPlatformBase#unwrapConnection(Connection)} + * method to consider {@link DelegatingConnection}s. + * + *

By design, only one level of unwrapping occurs; i.e. if the unwrapped + * {@link Connection} is itself a {@link DelegatingConnection} it is + * returned as is.

+ * + * @param c the {@link Connection} to unwrap; must not be {@code null} + * + * @return the unwrapped {@link Connection} (which may be the supplied + * {@link Connection}); never {@code null} + * + * @exception NullPointerException if {@code c} is {@code null} + * + * @see ServerPlatformBase#unwrapConnection(Connection) + */ + @Override // ServerPlatformBase + public Connection unwrapConnection(Connection c) { + return c instanceof DelegatingConnection dc ? super.unwrapConnection(dc.delegate()) : super.unwrapConnection(c); + } + /* * Inner and nested classes. diff --git a/integrations/cdi/eclipselink-cdi/src/main/java/module-info.java b/integrations/cdi/eclipselink-cdi/src/main/java/module-info.java index 218922a1abe..b56ddd60541 100644 --- a/integrations/cdi/eclipselink-cdi/src/main/java/module-info.java +++ b/integrations/cdi/eclipselink-cdi/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2022 Oracle and/or its affiliates. + * Copyright (c) 2020, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +31,13 @@ path = {"JPA", "EclipseLink"} ) @Aot(false) +@SuppressWarnings("deprecation") module io.helidon.integrations.cdi.eclipselink { requires static io.helidon.common.features.api; requires java.management; + requires io.helidon.integrations.jdbc; requires jakarta.transaction; requires jakarta.cdi; requires jakarta.inject; diff --git a/integrations/cdi/jpa-cdi/etc/spotbugs/exclude.xml b/integrations/cdi/jpa-cdi/etc/spotbugs/exclude.xml index 4a970463898..0f1115ba6d9 100644 --- a/integrations/cdi/jpa-cdi/etc/spotbugs/exclude.xml +++ b/integrations/cdi/jpa-cdi/etc/spotbugs/exclude.xml @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/integrations/cdi/jpa-cdi/pom.xml b/integrations/cdi/jpa-cdi/pom.xml index b3dbc91c458..06d8b0bc487 100644 --- a/integrations/cdi/jpa-cdi/pom.xml +++ b/integrations/cdi/jpa-cdi/pom.xml @@ -31,17 +31,14 @@ -syntax + false + false etc/spotbugs/exclude.xml - - io.helidon.integrations.cdi - helidon-integrations-cdi-delegates - compile - io.helidon.integrations.cdi helidon-integrations-cdi-reference-counted-context @@ -52,13 +49,45 @@ helidon-integrations-jta-jdbc compile - - - jakarta.persistence - jakarta.persistence-api - provided + jakarta.xml.bind + jakarta.xml.bind-api + + compile + true + + jakarta.annotation jakarta.annotation-api @@ -69,6 +98,11 @@ jakarta.enterprise.cdi-api provided + + jakarta.persistence + jakarta.persistence-api + provided + jakarta.transaction jakarta.transaction-api @@ -82,9 +116,28 @@ runtime - jakarta.xml.bind - jakarta.xml.bind-api - compile + org.glassfish.jaxb + jaxb-runtime + + runtime com.sun.xml.bind @@ -94,8 +147,13 @@ - org.junit.jupiter - junit-jupiter-api + com.h2database + h2 + test + + + io.helidon.microprofile.cdi + helidon-microprofile-cdi test @@ -104,13 +162,13 @@ test - org.slf4j - slf4j-jdk14 + org.junit.jupiter + junit-jupiter-api test - com.h2database - h2 + org.slf4j + slf4j-jdk14 test @@ -133,11 +191,6 @@ helidon-integrations-cdi-hibernate test - - io.helidon.microprofile.cdi - helidon-microprofile-cdi - test - @@ -150,7 +203,7 @@ - + org.apache.maven.plugins maven-compiler-plugin @@ -164,18 +217,24 @@ ${version.lib.hibernate} + + -Xlint:-processing + - - - - io.helidon.common.features - helidon-common-features-processor - ${helidon.version} - - - + + + + io.helidon.common.features + helidon-common-features-processor + ${helidon.version} + + + + -Xlint:-path + + @@ -188,7 +247,7 @@ Generate persistence.xml Java objects - xjc + xjc ${project.build.directory}/generated-sources/xjc @@ -221,30 +280,22 @@ maven-surefire-plugin + + com.fasterxml:classmate io.helidon.integrations.cdi:helidon-integrations-cdi-hibernate + net.bytebuddy:byte-buddy + org.antlr:antlr4-runtime + org.hibernate.common:hibernate-commons-annotations org.hibernate.orm:hibernate-core eclipselink - true + ${eclipselink.skip} ${project.basedir}/src/test/logging.properties ${project.build.directory}/eclipselink/test-classes - - default-cli - - false - - - - default-test - - - true - - hibernate @@ -252,11 +303,15 @@ + io.helidon.integrations.cdi:helidon-integrations-cdi-eclipselink + org.eclipse.persistence:org.eclipse.persistence.asm + org.eclipse.persistence:org.eclipse.persistence.core org.eclipse.persistence:org.eclipse.persistence.jpa + org.eclipse.persistence:org.eclipse.persistence.jpa.jpql hibernate - false + ${hibernate.skip} ${project.build.directory}/hibernate/test-classes @@ -271,8 +326,8 @@ false - maven.test.skip - !true + maven.test.skip + !true @@ -285,47 +340,46 @@ * maven-resources-plugin * exec-maven-plugin * hibernate-enhance-maven-plugin - * maven-surefire-plugin --> org.apache.maven.plugins maven-resources-plugin - - Copy test classes to Eclipselink weaving area - - copy-resources - - process-test-classes - - - - ${project.build.testOutputDirectory} - false - - - ${project.build.directory}/eclipselink/test-classes - true - - - - Copy test classes to Hibernate weaving area - - copy-resources - - process-test-classes - - - - ${project.build.testOutputDirectory} - false - - - ${project.build.directory}/hibernate/test-classes - true - - + + Copy test classes to Eclipselink weaving area + + copy-resources + + process-test-classes + + + + ${project.build.testOutputDirectory} + false + + + ${project.build.directory}/eclipselink/test-classes + true + + + + Copy test classes to Hibernate weaving area + + copy-resources + + process-test-classes + + + + ${project.build.testOutputDirectory} + false + + + ${project.build.directory}/hibernate/test-classes + true + + diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/AfterCompletionSynchronization.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/AfterCompletionSynchronization.java new file mode 100644 index 00000000000..75846c28507 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/AfterCompletionSynchronization.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.Objects; +import java.util.function.IntConsumer; + +import jakarta.transaction.Synchronization; + +final class AfterCompletionSynchronization implements IntConsumer, Synchronization { + + private final IntConsumer afterCompletion; + + AfterCompletionSynchronization(IntConsumer afterCompletion) { + super(); + this.afterCompletion = Objects.requireNonNull(afterCompletion, "afterCompletion"); + } + + @Override + public void beforeCompletion() { + + } + + @Override + public void afterCompletion(int completedTransactionStatus) { + this.accept(completedTransactionStatus); + } + + @Override + public void accept(int completedTransactionStatus) { + this.afterCompletion.accept(completedTransactionStatus); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingEntityManager.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingEntityManager.java index af20d071626..130f20aaa87 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingEntityManager.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingEntityManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; +import java.util.function.Supplier; import jakarta.persistence.EntityGraph; import jakarta.persistence.EntityManager; @@ -24,6 +25,7 @@ import jakarta.persistence.EntityTransaction; import jakarta.persistence.FlushModeType; import jakarta.persistence.LockModeType; +import jakarta.persistence.PersistenceException; import jakarta.persistence.Query; import jakarta.persistence.StoredProcedureQuery; import jakarta.persistence.TypedQuery; @@ -50,19 +52,7 @@ abstract class DelegatingEntityManager implements EntityManager, AutoCloseable { */ - /** - * The {@link EntityManager} to which all operations will be - * forwarded if it is non-{@code null}. - * - *

This field may be {@code null}.

- * - * @see #DelegatingEntityManager(EntityManager) - * - * @see #delegate() - * - * @see #acquireDelegate() - */ - private final EntityManager delegate; + private final Supplier supplier; /* @@ -80,7 +70,7 @@ abstract class DelegatingEntityManager implements EntityManager, AutoCloseable { * @see #acquireDelegate() */ DelegatingEntityManager() { - this(null); + this((Supplier) null); } /** @@ -96,9 +86,13 @@ abstract class DelegatingEntityManager implements EntityManager, AutoCloseable { * * @see #acquireDelegate() */ - DelegatingEntityManager(final EntityManager delegate) { + DelegatingEntityManager(EntityManager delegate) { + this(delegate == null ? (Supplier) null : () -> delegate); + } + + DelegatingEntityManager(Supplier supplier) { super(); - this.delegate = delegate; + this.supplier = supplier == null ? this::acquireDelegate : supplier; } @@ -118,31 +112,23 @@ abstract class DelegatingEntityManager implements EntityManager, AutoCloseable { * {@linkplain #DelegatingEntityManager(EntityManager) supplied at * construction time}.

* - * @return an {@link EntityManager}; never {@code null} + * @return an {@link EntityManager} * - * @exception jakarta.persistence.PersistenceException if an error - * occurs + * @exception PersistenceException if an error occurs * * @see #acquireDelegate() * * @see #DelegatingEntityManager(EntityManager) */ - protected EntityManager delegate() { - final EntityManager returnValue; - if (this.delegate == null) { - returnValue = this.acquireDelegate(); - } else { - returnValue = this.delegate; - } - return returnValue; + EntityManager delegate() { + return this.supplier.get(); } /** * Returns an {@link EntityManager} to which all operations will * be forwarded. * - *

Implementations of this method must not return {@code - * null}.

+ *

Overrides of this method must not return {@code null}.

* *

This method is called by the {@link #delegate()} method and * potentially on every method invocation of instances of this @@ -154,55 +140,56 @@ protected EntityManager delegate() { * * @return a non-{@code null} {@link EntityManager} * - * @exception jakarta.persistence.PersistenceException if an error - * occurs + * @exception PersistenceException if an error occurs * * @see #delegate() * * @see #DelegatingEntityManager(EntityManager) */ - protected abstract EntityManager acquireDelegate(); + EntityManager acquireDelegate() { + throw new PersistenceException(); + } @Override - public void persist(final Object entity) { + public void persist(Object entity) { this.delegate().persist(entity); } @Override - public T merge(final T entity) { + public T merge(T entity) { return this.delegate().merge(entity); } @Override - public void remove(final Object entity) { + public void remove(Object entity) { this.delegate().remove(entity); } @Override - public T find(final Class entityClass, final Object primaryKey) { + public T find(Class entityClass, Object primaryKey) { return this.delegate().find(entityClass, primaryKey); } @Override - public T find(final Class entityClass, final Object primaryKey, final Map properties) { + public T find(Class entityClass, Object primaryKey, Map properties) { return this.delegate().find(entityClass, primaryKey, properties); } @Override - public T find(final Class entityClass, final Object primaryKey, final LockModeType lockMode) { + public T find(Class entityClass, Object primaryKey, LockModeType lockMode) { return this.delegate().find(entityClass, primaryKey, lockMode); } @Override - public T find(final Class entityClass, - final Object primaryKey, - final LockModeType lockMode, - final Map properties) { + public T find(Class entityClass, + Object primaryKey, + LockModeType lockMode, + Map properties) { return this.delegate().find(entityClass, primaryKey, lockMode, properties); } @Override - public T getReference(final Class entityClass, final Object primaryKey) { + public T getReference(Class entityClass, Object primaryKey) { return this.delegate().getReference(entityClass, primaryKey); } @@ -212,7 +199,7 @@ public void flush() { } @Override - public void setFlushMode(final FlushModeType flushMode) { + public void setFlushMode(FlushModeType flushMode) { this.delegate().setFlushMode(flushMode); } @@ -222,39 +209,39 @@ public FlushModeType getFlushMode() { } @Override - public void lock(final Object entity, - final LockModeType lockMode) { + public void lock(Object entity, + LockModeType lockMode) { this.delegate().lock(entity, lockMode); } @Override - public void lock(final Object entity, - final LockModeType lockMode, - final Map properties) { + public void lock(Object entity, + LockModeType lockMode, + Map properties) { this.delegate().lock(entity, lockMode, properties); } @Override - public void refresh(final Object entity) { + public void refresh(Object entity) { this.delegate().refresh(entity); } @Override - public void refresh(final Object entity, - final Map properties) { + public void refresh(Object entity, + Map properties) { this.delegate().refresh(entity, properties); } @Override - public void refresh(final Object entity, - final LockModeType lockMode) { + public void refresh(Object entity, + LockModeType lockMode) { this.delegate().refresh(entity, lockMode); } @Override - public void refresh(final Object entity, - final LockModeType lockMode, - final Map properties) { + public void refresh(Object entity, + LockModeType lockMode, + Map properties) { this.delegate().refresh(entity, lockMode, properties); } @@ -264,22 +251,22 @@ public void clear() { } @Override - public void detach(final Object entity) { + public void detach(Object entity) { this.delegate().detach(entity); } @Override - public boolean contains(final Object entity) { + public boolean contains(Object entity) { return this.delegate().contains(entity); } @Override - public LockModeType getLockMode(final Object entity) { + public LockModeType getLockMode(Object entity) { return this.delegate().getLockMode(entity); } @Override - public void setProperty(final String propertyName, final Object propertyValue) { + public void setProperty(String propertyName, Object propertyValue) { this.delegate().setProperty(propertyName, propertyValue); } @@ -289,76 +276,76 @@ public Map getProperties() { } @Override - public Query createQuery(final String qlString) { + public Query createQuery(String qlString) { return this.delegate().createQuery(qlString); } @Override - public TypedQuery createQuery(final CriteriaQuery criteriaQuery) { + public TypedQuery createQuery(CriteriaQuery criteriaQuery) { return this.delegate().createQuery(criteriaQuery); } @Override @SuppressWarnings("rawtypes") - public Query createQuery(final CriteriaUpdate criteriaUpdate) { + public Query createQuery(CriteriaUpdate criteriaUpdate) { return this.delegate().createQuery(criteriaUpdate); } @Override @SuppressWarnings("rawtypes") - public Query createQuery(final CriteriaDelete criteriaDelete) { + public Query createQuery(CriteriaDelete criteriaDelete) { return this.delegate().createQuery(criteriaDelete); } @Override - public TypedQuery createQuery(final String qlString, final Class resultClass) { + public TypedQuery createQuery(String qlString, Class resultClass) { return this.delegate().createQuery(qlString, resultClass); } @Override - public Query createNamedQuery(final String sqlString) { + public Query createNamedQuery(String sqlString) { return this.delegate().createNamedQuery(sqlString); } @Override - public TypedQuery createNamedQuery(final String sqlString, final Class resultClass) { + public TypedQuery createNamedQuery(String sqlString, Class resultClass) { return this.delegate().createNamedQuery(sqlString, resultClass); } @Override - public Query createNativeQuery(final String sqlString) { + public Query createNativeQuery(String sqlString) { return this.delegate().createNativeQuery(sqlString); } @Override @SuppressWarnings("rawtypes") - public Query createNativeQuery(final String sqlString, final Class resultClass) { + public Query createNativeQuery(String sqlString, Class resultClass) { return this.delegate().createNativeQuery(sqlString, resultClass); } @Override - public Query createNativeQuery(final String sqlString, final String resultSetMapping) { + public Query createNativeQuery(String sqlString, String resultSetMapping) { return this.delegate().createNativeQuery(sqlString, resultSetMapping); } @Override - public StoredProcedureQuery createNamedStoredProcedureQuery(final String procedureName) { + public StoredProcedureQuery createNamedStoredProcedureQuery(String procedureName) { return this.delegate().createNamedStoredProcedureQuery(procedureName); } @Override - public StoredProcedureQuery createStoredProcedureQuery(final String procedureName) { + public StoredProcedureQuery createStoredProcedureQuery(String procedureName) { return this.delegate().createStoredProcedureQuery(procedureName); } @Override @SuppressWarnings("rawtypes") - public StoredProcedureQuery createStoredProcedureQuery(final String procedureName, final Class... resultClasses) { + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class... resultClasses) { return this.delegate().createStoredProcedureQuery(procedureName, resultClasses); } @Override - public StoredProcedureQuery createStoredProcedureQuery(final String procedureName, final String... resultSetMappings) { + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings) { return this.delegate().createStoredProcedureQuery(procedureName, resultSetMappings); } @@ -373,7 +360,10 @@ public boolean isJoinedToTransaction() { } @Override - public T unwrap(final Class c) { + public T unwrap(Class c) { + if (c != null && c.isInstance(this)) { + return c.cast(this); + } return this.delegate().unwrap(c); } @@ -413,22 +403,22 @@ public Metamodel getMetamodel() { } @Override - public EntityGraph createEntityGraph(final Class rootType) { + public EntityGraph createEntityGraph(Class rootType) { return this.delegate().createEntityGraph(rootType); } @Override - public EntityGraph createEntityGraph(final String graphName) { + public EntityGraph createEntityGraph(String graphName) { return this.delegate().createEntityGraph(graphName); } @Override - public EntityGraph getEntityGraph(final String graphName) { + public EntityGraph getEntityGraph(String graphName) { return this.delegate().getEntityGraph(graphName); } @Override - public List> getEntityGraphs(final Class entityClass) { + public List> getEntityGraphs(Class entityClass) { return this.delegate().getEntityGraphs(entityClass); } diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingQuery.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingQuery.java index abe5c506927..492611ae4a8 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingQuery.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,11 +28,11 @@ import jakarta.persistence.Query; import jakarta.persistence.TemporalType; -abstract class DelegatingQuery implements Query { +class DelegatingQuery implements Query { private final Query delegate; - DelegatingQuery(final Query delegate) { + DelegatingQuery(Query delegate) { super(); this.delegate = Objects.requireNonNull(delegate); } @@ -57,7 +57,7 @@ public int getMaxResults() { } @Override - public Query setMaxResults(final int maxResults) { + public Query setMaxResults(int maxResults) { this.delegate.setMaxResults(maxResults); return this; } @@ -69,7 +69,7 @@ public int getFirstResult() { } @Override - public Query setFirstResult(final int firstResult) { + public Query setFirstResult(int firstResult) { this.delegate.setFirstResult(firstResult); return this; } @@ -82,29 +82,29 @@ public Map getHints() { @Override - public Query setHint(final String hintName, final Object value) { + public Query setHint(String hintName, Object value) { this.delegate.setHint(hintName, value); return this; } @Override - public Parameter getParameter(final int position) { + public Parameter getParameter(int position) { return this.delegate.getParameter(position); } @Override - public Parameter getParameter(final int position, final Class type) { + public Parameter getParameter(int position, Class type) { return this.delegate.getParameter(position, type); } @Override - public Parameter getParameter(final String name) { + public Parameter getParameter(String name) { return this.delegate.getParameter(name); } @Override - public Parameter getParameter(final String name, final Class type) { + public Parameter getParameter(String name, Class type) { return this.delegate.getParameter(name, type); } @@ -116,80 +116,80 @@ public Set> getParameters() { @Override - public T getParameterValue(final Parameter parameter) { + public T getParameterValue(Parameter parameter) { return this.delegate.getParameterValue(parameter); } @Override - public Object getParameterValue(final int position) { + public Object getParameterValue(int position) { return this.delegate.getParameterValue(position); } @Override - public Object getParameterValue(final String name) { + public Object getParameterValue(String name) { return this.delegate.getParameterValue(name); } @Override - public boolean isBound(final Parameter parameter) { + public boolean isBound(Parameter parameter) { return this.delegate.isBound(parameter); } @Override - public Query setParameter(final Parameter parameter, final T value) { + public Query setParameter(Parameter parameter, T value) { this.delegate.setParameter(parameter, value); return this; } @Override - public Query setParameter(final Parameter parameter, final Calendar value, final TemporalType temporalType) { + public Query setParameter(Parameter parameter, Calendar value, TemporalType temporalType) { this.delegate.setParameter(parameter, value, temporalType); return this; } @Override - public Query setParameter(final Parameter parameter, final Date value, final TemporalType temporalType) { + public Query setParameter(Parameter parameter, Date value, TemporalType temporalType) { this.delegate.setParameter(parameter, value, temporalType); return this; } @Override - public Query setParameter(final int position, final Calendar value, final TemporalType temporalType) { + public Query setParameter(int position, Calendar value, TemporalType temporalType) { this.delegate.setParameter(position, value, temporalType); return this; } @Override - public Query setParameter(final String name, final Calendar value, final TemporalType temporalType) { + public Query setParameter(String name, Calendar value, TemporalType temporalType) { this.delegate.setParameter(name, value, temporalType); return this; } @Override - public Query setParameter(final int position, final Date value, final TemporalType temporalType) { + public Query setParameter(int position, Date value, TemporalType temporalType) { this.delegate.setParameter(position, value, temporalType); return this; } @Override - public Query setParameter(final String name, final Date value, final TemporalType temporalType) { + public Query setParameter(String name, Date value, TemporalType temporalType) { this.delegate.setParameter(name, value, temporalType); return this; } @Override - public Query setParameter(final int position, final Object value) { + public Query setParameter(int position, Object value) { this.delegate.setParameter(position, value); return this; } @Override - public Query setParameter(final String name, final Object value) { + public Query setParameter(String name, Object value) { this.delegate.setParameter(name, value); return this; } @@ -201,7 +201,7 @@ public FlushModeType getFlushMode() { } @Override - public Query setFlushMode(final FlushModeType flushMode) { + public Query setFlushMode(FlushModeType flushMode) { this.delegate.setFlushMode(flushMode); return this; } @@ -213,7 +213,7 @@ public LockModeType getLockMode() { } @Override - public Query setLockMode(final LockModeType lockMode) { + public Query setLockMode(LockModeType lockMode) { this.delegate.setLockMode(lockMode); return this; } @@ -226,7 +226,10 @@ public int executeUpdate() { @Override - public T unwrap(final Class cls) { + public T unwrap(Class cls) { + if (cls != null && cls.isInstance(this)) { + return cls.cast(this); + } return this.delegate.unwrap(cls); } diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingStoredProcedureQuery.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingStoredProcedureQuery.java index 928837622bf..f8b040b15d9 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingStoredProcedureQuery.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingStoredProcedureQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,23 +24,23 @@ import jakarta.persistence.StoredProcedureQuery; import jakarta.persistence.TemporalType; -abstract class DelegatingStoredProcedureQuery extends DelegatingQuery implements StoredProcedureQuery { +class DelegatingStoredProcedureQuery extends DelegatingQuery implements StoredProcedureQuery { private final StoredProcedureQuery delegate; - DelegatingStoredProcedureQuery(final StoredProcedureQuery delegate) { + DelegatingStoredProcedureQuery(StoredProcedureQuery delegate) { super(delegate); this.delegate = delegate; } @Override - public Object getOutputParameterValue(final int position) { + public Object getOutputParameterValue(int position) { return this.delegate.getOutputParameterValue(position); } @Override - public Object getOutputParameterValue(final String parameterName) { + public Object getOutputParameterValue(String parameterName) { return this.delegate.getOutputParameterValue(parameterName); } @@ -59,18 +59,18 @@ public int getUpdateCount() { @Override @SuppressWarnings("rawtypes") - public DelegatingStoredProcedureQuery registerStoredProcedureParameter(final int position, - final Class type, - final ParameterMode mode) { + public DelegatingStoredProcedureQuery registerStoredProcedureParameter(int position, + Class type, + ParameterMode mode) { this.delegate.registerStoredProcedureParameter(position, type, mode); return this; } @Override @SuppressWarnings("rawtypes") - public DelegatingStoredProcedureQuery registerStoredProcedureParameter(final String parameterName, - final Class type, - final ParameterMode mode) { + public DelegatingStoredProcedureQuery registerStoredProcedureParameter(String parameterName, + Class type, + ParameterMode mode) { this.delegate.registerStoredProcedureParameter(parameterName, type, mode); return this; } @@ -83,71 +83,71 @@ public DelegatingStoredProcedureQuery setFlushMode(FlushModeType flushMode) { @Override - public DelegatingStoredProcedureQuery setHint(final String hintName, - final Object value) { + public DelegatingStoredProcedureQuery setHint(String hintName, + Object value) { return (DelegatingStoredProcedureQuery) super.setHint(hintName, value); } @Override - public DelegatingStoredProcedureQuery setParameter(final Parameter parameter, - final T value) { + public DelegatingStoredProcedureQuery setParameter(Parameter parameter, + T value) { return (DelegatingStoredProcedureQuery) super.setParameter(parameter, value); } @Override - public DelegatingStoredProcedureQuery setParameter(final Parameter parameter, - final Calendar value, - final TemporalType temporalType) { + public DelegatingStoredProcedureQuery setParameter(Parameter parameter, + Calendar value, + TemporalType temporalType) { return (DelegatingStoredProcedureQuery) super.setParameter(parameter, value, temporalType); } @Override - public DelegatingStoredProcedureQuery setParameter(final Parameter parameter, - final Date value, - final TemporalType temporalType) { + public DelegatingStoredProcedureQuery setParameter(Parameter parameter, + Date value, + TemporalType temporalType) { return (DelegatingStoredProcedureQuery) super.setParameter(parameter, value, temporalType); } @Override - public DelegatingStoredProcedureQuery setParameter(final int position, - final Object value) { + public DelegatingStoredProcedureQuery setParameter(int position, + Object value) { return (DelegatingStoredProcedureQuery) super.setParameter(position, value); } @Override - public DelegatingStoredProcedureQuery setParameter(final int position, - final Calendar value, - final TemporalType temporalType) { + public DelegatingStoredProcedureQuery setParameter(int position, + Calendar value, + TemporalType temporalType) { return (DelegatingStoredProcedureQuery) super.setParameter(position, value, temporalType); } @Override - public DelegatingStoredProcedureQuery setParameter(final int position, - final Date value, - final TemporalType temporalType) { + public DelegatingStoredProcedureQuery setParameter(int position, + Date value, + TemporalType temporalType) { return (DelegatingStoredProcedureQuery) super.setParameter(position, value, temporalType); } @Override - public DelegatingStoredProcedureQuery setParameter(final String name, - final Object value) { + public DelegatingStoredProcedureQuery setParameter(String name, + Object value) { return (DelegatingStoredProcedureQuery) super.setParameter(name, value); } @Override - public DelegatingStoredProcedureQuery setParameter(final String name, - final Calendar value, - final TemporalType temporalType) { + public DelegatingStoredProcedureQuery setParameter(String name, + Calendar value, + TemporalType temporalType) { return (DelegatingStoredProcedureQuery) super.setParameter(name, value, temporalType); } @Override - public DelegatingStoredProcedureQuery setParameter(final String name, - final Date value, - final TemporalType temporalType) { + public DelegatingStoredProcedureQuery setParameter(String name, + Date value, + TemporalType temporalType) { return (DelegatingStoredProcedureQuery) super.setParameter(name, value, temporalType); } diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingTypedQuery.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingTypedQuery.java index a4d2e1bd2ac..fbf72e2c999 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingTypedQuery.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/DelegatingTypedQuery.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,11 +28,11 @@ import jakarta.persistence.TemporalType; import jakarta.persistence.TypedQuery; -abstract class DelegatingTypedQuery implements TypedQuery { +class DelegatingTypedQuery implements TypedQuery { private final TypedQuery delegate; - DelegatingTypedQuery(final TypedQuery delegate) { + DelegatingTypedQuery(TypedQuery delegate) { super(); this.delegate = Objects.requireNonNull(delegate); } @@ -56,7 +56,7 @@ public int getMaxResults() { } @Override - public TypedQuery setMaxResults(final int maxResults) { + public TypedQuery setMaxResults(int maxResults) { this.delegate.setMaxResults(maxResults); return this; } @@ -68,7 +68,7 @@ public int getFirstResult() { } @Override - public TypedQuery setFirstResult(final int startPosition) { + public TypedQuery setFirstResult(int startPosition) { this.delegate.setFirstResult(startPosition); return this; } @@ -80,7 +80,7 @@ public Map getHints() { } @Override - public TypedQuery setHint(final String hintName, final Object value) { + public TypedQuery setHint(String hintName, Object value) { this.delegate.setHint(hintName, value); return this; } @@ -92,113 +92,113 @@ public Set> getParameters() { } @Override - public Parameter getParameter(final String name) { + public Parameter getParameter(String name) { return this.delegate.getParameter(name); } @Override - public Parameter getParameter(final String name, final Class type) { + public Parameter getParameter(String name, Class type) { return this.delegate.getParameter(name, type); } @Override - public Parameter getParameter(final int position) { + public Parameter getParameter(int position) { return this.delegate.getParameter(position); } @Override - public Parameter getParameter(final int position, final Class type) { + public Parameter getParameter(int position, Class type) { return this.delegate.getParameter(position, type); } @Override - public T getParameterValue(final Parameter parameter) { + public T getParameterValue(Parameter parameter) { return this.delegate.getParameterValue(parameter); } @Override - public Object getParameterValue(final String name) { + public Object getParameterValue(String name) { return this.delegate.getParameterValue(name); } @Override - public Object getParameterValue(final int position) { + public Object getParameterValue(int position) { return this.delegate.getParameterValue(position); } @Override - public boolean isBound(final Parameter parameter) { + public boolean isBound(Parameter parameter) { return this.delegate.isBound(parameter); } @Override - public TypedQuery setParameter(final Parameter parameter, - final T value) { + public TypedQuery setParameter(Parameter parameter, + T value) { this.delegate.setParameter(parameter, value); return this; } @Override - public TypedQuery setParameter(final Parameter parameter, - final Calendar value, - final TemporalType temporalType) { + public TypedQuery setParameter(Parameter parameter, + Calendar value, + TemporalType temporalType) { this.delegate.setParameter(parameter, value, temporalType); return this; } @Override - public TypedQuery setParameter(final Parameter parameter, - final Date value, - final TemporalType temporalType) { + public TypedQuery setParameter(Parameter parameter, + Date value, + TemporalType temporalType) { this.delegate.setParameter(parameter, value, temporalType); return this; } @Override - public TypedQuery setParameter(final int position, - final Object value) { + public TypedQuery setParameter(int position, + Object value) { this.delegate.setParameter(position, value); return this; } @Override - public TypedQuery setParameter(final int position, - final Calendar value, - final TemporalType temporalType) { + public TypedQuery setParameter(int position, + Calendar value, + TemporalType temporalType) { this.delegate.setParameter(position, value, temporalType); return this; } @Override - public TypedQuery setParameter(final int position, - final Date value, - final TemporalType temporalType) { + public TypedQuery setParameter(int position, + Date value, + TemporalType temporalType) { this.delegate.setParameter(position, value, temporalType); return this; } @Override - public TypedQuery setParameter(final String name, - final Object value) { + public TypedQuery setParameter(String name, + Object value) { this.delegate.setParameter(name, value); return this; } @Override - public TypedQuery setParameter(final String name, - final Calendar value, - final TemporalType temporalType) { + public TypedQuery setParameter(String name, + Calendar value, + TemporalType temporalType) { this.delegate.setParameter(name, value, temporalType); return this; } @Override - public TypedQuery setParameter(final String name, - final Date value, - final TemporalType temporalType) { + public TypedQuery setParameter(String name, + Date value, + TemporalType temporalType) { this.delegate.setParameter(name, value, temporalType); return this; } @@ -210,7 +210,7 @@ public FlushModeType getFlushMode() { } @Override - public TypedQuery setFlushMode(final FlushModeType flushMode) { + public TypedQuery setFlushMode(FlushModeType flushMode) { this.delegate.setFlushMode(flushMode); return this; } @@ -222,7 +222,7 @@ public LockModeType getLockMode() { } @Override - public TypedQuery setLockMode(final LockModeType lockMode) { + public TypedQuery setLockMode(LockModeType lockMode) { this.delegate.setLockMode(lockMode); return this; } @@ -235,7 +235,10 @@ public int executeUpdate() { @Override - public T unwrap(final Class cls) { + public T unwrap(Class cls) { + if (cls != null && cls.isInstance(this)) { + return cls.cast(this); + } return this.delegate.unwrap(cls); } 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 index 08c1c9b8622..86594a348ac 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -104,21 +104,20 @@ import static jakarta.interceptor.Interceptor.Priority.LIBRARY_BEFORE; /** - * 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. + * 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. * *

Thread Safety

* - *

As with all CDI portable extensions, instances of this class are - * not safe for concurrent use by multiple - * threads.

+ *

As with all CDI portable extensions, instances of this class are not safe for concurrent use by + * multiple threads.

* * @see PersistenceUnitInfoBean */ public class JpaExtension implements Extension { + + /* * Static fields. */ @@ -132,32 +131,21 @@ public class JpaExtension implements Extension { JpaExtension.class.getPackage().getName() + ".Messages"); /** - * The name used to designate the only persistence unit in the - * environment, when there is exactly one persistence unit in the - * environment, and there is at least one {@link - * PersistenceContext @PersistenceContext}-annotated injection - * point that does not specify a value for the {@link - * PersistenceContext#unitName() unitName} element. - * - *

In such a case, the injection point will be effectively - * rewritten such that it will appear to the CDI container as - * though there were a value specified for the {@link - * PersistenceContext#unitName() unitName} element—namely - * this field's value. Additionally, a bean identical to the - * existing solitary {@link PersistenceUnitInfo}-typed bean will - * be added with this field's value as the {@linkplain - * Named#value() value of its Named qualifier}, thus - * serving as a kind of alias for the "real" bean.

- * - *

This is necessary because the empty string ({@code ""}) as - * the value of the {@link Named#value()} element has special - * semantics, so cannot be used to designate an unnamed - * persistence unit.

- * - *

The value of this field is subject to change without prior - * notice at any point. In general the mechanics around injection - * point rewriting are also subject to change without prior notice - * at any point.

+ * The name used to designate the only persistence unit in the environment, when there is exactly one persistence + * unit in the environment, and there is at least one {@link PersistenceContext @PersistenceContext}-annotated + * injection point that does not specify a value for the {@link PersistenceContext#unitName() unitName} element. + * + *

In such a case, the injection point will be effectively rewritten such that it will appear to the CDI + * container as though there were a value specified for the {@link PersistenceContext#unitName() unitName} + * element—namely this field's value. Additionally, a bean identical to the existing solitary {@link + * PersistenceUnitInfo}-typed bean will be added with this field's value as the {@linkplain Named#value() value of + * its Named qualifier}, thus serving as a kind of alias for the "real" bean.

+ * + *

This is necessary because the empty string ({@code ""}) as the value of the {@link Named#value()} element has + * special semantics, so cannot be used to designate an unnamed persistence unit.

+ * + *

The value of this field is subject to change without prior notice at any point. In general the mechanics + * around injection point rewriting are also subject to change without prior notice at any point.

*/ static final String DEFAULT_PERSISTENCE_UNIT_NAME = "__DEFAULT__"; @@ -167,6 +155,13 @@ public class JpaExtension implements Extension { */ + /** + * Whether or not this extension will do anything. + * + *

This field's value is {@code true} by default.

+ */ + private final boolean enabled; + /** * Indicates if JTA transactions can be supported. * @@ -175,22 +170,17 @@ public class JpaExtension implements Extension { private boolean transactionsSupported; /** - * A {@link Map} of {@link PersistenceUnitInfoBean} instances that - * were created by the {@link - * #gatherImplicitPersistenceUnits(ProcessAnnotatedType, - * BeanManager)} observer method, indexed by the names of + * 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.

+ *

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

* - *

This field is {@linkplain Map#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Map#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

* * @see #gatherImplicitPersistenceUnits(ProcessAnnotatedType, BeanManager) * @@ -199,132 +189,102 @@ public class JpaExtension implements Extension { 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}). + * 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}.

+ *

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

* *

This field is never {@code null}.

* - *

This field is {@linkplain Map#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Map#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

*/ private final Map>> unlistedManagedClassesByPersistenceUnitNames; /** - * A {@link Set} of {@link Set}s of CDI qualifiers annotating CDI - * injection points related to JPA. + * A {@link Set} of {@link Set}s of CDI qualifiers annotating CDI injection points related to JPA. * *

This field is never {@code null}.

* - *

These qualifiers are built up as this portable extension - * {@linkplain ProcessInjectionPoint discovers {@link + *

These qualifiers are built up as this portable extension {@linkplain ProcessInjectionPoint discovers {@link * EntityManager}-typed InjectionPoints}.

* - *

This field is {@linkplain Collection#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Collection#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

* * @see #saveEntityManagerQualifiers(ProcessInjectionPoint) */ private final Set> persistenceContextQualifiers; /** - * A {@link Set} of {@link Set}s of CDI qualifiers for which - * {@link EntityManagerFactory} beans may be created. + * A {@link Set} of {@link Set}s of CDI qualifiers for which {@link EntityManagerFactory} beans may be created. * *

This field is never {@code null}.

* - *

These qualifiers are built up as this portable extension - * {@linkplain ProcessInjectionPoint discovers {@link + *

These qualifiers are built up as this portable extension {@linkplain ProcessInjectionPoint discovers {@link * EntityManagerFactory}-typed InjectionPoints}.

* - *

This field is {@linkplain Collection#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Collection#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

* * @see #saveEntityManagerFactoryQualifiers(ProcessInjectionPoint) */ private final Set> persistenceUnitQualifiers; /** - * A {@link Set} of {@link Set}s of CDI qualifiers that serves as - * a kind of cache, preventing more than one {@link - * CdiTransactionScoped}-qualified {@link EntityManager}-typed - * bean from being added for the same set of qualifiers. + * A {@link Set} of {@link Set}s of CDI qualifiers that serves as a kind of cache, preventing more than one {@link + * CdiTransactionScoped}-qualified {@link EntityManager}-typed bean from being added for the same set of qualifiers. * *

This field is never {@code null}.

* - *

This field is {@linkplain Collection#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Collection#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

* - * @see - * #addCdiTransactionScopedEntityManagerBeans(AfterBeanDiscovery, - * Set) + * @see #addCdiTransactionScopedEntityManagerBeans(AfterBeanDiscovery, Set) */ private final Set> cdiTransactionScopedEntityManagerQualifiers; /** - * A {@link Set} of {@link Set}s of CDI qualifiers that serves as - * a kind of cache, preventing more than one {@link - * NonTransactional}-qualified {@link EntityManager}-typed bean - * from being added for the same set of qualifiers. + * A {@link Set} of {@link Set}s of CDI qualifiers that serves as a kind of cache, preventing more than one {@link + * NonTransactional}-qualified {@link EntityManager}-typed bean from being added for the same set of qualifiers. * *

This field is never {@code null}.

* - *

This field is {@linkplain Collection#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Collection#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

* - * @see #addNonTransactionalEntityManagerBeans(AfterBeanDiscovery, - * Set, BeanManager) + * @see #addNonTransactionalEntityManagerBeans(AfterBeanDiscovery, Set, BeanManager) */ private final Set> nonTransactionalEntityManagerQualifiers; /** - * A {@link Set} of {@link Set}s of CDI qualifiers that serves as - * a kind of cache, preventing more than one {@link - * ContainerManaged}-qualified {@link EntityManagerFactory}-typed - * bean from being added for the same set of qualifiers. + * A {@link Set} of {@link Set}s of CDI qualifiers that serves as a kind of cache, preventing more than one {@link + * ContainerManaged}-qualified {@link EntityManagerFactory}-typed bean from being added for the same set of + * qualifiers. * *

This field is never {@code null}.

* - *

This field is {@linkplain Collection#clear() cleared} at the - * termination of the {@link - * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container - * lifecycle method.

+ *

This field is {@linkplain Collection#clear() cleared} at the termination of the {@link + * #afterBeanDiscovery(AfterBeanDiscovery, BeanManager)} container lifecycle method.

* - * @see - * #addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery, - * Set, BeanManager) + * @see #addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery, Set, BeanManager) */ private final Set> containerManagedEntityManagerFactoryQualifiers; /** - * Indicates whether an injection point has called for the default - * persistence unit. This has implications on how beans are - * installed. + * Indicates whether an injection point has called for the default persistence unit. This has implications on how + * beans are installed. * - * @see #validate(AfterDeploymentValidation) + * @see #validate(AfterDeploymentValidation, BeanManager) */ private boolean defaultPersistenceUnitInEffect; /** - * Indicates whether a bean for the default persistence unit - * has been added. + * Indicates whether a bean for the default persistence unit has been added. * - * @see #validate(AfterDeploymentValidation) + * @see #validate(AfterDeploymentValidation, BeanManager) */ private boolean addedDefaultPersistenceUnit; @@ -337,20 +297,22 @@ public class JpaExtension implements Extension { /** * Creates a new {@link JpaExtension}. * - *

Normally {@link JpaExtension} classes are created - * automatically and only as needed by the CDI container. End - * users should have no need to create instances of this - * class.

+ *

Normally {@link JpaExtension} classes are created automatically and only as needed by the CDI container. End + * users should have no need to create instances of this class.

* * @see Extension */ public JpaExtension() { super(); - final String cn = JpaExtension.class.getName(); - final String mn = ""; + String cn = JpaExtension.class.getName(); + String mn = ""; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn); } + this.enabled = Boolean.parseBoolean(System.getProperty(this.getClass().getName() + ".enabled", "true")); + if (LOGGER.isLoggable(Level.FINE) && !this.enabled) { + LOGGER.logp(Level.FINE, cn, mn, "jpaExtensionDisabled", this.getClass().getName()); + } this.unlistedManagedClassesByPersistenceUnitNames = new HashMap<>(); this.implicitPersistenceUnits = new HashMap<>(); this.persistenceContextQualifiers = new HashSet<>(); @@ -358,10 +320,8 @@ public JpaExtension() { this.containerManagedEntityManagerFactoryQualifiers = new HashSet<>(); this.nonTransactionalEntityManagerQualifiers = new HashSet<>(); this.persistenceUnitQualifiers = new HashSet<>(); - // We start by presuming that JTA transactions can be - // supported. See the - // #disableTransactionSupport(ProcessAnnotatedType) method - // where this decision might be reversed. + // We start by presuming that JTA transactions can be supported. See the + // #disableTransactionSupport(ProcessAnnotatedType) method where this decision might be reversed. this.transactionsSupported = true; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.exiting(cn, mn); @@ -375,17 +335,13 @@ public JpaExtension() { /** - * If {@code event} is non-{@code null}, then when this method is - * invoked it irrevocably sets the {@link #transactionsSupported} - * field to {@code false}. + * If {@code event} is non-{@code null}, then when this method is invoked it irrevocably sets the {@link + * #transactionsSupported} field to {@code false}. * - * @param event a {@link ProcessAnnotatedType - * ProcessAnnotatedType<}{@link NoTransactionSupport - * NoTransactionSupport>} whose presence indicates that JTA - * support is not available; must not be {@code null} + * @param event a {@link ProcessAnnotatedType ProcessAnnotatedType<}{@link NoTransactionSupport + * NoTransactionSupport>} whose presence indicates that JTA support is not available; must not be {@code null} * - * @exception NullPointerException if {@code event} is {@code - * null} + * @exception NullPointerException if {@code event} is {@code null} * * @see #transactionsSupported * @@ -393,22 +349,25 @@ public JpaExtension() { */ private void disableTransactionSupport(@Observes @Priority(LIBRARY_BEFORE) - final ProcessAnnotatedType event) { - final String cn = JpaExtension.class.getName(); - final String mn = "disableTransactionSupport"; + ProcessAnnotatedType event) { + String cn = JpaExtension.class.getName(); + String mn = "disableTransactionSupport"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, event); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); - // If we receive an event of this type, then beans.xml - // exclusions have fired such that it has been determined that - // JTA is not loadable. This of course means that JTA - // transactions cannot be supported, and hence many (but not - // all) features of JPA integration cannot be supported as - // well. See ../../../../../../resources/META-INF/beans.xml - // and note the if-class-available and if-class-not-available + // If we receive an event of this type, then beans.xml exclusions have fired such that it has been determined + // that JTA is not loadable. This of course means that JTA transactions cannot be supported, and hence many + // (but not all) features of JPA integration cannot be supported as well. See + // ../../../../../../resources/META-INF/beans.xml and note the if-class-available and if-class-not-available // elements. this.transactionsSupported = false; @@ -418,15 +377,11 @@ private void disableTransactionSupport(@Observes } /** - * Converts fields and setter methods annotated with {@link - * PersistenceContext} to CDI injection points annotated with an - * appropriate combination of {@link Inject}, {@link - * ContainerManaged}, {@link Extended}, {@link - * JpaTransactionScoped}, {@link Synchronized} and/or {@link - * Unsynchronized}. + * Converts fields and setter methods annotated with {@link PersistenceContext} to CDI injection points annotated + * with an appropriate combination of {@link Inject}, {@link ContainerManaged}, {@link Extended}, {@link + * JpaTransactionScoped}, {@link Synchronized} and/or {@link Unsynchronized}. * - * @param event the {@link ProcessAnnotatedType} container - * lifecycle event being observed; must not be {@code null} + * @param event the {@link ProcessAnnotatedType} container lifecycle event being observed; must not be {@code null} * * @exception NullPointerException if {@code event} is {@code null} */ @@ -435,16 +390,22 @@ private void rewriteJpaAnnotations(@Observes PersistenceContext.class, PersistenceUnit.class }) - final ProcessAnnotatedType event) { - final String cn = JpaExtension.class.getName(); - final String mn = "rewriteJpaAnnotations"; + ProcessAnnotatedType event) { + String cn = JpaExtension.class.getName(); + String mn = "rewriteJpaAnnotations"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, event); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); - final AnnotatedTypeConfigurator atc = event.configureAnnotatedType(); + AnnotatedTypeConfigurator atc = event.configureAnnotatedType(); atc.filterFields(JpaExtension::isEligiblePersistenceContextField) .forEach(this::rewritePersistenceContextFieldAnnotations); atc.filterFields(JpaExtension::isEligiblePersistenceUnitField) @@ -460,21 +421,15 @@ private void rewriteJpaAnnotations(@Observes } /** - * 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; - * must not be {@code null} + * 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 beanManager the {@link BeanManager} in effect; must not - * be {@code null} + * @param event the {@link ProcessAnnotatedType} event occurring; must not be {@code null} * - * @exception NullPointerException if either {@code event} or - * {@code beanManager} is {@code null} + * @param beanManager the {@link BeanManager} in effect; must not be {@code null} + * + * @exception NullPointerException if either {@code event} or {@code beanManager} is {@code null} * * @see PersistenceContext * @@ -485,54 +440,60 @@ private void rewriteJpaAnnotations(@Observes private void gatherImplicitPersistenceUnits(@Observes // yes, @PersistenceContext, not @PersistenceUnit @WithAnnotations(PersistenceContext.class) - final ProcessAnnotatedType event, - final BeanManager beanManager) { - final String cn = JpaExtension.class.getName(); - final String mn = "gatherImplicitPersistenceUnits"; + ProcessAnnotatedType event, + BeanManager beanManager) { + String cn = JpaExtension.class.getName(); + String mn = "gatherImplicitPersistenceUnits"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, beanManager}); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); Objects.requireNonNull(beanManager); - final AnnotatedType annotatedType = event.getAnnotatedType(); + AnnotatedType annotatedType = event.getAnnotatedType(); if (annotatedType != null && !annotatedType.isAnnotationPresent(Vetoed.class)) { - final Set persistenceContexts = + Set persistenceContexts = annotatedType.getAnnotations(PersistenceContext.class); if (persistenceContexts != null && !persistenceContexts.isEmpty()) { - for (final PersistenceContext persistenceContext : persistenceContexts) { + for (PersistenceContext persistenceContext : persistenceContexts) { if (LOGGER.isLoggable(Level.INFO)) { - final String name = persistenceContext.name().trim(); + String name = persistenceContext.name().trim(); if (!name.isEmpty()) { LOGGER.logp(Level.INFO, cn, mn, "persistenceContextNameIgnored", new Object[] {annotatedType, name}); } } - final PersistenceProperty[] persistenceProperties = persistenceContext.properties(); + PersistenceProperty[] persistenceProperties = persistenceContext.properties(); if (persistenceProperties != null && persistenceProperties.length > 0) { - final String persistenceUnitName = persistenceContext.unitName(); + String persistenceUnitName = persistenceContext.unitName(); assert persistenceUnitName != null; PersistenceUnitInfoBean persistenceUnit = this.implicitPersistenceUnits.get(persistenceUnitName); if (persistenceUnit == null) { - final String jtaDataSourceName; + String jtaDataSourceName; if (persistenceUnitName.isEmpty()) { jtaDataSourceName = null; } else { jtaDataSourceName = persistenceUnitName; } - final Class javaClass = annotatedType.getJavaClass(); + Class javaClass = annotatedType.getJavaClass(); URL persistenceUnitRoot = null; - final ProtectionDomain pd = javaClass.getProtectionDomain(); + ProtectionDomain pd = javaClass.getProtectionDomain(); if (pd != null) { - final CodeSource cs = pd.getCodeSource(); + CodeSource cs = pd.getCodeSource(); if (cs != null) { persistenceUnitRoot = cs.getLocation(); } } - final Properties properties = new Properties(); - for (final PersistenceProperty persistenceProperty : persistenceProperties) { - final String persistencePropertyName = persistenceProperty.name(); + Properties properties = new Properties(); + for (PersistenceProperty persistenceProperty : persistenceProperties) { + String persistencePropertyName = persistenceProperty.name(); if (!persistencePropertyName.isEmpty()) { properties.setProperty(persistencePropertyName, persistenceProperty.value()); } @@ -559,24 +520,17 @@ private void gatherImplicitPersistenceUnits(@Observes } /** - * 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; must not be {@code null} - * - * @exception NullPointerException if {@code event} is {@code - * null} + * 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; must not be {@code null} + * + * @exception NullPointerException if {@code event} is {@code null} * * @see Converter * @@ -595,16 +549,22 @@ private void discoverManagedClasses(@Observes Entity.class, MappedSuperclass.class }) - final ProcessAnnotatedType event) { - final String cn = JpaExtension.class.getName(); - final String mn = "discoverManagedClasses"; + ProcessAnnotatedType event) { + String cn = JpaExtension.class.getName(); + String mn = "discoverManagedClasses"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, event); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); - final AnnotatedType annotatedType = event.getAnnotatedType(); + AnnotatedType annotatedType = event.getAnnotatedType(); if (annotatedType != null && !annotatedType.isAnnotationPresent(Vetoed.class)) { this.assignManagedClassToPersistenceUnit(annotatedType.getAnnotations(PersistenceContext.class), annotatedType.getAnnotations(PersistenceUnit.class), @@ -618,33 +578,28 @@ private void discoverManagedClasses(@Observes } /** - * 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. + * 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 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 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 + * @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"; + private void assignManagedClassToPersistenceUnit(Set persistenceContexts, + Set persistenceUnits, + Class c) { + String cn = JpaExtension.class.getName(); + String mn = "assignManagedClassToPersistenceUnit"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {persistenceContexts, persistenceUnits, c}); } @@ -652,7 +607,7 @@ private void assignManagedClassToPersistenceUnit(final Set managedClass) { - final String cn = JpaExtension.class.getName(); - final String mn = "addUnlistedManagedClass"; + private void addUnlistedManagedClass(String name, Class managedClass) { + String cn = JpaExtension.class.getName(); + String mn = "addUnlistedManagedClass"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {name, managedClass}); } @@ -727,29 +679,31 @@ private void addUnlistedManagedClass(String name, final Class managedClass) { } /** - * Stores {@link Set}s of qualifiers that annotate {@link - * EntityManagerFactory}-typed injection points. + * Stores {@link Set}s of qualifiers that annotate {@link EntityManagerFactory}-typed injection points. * - *

{@link EntityManagerFactory}-typed beans will be added for - * each such {@link Set}.

+ *

{@link EntityManagerFactory}-typed beans will be added for each such {@link Set}.

* - * @param event a {@link ProcessInjectionPoint} container - * lifecycle event; must not be {@code null} + * @param event a {@link ProcessInjectionPoint} container lifecycle event; must not be {@code null} * - * @exception NullPointerException if {@code event} is {@code - * null} + * @exception NullPointerException if {@code event} is {@code null} */ private void saveEntityManagerFactoryQualifiers(@Observes - final ProcessInjectionPoint event) { - final String cn = JpaExtension.class.getName(); - final String mn = "saveEntityManagerFactoryQualifiers"; + ProcessInjectionPoint event) { + String cn = JpaExtension.class.getName(); + String mn = "saveEntityManagerFactoryQualifiers"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, event); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); - final InjectionPoint injectionPoint = event.getInjectionPoint(); + InjectionPoint injectionPoint = event.getInjectionPoint(); assert injectionPoint != null; this.persistenceUnitQualifiers.add(injectionPoint.getQualifiers()); @@ -760,32 +714,34 @@ private void saveEntityManagerFactoryQualifiers } /** - * Stores {@link Set}s of qualifiers that annotate {@link - * EntityManager}-typed injection points. + * Stores {@link Set}s of qualifiers that annotate {@link EntityManager}-typed injection points. * - *

{@link EntityManager}-typed beans will be added for each - * such {@link Set}.

+ *

{@link EntityManager}-typed beans will be added for each such {@link Set}.

* - * @param event a {@link ProcessInjectionPoint} container - * lifecycle event; must not be {@code null} + * @param event a {@link ProcessInjectionPoint} container lifecycle event; must not be {@code null} * - * @exception NullPointerException if {@code event} is {@code - * null} + * @exception NullPointerException if {@code event} is {@code null} */ private void saveEntityManagerQualifiers(@Observes - final ProcessInjectionPoint event) { - final String cn = JpaExtension.class.getName(); - final String mn = "saveEntityManagerQualifiers"; + ProcessInjectionPoint event) { + String cn = JpaExtension.class.getName(); + String mn = "saveEntityManagerQualifiers"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, event); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); - final InjectionPoint injectionPoint = event.getInjectionPoint(); + InjectionPoint injectionPoint = event.getInjectionPoint(); assert injectionPoint != null; - final Set qualifiers = injectionPoint.getQualifiers(); + Set qualifiers = injectionPoint.getQualifiers(); assert qualifiers != null; boolean error = false; if (qualifiers.contains(JpaTransactionScoped.Literal.INSTANCE)) { @@ -823,82 +779,74 @@ private void saveEntityManagerQualifiers(@Observes /** * Adds various beans that integrate JPA into CDI SE. * - *

This method first 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 method first 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.

+ *

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

* - *

Next, this method adds beans to produce {@link - * EntityManager}s and {@link EntityManagerFactory} instances in + *

Next, this method adds beans to produce {@link EntityManager}s and {@link EntityManagerFactory} instances in * accordance with the JPA specification.

* - * @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; + * @param event the {@link AfterBeanDiscovery} event describing the fact that bean discovery has been performed; * 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 + * @param beanManager the {@link BeanManager} currently in effect; must not be {@code null} * - * @exception JAXBException if there was a problem {@linkplain - * Unmarshaller#unmarshal(Reader) unmarshalling} a {@code - * META-INF/persistence.xml} resource + * @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 NullPointerException if either {@code event} or - * {@code beanManager} is {@code null} + * @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 + * @exception XMLStreamException if there was a problem setting up JAXB * * @see PersistenceUnitInfo */ private void afterBeanDiscovery(@Observes @Priority(LIBRARY_AFTER) - final AfterBeanDiscovery event, - final BeanManager beanManager) + AfterBeanDiscovery event, + BeanManager beanManager) throws IOException, JAXBException, ReflectiveOperationException, XMLStreamException { - final String cn = JpaExtension.class.getName(); - final String mn = "afterBeanDiscovery"; + String cn = JpaExtension.class.getName(); + String mn = "afterBeanDiscovery"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, beanManager}); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); Objects.requireNonNull(beanManager); - final Collection providers = addPersistenceProviderBeans(event); + Collection providers = addPersistenceProviderBeans(event); - // Should we consider type-level @PersistenceContext - // definitions of persistence units ("implicits")? + // 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 = + // 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.) + Set> preexistingPersistenceUnitInfoBeans = beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); if (preexistingPersistenceUnitInfoBeans != null && !preexistingPersistenceUnitInfoBeans.isEmpty()) { processImplicits = false; this.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(); - final Enumeration urls = classLoader.getResources("META-INF/persistence.xml"); + // 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). + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration urls = classLoader.getResources("META-INF/persistence.xml"); if (urls != null && urls.hasMoreElements()) { processImplicits = false; this.processPersistenceXmls(event, @@ -910,21 +858,17 @@ private void afterBeanDiscovery(@Observes && !preexistingPersistenceUnitInfoBeans.isEmpty()); } - // 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 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); } - // Add beans to support JPA. In some cases, JTA must be - // present (see JPA section 7.5, for example: "A - // container-managed entity manager must be a JTA entity - // manager."). + // Add beans to support JPA. In some cases, JTA must be present (see JPA section 7.5, for example: "A + // container-managed entity manager must be a JTA entity manager."). this.addContainerManagedJpaBeans(event, beanManager); - // Clear out no-longer-needed-or-used collections to save - // memory. + // Clear out no-longer-needed-or-used collections to save memory. this.cdiTransactionScopedEntityManagerQualifiers.clear(); this.containerManagedEntityManagerFactoryQualifiers.clear(); this.implicitPersistenceUnits.clear(); @@ -939,46 +883,45 @@ private void afterBeanDiscovery(@Observes } /** - * Ensures that {@link PersistenceUnitInfo}-typed injection points - * are satisfied. + * Ensures that {@link PersistenceUnitInfo}-typed injection points are satisfied. * - * @param event the {@link AfterDeploymentValidation} container - * lifecycle event; must not be {@code null} + * @param event the {@link AfterDeploymentValidation} container lifecycle event; must not be {@code null} * - * @param beanManager the {@link BeanManager} currently in effect; - * must not be {@code null} + * @param beanManager the {@link BeanManager} currently in effect; must not be {@code null} * - * @exception NullPointerException if either {@code event} or - * {@code beanManager} is {@code null} + * @exception NullPointerException if either {@code event} or {@code beanManager} is {@code null} */ - private void validate(@Observes final AfterDeploymentValidation event, final BeanManager beanManager) { - final String cn = JpaExtension.class.getName(); - final String mn = "validateJpaInjectionPoints"; + private void validate(@Observes AfterDeploymentValidation event, BeanManager beanManager) { + String cn = JpaExtension.class.getName(); + String mn = "validateJpaInjectionPoints"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, beanManager}); } + if (!this.enabled) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(cn, mn); + } + return; + } Objects.requireNonNull(event); Objects.requireNonNull(beanManager); if (this.defaultPersistenceUnitInEffect && !this.addedDefaultPersistenceUnit) { - // The user had originally specified something like - // just @PersistenceContext (instead - // of @PersistenceContext(unitName = "something")), but - // for whatever reason a default PersistenceUnitInfo bean - // was not added. This will only ever be the case if - // multiple persistence units are present. - final Set> persistenceUnitInfoBeans = beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); + // The user had originally specified something like just @PersistenceContext (instead + // of @PersistenceContext(unitName = "something")), but for whatever reason a default PersistenceUnitInfo + // bean was not added. This will only ever be the case if multiple persistence units are present. + Set> persistenceUnitInfoBeans = beanManager.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); assert persistenceUnitInfoBeans != null; assert persistenceUnitInfoBeans.size() > 1 : "Unexpected persistenceUnitInfoBeans: " + persistenceUnitInfoBeans; try { beanManager.resolve(persistenceUnitInfoBeans); - } catch (final AmbiguousResolutionException expected) { - final Set names = new HashSet<>(); - for (final Bean bean : persistenceUnitInfoBeans) { + } catch (AmbiguousResolutionException expected) { + Set names = new HashSet<>(); + for (Bean bean : persistenceUnitInfoBeans) { assert bean != null; - final Set qualifiers = bean.getQualifiers(); - for (final Annotation qualifier : qualifiers) { + Set qualifiers = bean.getQualifiers(); + for (Annotation qualifier : qualifiers) { if (qualifier instanceof Named) { names.add(((Named) qualifier).value()); break; @@ -997,41 +940,29 @@ private void validate(@Observes final AfterDeploymentValidation event, final Bea } /** - * Adds certain beans to support injection of {@link - * EntityManagerFactory} and {@link EntityManager} instances + * Adds certain beans to support injection of {@link EntityManagerFactory} and {@link EntityManager} instances * according to the JPA specification. * - * @param event an {@link AfterBeanDiscovery} container lifecycle - * event; must not be {@code null} + * @param event an {@link AfterBeanDiscovery} container lifecycle event; must not be {@code null} * - * @param beanManager the current {@link BeanManager}; must not be - * {@code null} + * @param beanManager the current {@link BeanManager}; must not be {@code null} * - * @exception NullPointerException if either {@code event} or - * {@code beanManager} is {@code null} + * @exception NullPointerException if either {@code event} or {@code beanManager} is {@code null} * - * @see - * #addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery, - * Set, BeanManager) + * @see #addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery, Set, BeanManager) * - * @see - * #addCdiTransactionScopedEntityManagerBeans(AfterBeanDiscovery, - * Set) + * @see #addCdiTransactionScopedEntityManagerBeans(AfterBeanDiscovery, Set) * - * @see #addExtendedEntityManagerBeans(AfterBeanDiscovery, Set, - * BeanManager) + * @see #addExtendedEntityManagerBeans(AfterBeanDiscovery, Set, BeanManager) * - * @see #addNonTransactionalEntityManagerBeans(AfterBeanDiscovery, - * Set, BeanManager) + * @see #addNonTransactionalEntityManagerBeans(AfterBeanDiscovery, Set, BeanManager) * - * @see - * #addJpaTransactionScopedEntityManagerBeans(AfterBeanDiscovery, - * Set) + * @see #addJpaTransactionScopedEntityManagerBeans(AfterBeanDiscovery, Set) */ - private void addContainerManagedJpaBeans(final AfterBeanDiscovery event, final BeanManager beanManager) + private void addContainerManagedJpaBeans(AfterBeanDiscovery event, BeanManager beanManager) throws ReflectiveOperationException { - final String cn = JpaExtension.class.getName(); - final String mn = "addContainerManagedJpaBeans"; + String cn = JpaExtension.class.getName(); + String mn = "addContainerManagedJpaBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, beanManager}); } @@ -1039,16 +970,13 @@ private void addContainerManagedJpaBeans(final AfterBeanDiscovery event, final B Objects.requireNonNull(event); Objects.requireNonNull(beanManager); - for (final Set qualifiers : this.persistenceUnitQualifiers) { + for (Set qualifiers : this.persistenceUnitQualifiers) { addContainerManagedEntityManagerFactoryBeans(event, qualifiers, beanManager); } if (this.transactionsSupported) { - for (final Set qualifiers : this.persistenceContextQualifiers) { - // Note that each add* method invoked below is - // responsible for ensuring that it adds beans only - // once if at all, i.e. for validating and - // de-duplicating the qualifiers that it is supplied - // with if necessary. + for (Set qualifiers : this.persistenceContextQualifiers) { + // Note that each add* method invoked below is responsible for ensuring that it adds beans only once if + // at all, i.e. for validating and de-duplicating the qualifiers that it is supplied with if necessary. addContainerManagedEntityManagerFactoryBeans(event, qualifiers, beanManager); addCdiTransactionScopedEntityManagerBeans(event, qualifiers); if (qualifiers.contains(Extended.Literal.INSTANCE)) { @@ -1060,11 +988,9 @@ private void addContainerManagedJpaBeans(final AfterBeanDiscovery event, final B } } } else { - for (final Set qualifiers : this.persistenceContextQualifiers) { - // Note that each add* method invoked below is - // responsible for ensuring that it adds beans only - // once if at all, i.e. for validating the qualifiers - // that it is supplied with. + for (Set qualifiers : this.persistenceContextQualifiers) { + // Note that each add* method invoked below is responsible for ensuring that it adds beans only once if + // at all, i.e. for validating the qualifiers that it is supplied with. addContainerManagedEntityManagerFactoryBeans(event, qualifiers, beanManager); } } @@ -1074,11 +1000,11 @@ private void addContainerManagedJpaBeans(final AfterBeanDiscovery event, final B } } - private void addContainerManagedEntityManagerFactoryBeans(final AfterBeanDiscovery event, - final Set suppliedQualifiers, - final BeanManager beanManager) { - final String cn = JpaExtension.class.getName(); - final String mn = "addContainerManagedEntityManagerFactoryBeans"; + private void addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery event, + Set suppliedQualifiers, + BeanManager beanManager) { + String cn = JpaExtension.class.getName(); + String mn = "addContainerManagedEntityManagerFactoryBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, suppliedQualifiers, beanManager}); } @@ -1092,7 +1018,7 @@ private void addContainerManagedEntityManagerFactoryBeans(final AfterBeanDiscove // @ContainerManaged // @Named("test") // private final EntityManagerFactory emf; - final Set qualifiers = new HashSet<>(suppliedQualifiers); + Set qualifiers = new HashSet<>(suppliedQualifiers); qualifiers.removeAll(JpaCdiQualifiers.JPA_CDI_QUALIFIERS); qualifiers.add(ContainerManaged.Literal.INSTANCE); if (this.containerManagedEntityManagerFactoryQualifiers.add(qualifiers)) { @@ -1118,11 +1044,11 @@ private void addContainerManagedEntityManagerFactoryBeans(final AfterBeanDiscove } } - private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery event, - final Set suppliedQualifiers) + private void addCdiTransactionScopedEntityManagerBeans(AfterBeanDiscovery event, + Set suppliedQualifiers) throws ReflectiveOperationException { - final String cn = JpaExtension.class.getName(); - final String mn = "addCdiTransactionScopedEntityManagerBeans"; + String cn = JpaExtension.class.getName(); + String mn = "addCdiTransactionScopedEntityManagerBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, suppliedQualifiers}); } @@ -1149,7 +1075,7 @@ private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery // @Unsynchronized // <-- NOTE // @Named("test") // private final EntityManager cdiTransactionScopedEm; - final Set qualifiers = new HashSet<>(suppliedQualifiers); + Set qualifiers = new HashSet<>(suppliedQualifiers); qualifiers.add(ContainerManaged.Literal.INSTANCE); qualifiers.add(CdiTransactionScoped.Literal.INSTANCE); qualifiers.remove(Extended.Literal.INSTANCE); @@ -1159,20 +1085,18 @@ private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery qualifiers.remove(Unsynchronized.Literal.INSTANCE); if (!this.cdiTransactionScopedEntityManagerQualifiers.contains(qualifiers)) { this.cdiTransactionScopedEntityManagerQualifiers.add(new HashSet<>(qualifiers)); - final Class scope; + Class scope; Class temp = null; try { @SuppressWarnings("unchecked") - final Class transactionScopedAnnotationClass = + Class transactionScopedAnnotationClass = (Class) Class.forName("jakarta.transaction.TransactionScoped", true, Thread.currentThread().getContextClassLoader()); temp = transactionScopedAnnotationClass; - } catch (final ClassNotFoundException classNotFoundException) { - // This will not happen if this.transactionsSupported - // is true, or else CDI's exclusion mechanisms are - // broken. If somehow it does we throw a severe - // error. + } catch (ClassNotFoundException classNotFoundException) { + // This will not happen if this.transactionsSupported is true, or else CDI's exclusion mechanisms are + // broken. If somehow it does we throw a severe error. throw new InternalError(classNotFoundException.getMessage(), classNotFoundException); } finally { @@ -1181,7 +1105,7 @@ private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery assert scope != null; qualifiers.add(Synchronized.Literal.INSTANCE); - final Set synchronizedQualifiers = new HashSet<>(qualifiers); + Set synchronizedQualifiers = new HashSet<>(qualifiers); event.addBean() .addTransitiveTypeClosure(CdiTransactionScopedEntityManager.class) .scope(scope) @@ -1194,7 +1118,7 @@ private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery qualifiers.remove(Synchronized.Literal.INSTANCE); qualifiers.add(Unsynchronized.Literal.INSTANCE); - final Set unsynchronizedQualifiers = new HashSet<>(qualifiers); + Set unsynchronizedQualifiers = new HashSet<>(qualifiers); event.addBean() .addTransitiveTypeClosure(CdiTransactionScopedEntityManager.class) .scope(scope) @@ -1211,10 +1135,10 @@ private void addCdiTransactionScopedEntityManagerBeans(final AfterBeanDiscovery } } - private void addJpaTransactionScopedEntityManagerBeans(final AfterBeanDiscovery event, - final Set suppliedQualifiers) { - final String cn = JpaExtension.class.getName(); - final String mn = "addJpaTransactionScopedEntityManagerBeans"; + private void addJpaTransactionScopedEntityManagerBeans(AfterBeanDiscovery event, + Set suppliedQualifiers) { + String cn = JpaExtension.class.getName(); + String mn = "addJpaTransactionScopedEntityManagerBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, suppliedQualifiers}); } @@ -1225,8 +1149,7 @@ private void addJpaTransactionScopedEntityManagerBeans(final AfterBeanDiscovery throw new IllegalStateException(); } - // The JpaTransactionScopedEntityManager "tunnels" another - // scope through it. + // The JpaTransactionScopedEntityManager "tunnels" another scope through it. // Provide support for, e.g.: // @Inject @@ -1235,7 +1158,7 @@ private void addJpaTransactionScopedEntityManagerBeans(final AfterBeanDiscovery // @Synchronized // or @Unsynchronized, or none // @Named("test") // private final EntityManager jpaTransactionScopedEm; - final Set qualifiers = new HashSet<>(suppliedQualifiers); + Set qualifiers = new HashSet<>(suppliedQualifiers); qualifiers.add(ContainerManaged.Literal.INSTANCE); qualifiers.add(JpaTransactionScoped.Literal.INSTANCE); qualifiers.remove(CdiTransactionScoped.Literal.INSTANCE); @@ -1257,11 +1180,11 @@ private void addJpaTransactionScopedEntityManagerBeans(final AfterBeanDiscovery } } - private void addNonTransactionalEntityManagerBeans(final AfterBeanDiscovery event, - final Set suppliedQualifiers, - final BeanManager beanManager) { - final String cn = JpaExtension.class.getName(); - final String mn = "addNonTransactionalEntityManagerBeans"; + private void addNonTransactionalEntityManagerBeans(AfterBeanDiscovery event, + Set suppliedQualifiers, + BeanManager beanManager) { + String cn = JpaExtension.class.getName(); + String mn = "addNonTransactionalEntityManagerBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, suppliedQualifiers}); } @@ -1275,7 +1198,7 @@ private void addNonTransactionalEntityManagerBeans(final AfterBeanDiscovery even // @NonTransactional // @Named("test") // private final EntityManager nonTransactionalEm; - final Set qualifiers = new HashSet<>(suppliedQualifiers); + Set qualifiers = new HashSet<>(suppliedQualifiers); qualifiers.removeAll(JpaCdiQualifiers.JPA_CDI_QUALIFIERS); qualifiers.add(NonTransactional.Literal.INSTANCE); if (this.nonTransactionalEntityManagerQualifiers.add(qualifiers)) { @@ -1287,12 +1210,9 @@ private void addNonTransactionalEntityManagerBeans(final AfterBeanDiscovery even // On its own line to ease debugging. return new NonTransactionalEntityManager(instance, suppliedQualifiers); }) - // Revisit: ReferenceCountedContext does not - // automatically pick up synthetic beans like this - // one. So we have to tell it somehow to "work on" - // this bean. Right now this bean is in what amounts - // to a thread-specific singleton scope. As it - // happens, this might actually be OK. + // Revisit: ReferenceCountedContext does not automatically pick up synthetic beans like this one. So we + // have to tell it somehow to "work on" this bean. Right now this bean is in what amounts to a + // thread-specific singleton scope. As it happens, this might actually be OK. .disposeWith((em, instance) -> { if (em.isOpen()) { em.close(); @@ -1305,11 +1225,11 @@ private void addNonTransactionalEntityManagerBeans(final AfterBeanDiscovery even } } - private void addExtendedEntityManagerBeans(final AfterBeanDiscovery event, - final Set suppliedQualifiers, - final BeanManager beanManager) { - final String cn = JpaExtension.class.getName(); - final String mn = "addExtendedEntityManagerBeans"; + private void addExtendedEntityManagerBeans(AfterBeanDiscovery event, + Set suppliedQualifiers, + BeanManager beanManager) { + String cn = JpaExtension.class.getName(); + String mn = "addExtendedEntityManagerBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, suppliedQualifiers, beanManager}); } @@ -1328,7 +1248,7 @@ private void addExtendedEntityManagerBeans(final AfterBeanDiscovery event, // @Synchronized // or @Unsynchronized, or none // @Named("test") // private final EntityManager extendedEm; - final Set qualifiers = new HashSet<>(suppliedQualifiers); + Set qualifiers = new HashSet<>(suppliedQualifiers); qualifiers.add(ContainerManaged.Literal.INSTANCE); qualifiers.add(Extended.Literal.INSTANCE); qualifiers.remove(JpaTransactionScoped.Literal.INSTANCE); @@ -1349,11 +1269,11 @@ private void addExtendedEntityManagerBeans(final AfterBeanDiscovery event, } } - private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, - final Collection providers) + private void processImplicitPersistenceUnits(AfterBeanDiscovery event, + Collection providers) throws ReflectiveOperationException { - final String cn = JpaExtension.class.getName(); - final String mn = "processImplicitPersistenceUnits"; + String cn = JpaExtension.class.getName(); + String mn = "processImplicitPersistenceUnits"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, providers}); } @@ -1362,7 +1282,7 @@ private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, int persistenceUnitCount = 0; PersistenceUnitInfoBean solePersistenceUnitInfoBean = null; - for (final PersistenceUnitInfoBean persistenceUnitInfoBean : this.implicitPersistenceUnits.values()) { + for (PersistenceUnitInfoBean persistenceUnitInfoBean : this.implicitPersistenceUnits.values()) { assert persistenceUnitInfoBean != null; String persistenceUnitName = persistenceUnitInfoBean.getPersistenceUnitName(); if (persistenceUnitName == null || persistenceUnitName.isEmpty()) { @@ -1370,10 +1290,10 @@ private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, this.defaultPersistenceUnitInEffect = true; } if (!persistenceUnitInfoBean.excludeUnlistedClasses()) { - final Collection> unlistedManagedClasses = + Collection> unlistedManagedClasses = this.unlistedManagedClassesByPersistenceUnitNames.get(persistenceUnitName); if (unlistedManagedClasses != null && !unlistedManagedClasses.isEmpty()) { - for (final Class unlistedManagedClass : unlistedManagedClasses) { + for (Class unlistedManagedClass : unlistedManagedClasses) { persistenceUnitInfoBean.addManagedClassName(unlistedManagedClass.getName()); } } @@ -1404,11 +1324,11 @@ private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, case 1: assert solePersistenceUnitInfoBean != null; - final String name = solePersistenceUnitInfoBean.getPersistenceUnitName(); + String name = solePersistenceUnitInfoBean.getPersistenceUnitName(); if (name != null && !name.isEmpty()) { this.defaultPersistenceUnitInEffect = true; this.addedDefaultPersistenceUnit = true; - final PersistenceUnitInfoBean instance = solePersistenceUnitInfoBean; + PersistenceUnitInfoBean instance = solePersistenceUnitInfoBean; event.addBean() .beanClass(PersistenceUnitInfoBean.class) .types(Collections.singleton(PersistenceUnitInfo.class)) @@ -1431,15 +1351,15 @@ private void processImplicitPersistenceUnits(final AfterBeanDiscovery event, } } - private void processPersistenceXmls(final AfterBeanDiscovery event, - final BeanManager beanManager, - final ClassLoader classLoader, - final Enumeration urls, - final Collection providers, - final boolean userSuppliedPersistenceUnitInfoBeans) + private void processPersistenceXmls(AfterBeanDiscovery event, + BeanManager beanManager, + ClassLoader classLoader, + Enumeration urls, + Collection providers, + boolean userSuppliedPersistenceUnitInfoBeans) throws IOException, JAXBException, ReflectiveOperationException, XMLStreamException { - final String cn = JpaExtension.class.getName(); - final String mn = "processPersistenceXmls"; + String cn = JpaExtension.class.getName(); + String mn = "processPersistenceXmls"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, beanManager, classLoader, urls, providers}); } @@ -1447,7 +1367,7 @@ private void processPersistenceXmls(final AfterBeanDiscovery event, Objects.requireNonNull(event); if (urls != null && urls.hasMoreElements()) { - final Supplier tempClassLoaderSupplier = () -> { + Supplier tempClassLoaderSupplier = () -> { if (classLoader instanceof URLClassLoader) { return new URLClassLoader(((URLClassLoader) classLoader).getURLs()); } else { @@ -1455,11 +1375,9 @@ private void processPersistenceXmls(final AfterBeanDiscovery event, } }; - // We use StAX for XML loading because it is the same XML - // parsing strategy used by all known CDI implementations. - // If the end user wants to customize the StAX - // implementation then we want that customization to apply - // here as well. + // We use StAX for XML loading because it is the same XML parsing strategy used by all known 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-- @@ -1474,38 +1392,37 @@ private void processPersistenceXmls(final AfterBeanDiscovery event, // 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. + // So we suppress deprecation warnings since deprecation in JDK 9 appears to have been a mistake. @SuppressWarnings("deprecation") - final XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); // See // https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.md#xmlinputfactory-a-stax-parser xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); - final Unmarshaller unmarshaller = + Unmarshaller unmarshaller = JAXBContext.newInstance(Persistence.class.getPackage().getName()).createUnmarshaller(); - final Supplier dataSourceProviderSupplier = + Supplier dataSourceProviderSupplier = () -> beanManager.createInstance().select(DataSourceProvider.class).get(); PersistenceUnitInfo solePersistenceUnitInfo = null; while (urls.hasMoreElements()) { - final URL url = urls.nextElement(); + URL url = urls.nextElement(); assert url != null; Collection persistenceUnitInfos = null; Persistence persistence = null; try (InputStream inputStream = new BufferedInputStream(url.openStream())) { - final XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(inputStream); + XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(inputStream); try { persistence = (Persistence) unmarshaller.unmarshal(reader); } finally { reader.close(); } } - final Collection persistenceUnits = persistence.getPersistenceUnit(); + Collection persistenceUnits = persistence.getPersistenceUnit(); if (persistenceUnits != null && !persistenceUnits.isEmpty()) { persistenceUnitInfos = new ArrayList<>(); - for (final Persistence.PersistenceUnit persistenceUnit : persistenceUnits) { + for (Persistence.PersistenceUnit persistenceUnit : persistenceUnits) { if (persistenceUnit != null) { persistenceUnitInfos .add(PersistenceUnitInfoBean.fromPersistenceUnit(persistenceUnit, @@ -1518,7 +1435,7 @@ private void processPersistenceXmls(final AfterBeanDiscovery event, } } if (persistenceUnitInfos != null && !persistenceUnitInfos.isEmpty()) { - for (final PersistenceUnitInfo persistenceUnitInfo : persistenceUnitInfos) { + for (PersistenceUnitInfo persistenceUnitInfo : persistenceUnitInfos) { String persistenceUnitName = persistenceUnitInfo.getPersistenceUnitName(); if (persistenceUnitName == null || persistenceUnitName.isEmpty()) { persistenceUnitName = DEFAULT_PERSISTENCE_UNIT_NAME; @@ -1544,11 +1461,11 @@ private void processPersistenceXmls(final AfterBeanDiscovery event, } } if (!userSuppliedPersistenceUnitInfoBeans && solePersistenceUnitInfo != null) { - final String name = solePersistenceUnitInfo.getPersistenceUnitName(); + String name = solePersistenceUnitInfo.getPersistenceUnitName(); if (name != null && !name.isEmpty() && !name.equals(DEFAULT_PERSISTENCE_UNIT_NAME)) { this.defaultPersistenceUnitInEffect = true; this.addedDefaultPersistenceUnit = true; - final PersistenceUnitInfo instance = solePersistenceUnitInfo; + PersistenceUnitInfo instance = solePersistenceUnitInfo; event.addBean() .beanClass(PersistenceUnitInfoBean.class) .types(Collections.singleton(PersistenceUnitInfo.class)) @@ -1569,31 +1486,27 @@ private void processPersistenceXmls(final AfterBeanDiscovery event, */ /** - * Returns {@code true} if the supplied {@link AnnotatedField} is - * annotated with {@link PersistenceContext}, is not annotated - * with {@link Inject} and has a type assignable to {@link - * EntityManager}. - * - * @param f the {@link AnnotatedField} in question; may be {@code - * null} in which case {@code false} will be returned - * - * @return {@code true} if the supplied {@link AnnotatedField} is - * annotated with {@link PersistenceContext}, is not annotated - * with {@link Inject} and has a type assignable to {@link - * EntityManager}; {@code false} in all other cases + * Returns {@code true} if the supplied {@link AnnotatedField} is annotated with {@link PersistenceContext}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManager}. + * + * @param f the {@link AnnotatedField} in question; may be {@code null} in which case {@code false} will be returned + * + * @return {@code true} if the supplied {@link AnnotatedField} is annotated with {@link PersistenceContext}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManager}; {@code false} in all other + * cases */ - private static boolean isEligiblePersistenceContextField(final AnnotatedField f) { - final String cn = JpaExtension.class.getName(); - final String mn = "isEligiblePersistenceContextField"; + private static boolean isEligiblePersistenceContextField(AnnotatedField f) { + String cn = JpaExtension.class.getName(); + String mn = "isEligiblePersistenceContextField"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, f); } - final boolean returnValue; + boolean returnValue; if (f != null && f.isAnnotationPresent(PersistenceContext.class) && !f.isAnnotationPresent(Inject.class)) { - final Type fieldType = f.getBaseType(); + Type fieldType = f.getBaseType(); returnValue = fieldType instanceof Class && EntityManager.class.isAssignableFrom((Class) fieldType); } else { returnValue = false; @@ -1606,31 +1519,27 @@ private static boolean isEligiblePersistenceContextField(final AnnotatedFiel } /** - * Returns {@code true} if the supplied {@link AnnotatedField} is - * annotated with {@link PersistenceUnit}, is not annotated - * with {@link Inject} and has a type assignable to {@link - * EntityManagerFactory}. + * Returns {@code true} if the supplied {@link AnnotatedField} is annotated with {@link PersistenceUnit}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManagerFactory}. * - * @param f the {@link AnnotatedField} in question; may be {@code - * null} in which case {@code false} will be returned + * @param f the {@link AnnotatedField} in question; may be {@code null} in which case {@code false} will be returned * - * @return {@code true} if the supplied {@link AnnotatedField} is - * annotated with {@link PersistenceUnit}, is not annotated with - * {@link Inject} and has a type assignable to {@link - * EntityManagerFactory}; {@code false} in all other cases + * @return {@code true} if the supplied {@link AnnotatedField} is annotated with {@link PersistenceUnit}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManagerFactory}; {@code false} in all + * other cases */ - private static boolean isEligiblePersistenceUnitField(final AnnotatedField f) { - final String cn = JpaExtension.class.getName(); - final String mn = "isEligiblePersistenceUnitField"; + private static boolean isEligiblePersistenceUnitField(AnnotatedField f) { + String cn = JpaExtension.class.getName(); + String mn = "isEligiblePersistenceUnitField"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, f); } - final boolean returnValue; + boolean returnValue; if (f != null && f.isAnnotationPresent(PersistenceUnit.class) && !f.isAnnotationPresent(Inject.class)) { - final Type fieldType = f.getBaseType(); + Type fieldType = f.getBaseType(); returnValue = fieldType instanceof Class && EntityManagerFactory.class.isAssignableFrom((Class) fieldType); } else { returnValue = false; @@ -1643,31 +1552,27 @@ private static boolean isEligiblePersistenceUnitField(final AnnotatedFieldPersistenceContext-annotated - * AnnotatedField} such that the resulting {@link - * AnnotatedField} is a true CDI injection point representing all - * the same information. + * Reconfigures annotations on an {@linkplain #isEligiblePersistenceContextField(AnnotatedField) eligible + * PersistenceContext-annotated AnnotatedField} such that the resulting {@link + * AnnotatedField} is a true CDI injection point representing all the same information. * - *

The original {@link PersistenceContext} annotation is - * removed.

+ *

The original {@link PersistenceContext} annotation is removed.

* - * @param fc the {@link AnnotatedFieldConfigurator} that allows - * the field to be re-annotated; must not be {@code null} + * @param fc the {@link AnnotatedFieldConfigurator} that allows the field to be re-annotated; must not be {@code + * null} * * @exception NullPointerException if {@code fc} is {@code null} */ - private void rewritePersistenceContextFieldAnnotations(final AnnotatedFieldConfigurator fc) { - final String cn = JpaExtension.class.getName(); - final String mn = "rewritePersistenceContextFieldAnnotations"; + private void rewritePersistenceContextFieldAnnotations(AnnotatedFieldConfigurator fc) { + String cn = JpaExtension.class.getName(); + String mn = "rewritePersistenceContextFieldAnnotations"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, fc); } Objects.requireNonNull(fc); - final PersistenceContext pc = fc.getAnnotated().getAnnotation(PersistenceContext.class); + PersistenceContext pc = fc.getAnnotated().getAnnotation(PersistenceContext.class); if (pc != null) { fc.remove(a -> a == pc); fc.add(InjectLiteral.INSTANCE); @@ -1683,7 +1588,7 @@ private void rewritePersistenceContextFieldAnnotations(final AnnotatedFieldC fc.add(Synchronized.Literal.INSTANCE); } if (LOGGER.isLoggable(Level.INFO)) { - final String name = pc.name().trim(); + String name = pc.name().trim(); if (!name.isEmpty()) { LOGGER.logp(Level.INFO, cn, mn, "persistenceContextNameIgnored", new Object[] {fc.getAnnotated(), name}); } @@ -1702,31 +1607,27 @@ private void rewritePersistenceContextFieldAnnotations(final AnnotatedFieldC } /** - * Reconfigures annotations on an {@linkplain - * #isEligiblePersistenceUnitField(AnnotatedField) eligible - * PersistenceUnit-annotated - * AnnotatedField} such that the resulting {@link - * AnnotatedField} is a true CDI injection point representing all - * the same information. + * Reconfigures annotations on an {@linkplain #isEligiblePersistenceUnitField(AnnotatedField) eligible + * PersistenceUnit-annotated AnnotatedField} such that the resulting {@link + * AnnotatedField} is a true CDI injection point representing all the same information. * - *

The original {@link PersistenceUnit} annotation is - * removed.

+ *

The original {@link PersistenceUnit} annotation is removed.

* - * @param fc the {@link AnnotatedFieldConfigurator} that allows - * the field to be re-annotated; must not be {@code null} + * @param fc the {@link AnnotatedFieldConfigurator} that allows the field to be re-annotated; must not be {@code + * null} * * @exception NullPointerException if {@code fc} is {@code null} */ - private void rewritePersistenceUnitFieldAnnotations(final AnnotatedFieldConfigurator fc) { - final String cn = JpaExtension.class.getName(); - final String mn = "rewritePersistenceUnitFieldAnnotations"; + private void rewritePersistenceUnitFieldAnnotations(AnnotatedFieldConfigurator fc) { + String cn = JpaExtension.class.getName(); + String mn = "rewritePersistenceUnitFieldAnnotations"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, fc); } Objects.requireNonNull(fc); - final PersistenceUnit pu = fc.getAnnotated().getAnnotation(PersistenceUnit.class); + PersistenceUnit pu = fc.getAnnotated().getAnnotation(PersistenceUnit.class); if (pu != null) { fc.remove(a -> a == pu); } @@ -1745,35 +1646,31 @@ private void rewritePersistenceUnitFieldAnnotations(final AnnotatedFieldConf } /** - * Returns {@code true} if the supplied {@link AnnotatedMethod} is - * annotated with {@link PersistenceContext}, is not annotated - * with {@link Inject} and has at least one parameter whose type - * is assignable to {@link EntityManager}. - * - * @param m the {@link AnnotatedMethod} in question; may be {@code - * null} in which case {@code false} will be returned - * - * @return {@code true} if the supplied {@link AnnotatedMethod} is - * annotated with {@link PersistenceContext}, is not annotated - * with {@link Inject} and has at least one parameter whose type - * is assignable to {@link EntityManager} + * Returns {@code true} if the supplied {@link AnnotatedMethod} is annotated with {@link PersistenceContext}, is not + * annotated with {@link Inject} and has at least one parameter whose type is assignable to {@link EntityManager}. + * + * @param m the {@link AnnotatedMethod} in question; may be {@code null} in which case {@code false} will be + * returned + * + * @return {@code true} if the supplied {@link AnnotatedMethod} is annotated with {@link PersistenceContext}, is not + * annotated with {@link Inject} and has at least one parameter whose type is assignable to {@link EntityManager} */ - private static boolean isEligiblePersistenceContextSetterMethod(final AnnotatedMethod m) { - final String cn = JpaExtension.class.getName(); - final String mn = "isEligiblePersistenceContextSetterMethod"; + private static boolean isEligiblePersistenceContextSetterMethod(AnnotatedMethod m) { + String cn = JpaExtension.class.getName(); + String mn = "isEligiblePersistenceContextSetterMethod"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, m); } - final boolean returnValue; + boolean returnValue; if (m != null && m.isAnnotationPresent(PersistenceContext.class) && !m.isAnnotationPresent(Inject.class)) { - final List> parameters = m.getParameters(); + List> parameters = m.getParameters(); if (parameters != null && !parameters.isEmpty()) { boolean temp = false; - for (final Annotated parameter : parameters) { - final Type type = parameter.getBaseType(); + for (Annotated parameter : parameters) { + Type type = parameter.getBaseType(); if (type instanceof Class && EntityManager.class.isAssignableFrom((Class) type)) { if (temp) { temp = false; @@ -1798,35 +1695,33 @@ private static boolean isEligiblePersistenceContextSetterMethod(final Annota } /** - * Returns {@code true} if the supplied {@link AnnotatedMethod} is - * annotated with {@link PersistenceUnit}, is not annotated with - * {@link Inject} and has at least one parameter whose type is - * assignable to {@link EntityManagerFactory}. - * - * @param m the {@link AnnotatedMethod} in question; may be {@code - * null} in which case {@code false} will be returned - * - * @return {@code true} if the supplied {@link AnnotatedMethod} is - * annotated with {@link PersistenceUnit}, is not annotated with - * {@link Inject} and has at least one parameter whose type is - * assignable to {@link EntityManagerFactory} + * Returns {@code true} if the supplied {@link AnnotatedMethod} is annotated with {@link PersistenceUnit}, is not + * annotated with {@link Inject} and has at least one parameter whose type is assignable to {@link + * EntityManagerFactory}. + * + * @param m the {@link AnnotatedMethod} in question; may be {@code null} in which case {@code false} will be + * returned + * + * @return {@code true} if the supplied {@link AnnotatedMethod} is annotated with {@link PersistenceUnit}, is not + * annotated with {@link Inject} and has at least one parameter whose type is assignable to {@link + * EntityManagerFactory} */ - private static boolean isEligiblePersistenceUnitSetterMethod(final AnnotatedMethod m) { - final String cn = JpaExtension.class.getName(); - final String mn = "isEligiblePersistenceUnitSetterMethod"; + private static boolean isEligiblePersistenceUnitSetterMethod(AnnotatedMethod m) { + String cn = JpaExtension.class.getName(); + String mn = "isEligiblePersistenceUnitSetterMethod"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, m); } - final boolean returnValue; + boolean returnValue; if (m != null && m.isAnnotationPresent(PersistenceUnit.class) && !m.isAnnotationPresent(Inject.class)) { - final List> parameters = m.getParameters(); + List> parameters = m.getParameters(); if (parameters != null && !parameters.isEmpty()) { boolean temp = false; - for (final Annotated parameter : parameters) { - final Type type = parameter.getBaseType(); + for (Annotated parameter : parameters) { + Type type = parameter.getBaseType(); if (type instanceof Class && EntityManagerFactory.class.isAssignableFrom((Class) type)) { if (temp) { temp = false; @@ -1850,37 +1745,37 @@ private static boolean isEligiblePersistenceUnitSetterMethod(final Annotated return returnValue; } - private void rewritePersistenceContextSetterMethodAnnotations(final AnnotatedMethodConfigurator mc) { - final String cn = JpaExtension.class.getName(); - final String mn = "rewritePersistenceContextSetterMethodAnnotations"; + private void rewritePersistenceContextSetterMethodAnnotations(AnnotatedMethodConfigurator mc) { + String cn = JpaExtension.class.getName(); + String mn = "rewritePersistenceContextSetterMethodAnnotations"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, mc); } Objects.requireNonNull(mc); - final Annotated annotated = mc.getAnnotated(); + Annotated annotated = mc.getAnnotated(); if (!annotated.isAnnotationPresent(Inject.class)) { - final PersistenceContext pc = annotated.getAnnotation(PersistenceContext.class); + PersistenceContext pc = annotated.getAnnotation(PersistenceContext.class); if (pc != null) { if (LOGGER.isLoggable(Level.INFO)) { - final String name = pc.name().trim(); + String name = pc.name().trim(); if (!name.isEmpty()) { LOGGER.logp(Level.INFO, cn, mn, "persistenceContextNameIgnored", new Object[] {annotated, name}); } } boolean observerMethod = false; - final List> parameters = mc.params(); + List> parameters = mc.params(); if (parameters != null && !parameters.isEmpty()) { - for (final AnnotatedParameterConfigurator apc : parameters) { - final Annotated parameter = apc.getAnnotated(); + for (AnnotatedParameterConfigurator apc : parameters) { + Annotated parameter = apc.getAnnotated(); if (parameter.isAnnotationPresent(Observes.class)) { if (!observerMethod) { observerMethod = true; } } else { - final Type parameterType = parameter.getBaseType(); + Type parameterType = parameter.getBaseType(); if (parameterType instanceof Class && EntityManager.class.isAssignableFrom((Class) parameterType)) { apc.add(ContainerManaged.Literal.INSTANCE); @@ -1915,31 +1810,31 @@ private void rewritePersistenceContextSetterMethodAnnotations(final Annotate } } - private void rewritePersistenceUnitSetterMethodAnnotations(final AnnotatedMethodConfigurator mc) { - final String cn = JpaExtension.class.getName(); - final String mn = "rewritePersistenceUnitSetterMethodAnnotations"; + private void rewritePersistenceUnitSetterMethodAnnotations(AnnotatedMethodConfigurator mc) { + String cn = JpaExtension.class.getName(); + String mn = "rewritePersistenceUnitSetterMethodAnnotations"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, mc); } Objects.requireNonNull(mc); - final Annotated annotated = mc.getAnnotated(); + Annotated annotated = mc.getAnnotated(); if (!annotated.isAnnotationPresent(Inject.class)) { - final PersistenceUnit pu = annotated.getAnnotation(PersistenceUnit.class); + PersistenceUnit pu = annotated.getAnnotation(PersistenceUnit.class); if (pu != null) { boolean observerMethod = false; - final List> parameters = mc.params(); + List> parameters = mc.params(); if (parameters != null && !parameters.isEmpty()) { - for (final AnnotatedParameterConfigurator apc : parameters) { - final Annotated parameter = apc.getAnnotated(); + for (AnnotatedParameterConfigurator apc : parameters) { + Annotated parameter = apc.getAnnotated(); if (parameter.isAnnotationPresent(Observes.class)) { if (!observerMethod) { observerMethod = true; } } else { - final Type parameterType = parameter.getBaseType(); + Type parameterType = parameter.getBaseType(); if (parameterType instanceof Class && EntityManagerFactory.class.isAssignableFrom((Class) parameterType)) { apc.add(ContainerManaged.Literal.INSTANCE); @@ -1966,13 +1861,13 @@ private void rewritePersistenceUnitSetterMethodAnnotations(final AnnotatedMe } } - private void maybeAddPersistenceProviderBeans(final AfterBeanDiscovery event, - final BeanManager beanManager, - final Set> preexistingPersistenceUnitInfoBeans, - final Collection providers) + private void maybeAddPersistenceProviderBeans(AfterBeanDiscovery event, + BeanManager beanManager, + Set> preexistingPersistenceUnitInfoBeans, + Collection providers) throws ReflectiveOperationException { - final String cn = JpaExtension.class.getName(); - final String mn = "maybeAddPersistenceProviderBeans"; + String cn = JpaExtension.class.getName(); + String mn = "maybeAddPersistenceProviderBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, beanManager, preexistingPersistenceUnitInfoBeans, providers}); } @@ -1981,37 +1876,25 @@ private void maybeAddPersistenceProviderBeans(final AfterBeanDiscovery event, Objects.requireNonNull(beanManager); Objects.requireNonNull(preexistingPersistenceUnitInfoBeans); - for (final Bean bean : preexistingPersistenceUnitInfoBeans) { + for (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. + 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 = + // 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. + CreationalContext cc = beanManager.createCreationalContext(preexistingPersistenceUnitInfoBean); - final PersistenceUnitInfo pui = preexistingPersistenceUnitInfoBean.create(cc); + PersistenceUnitInfo pui = preexistingPersistenceUnitInfoBean.create(cc); try { this.maybeAddPersistenceProviderBean(event, pui, providers); } finally { @@ -2026,16 +1909,16 @@ private void maybeAddPersistenceProviderBeans(final AfterBeanDiscovery event, } } - private static Collection addPersistenceProviderBeans(final AfterBeanDiscovery event) { - final String cn = JpaExtension.class.getName(); - final String mn = "addPersistenceProviderBeans"; + private static Collection addPersistenceProviderBeans(AfterBeanDiscovery event) { + String cn = JpaExtension.class.getName(); + String mn = "addPersistenceProviderBeans"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, event); } Objects.requireNonNull(event); - final PersistenceProviderResolver resolver = PersistenceProviderResolverHolder.getPersistenceProviderResolver(); + PersistenceProviderResolver resolver = PersistenceProviderResolverHolder.getPersistenceProviderResolver(); // Provide support for, e.g.: // @Inject @@ -2044,8 +1927,8 @@ private static Collection addPersistenceProviderB .types(PersistenceProviderResolver.class) .scope(ApplicationScoped.class) .createWith(cc -> resolver); - final Collection providers = resolver.getPersistenceProviders(); - for (final PersistenceProvider provider : providers) { + Collection providers = resolver.getPersistenceProviders(); + for (PersistenceProvider provider : providers) { // Provide support for, e.g.: // @Inject // private MyPersistenceProviderSubclassMaybeFromPersistenceXml ppr; @@ -2062,42 +1945,31 @@ private static Collection addPersistenceProviderB } /** - * 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 the supplied {@link - * Collection} of {@link PersistenceProvider}s does not contain - * an instance of it. - * - * @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 + * 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 the supplied {@link Collection} of {@link PersistenceProvider}s does not + * contain an instance of it. + * + * @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 void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, - final PersistenceUnitInfo persistenceUnitInfo, - final Collection providers) + private void maybeAddPersistenceProviderBean(AfterBeanDiscovery event, + PersistenceUnitInfo persistenceUnitInfo, + Collection providers) throws ReflectiveOperationException { - final String cn = JpaExtension.class.getName(); - final String mn = "maybeAddPersistenceProviderBean"; + String cn = JpaExtension.class.getName(); + String mn = "maybeAddPersistenceProviderBean"; if (LOGGER.isLoggable(Level.FINER)) { LOGGER.entering(cn, mn, new Object[] {event, persistenceUnitInfo, providers}); } @@ -2105,11 +1977,11 @@ private void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, Objects.requireNonNull(event); Objects.requireNonNull(persistenceUnitInfo); - final String providerClassName = persistenceUnitInfo.getPersistenceProviderClassName(); + String providerClassName = persistenceUnitInfo.getPersistenceProviderClassName(); if (providerClassName != null) { boolean add = true; if (providers != null && !providers.isEmpty()) { - for (final PersistenceProvider provider : providers) { + for (PersistenceProvider provider : providers) { if (provider != null && provider.getClass().getName().equals(providerClassName)) { add = false; break; @@ -2117,8 +1989,7 @@ private void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, } } if (add) { - // The PersistenceProvider class in question is not one we - // already loaded. Add a bean for it too. + // The PersistenceProvider class in question is not one we already loaded. Add a bean for it too. String persistenceUnitName = persistenceUnitInfo.getPersistenceUnitName(); if (persistenceUnitName == null || persistenceUnitName.isEmpty()) { persistenceUnitName = DEFAULT_PERSISTENCE_UNIT_NAME; @@ -2141,10 +2012,10 @@ private void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, } assert classLoader != null; @SuppressWarnings("unchecked") - final Class c = + Class c = (Class) Class.forName(providerClassName, true, classLoader); return c.getDeclaredConstructor().newInstance(); - } catch (final ReflectiveOperationException reflectiveOperationException) { + } catch (ReflectiveOperationException reflectiveOperationException) { throw new CreationException(reflectiveOperationException.getMessage(), reflectiveOperationException); } @@ -2157,22 +2028,18 @@ private void maybeAddPersistenceProviderBean(final AfterBeanDiscovery event, } } - private static void onStartup(@Observes - @Initialized(ApplicationScoped.class) - @Priority(LIBRARY_BEFORE) - final Object event, - @ContainerManaged - final Instance emfs) { + private void onStartup(@Observes + @Initialized(ApplicationScoped.class) + @Priority(LIBRARY_BEFORE) + Object event, + @ContainerManaged + Instance emfs) { if (!emfs.isUnsatisfied()) { - for (final EntityManagerFactory emfProxy : emfs) { - // Container-managed EntityManagerFactory instances - // are client proxies, so we call a business method to - // force "inflation" of the proxied instance. This, - // in turn, may run DDL and persistence provider - // validation if the persistence provider has been - // configured to do such things early (like - // Eclipselink with its eclipselink.deploy-on-startup - // property). + for (EntityManagerFactory emfProxy : emfs) { + // Container-managed EntityManagerFactory instances are client proxies, so we call a business method to + // force "inflation" of the proxied instance. This, in turn, may run DDL and persistence provider + // validation if the persistence provider has been configured to do such things early (like Eclipselink + // with its eclipselink.deploy-on-startup property). emfProxy.isOpen(); } } diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaTransactionScopedEntityManager.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaTransactionScopedEntityManager.java index 0db024200c7..e26f499fb81 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaTransactionScopedEntityManager.java +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JpaTransactionScopedEntityManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -215,10 +215,9 @@ final class JpaTransactionScopedEntityManager extends DelegatingEntityManager { * *

This method never returns {@code null}.

* - *

If a {@linkplain TransactionSupport#inTransaction() JTA - * transaction is active}, then an {@link EntityManager} that is - * joined to it is returned. Otherwise a non-transactional {@link - * EntityManager} is returned.

+ *

If a JTA transaction is active, then an + * {@link EntityManager}} that is joined to it is returned. Otherwise a + * non-transactional {@link EntityManager} is returned.

* *

Recall that this method is invoked by all {@link * DelegatingEntityManager} methods.

diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAbsentDataSourceProvider.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAbsentDataSourceProvider.java new file mode 100644 index 00000000000..7cc8fc7b09d --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAbsentDataSourceProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.sql.DataSource; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.literal.NamedLiteral; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +class JtaAbsentDataSourceProvider implements PersistenceUnitInfoBean.DataSourceProvider { + + + /* + * Instance fields. + */ + + + private final Instance instance; + + + /* + * Constructors. + */ + + + @Deprecated + JtaAbsentDataSourceProvider() { + this(null); + } + + @Inject + JtaAbsentDataSourceProvider(Instance instance) { + super(); + this.instance = instance; + } + + + /* + * Instance methods. + */ + + + @Override + public DataSource getDataSource(boolean jta, boolean useDefaultJta, String dataSourceName) { + Instance instance = this.instance; + if (dataSourceName == null) { + if (useDefaultJta) { + instance = null; + } + } else if (instance != null) { + instance = instance.select(NamedLiteral.of(dataSourceName)); + } + return instance == null ? null : instance.get(); + } +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAdaptingDataSourceProvider.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAdaptingDataSourceProvider.java new file mode 100644 index 00000000000..c790ff6ed40 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaAdaptingDataSourceProvider.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import javax.sql.DataSource; + +import io.helidon.integrations.jta.jdbc.ExceptionConverter; +import io.helidon.integrations.jta.jdbc.JtaAdaptingDataSource; + +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.literal.NamedLiteral; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +@Singleton +final class JtaAdaptingDataSourceProvider implements PersistenceUnitInfoBean.DataSourceProvider { + + + /* + * Static fields. + */ + + + /** + * A token to use as a key in the {@link #jtaDataSourcesByName} field value for a data source name when the real + * data source name is {@code null}. + * + *

Real data source names can be {@code null} and the empty string ({@code ""}), so a different value is used + * here.

+ * + *

This field is never {@code null}.

+ */ + private static final String NULL_DATASOURCE_NAME = "\u0000"; + + + /* + * Instance fields. + */ + + + private final ConcurrentMap jtaDataSourcesByName; + + /** + * An {@link Instance} providing access to CDI contextual references. + * + *

This field may be {@code null} if the {@linkplain #JtaAdaptingDataSourceProvider() deprecated zero-argument + * constructor of this class} is used.

+ */ + private final Instance objects; + + /** + * The {@link TransactionManager} for the system. + * + *

This field may be {@code null} if the {@linkplain #JtaAdaptingDataSourceProvider() deprecated zero-argument + * constructor of this class} is used.

+ */ + private final TransactionManager transactionManager; + + private final TransactionSynchronizationRegistry tsr; + + private final boolean interposedSynchronizations; + + private final boolean immediateEnlistment; + + private final ExceptionConverter exceptionConverter; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link JtaAdaptingDataSourceProvider}. + * + * @param objects an {@link Instance} providing access to CDI + * beans; must not be {@code null} + * + * @param transactionManager a {@link TransactionManager}; must + * not be {@code null} + * + * @param tsr a {@link TransactionSynchronizationRegistry}; must + * not be {@code null} + * + * @exception NullPointerException if either {@code objects} or + * {@code transactionManager} or {@code tsr} is {@code null} + */ + @Inject + JtaAdaptingDataSourceProvider(Instance objects, + TransactionManager transactionManager, + TransactionSynchronizationRegistry tsr) { + super(); + this.jtaDataSourcesByName = new ConcurrentHashMap<>(); + this.objects = Objects.requireNonNull(objects, "objects"); + this.transactionManager = Objects.requireNonNull(transactionManager, "transactionManager"); + this.tsr = Objects.requireNonNull(tsr, "tsr"); + this.interposedSynchronizations = + Boolean.parseBoolean(System.getProperty("helidon.jta.interposedSynchronizations", "true")); + this.immediateEnlistment = + Boolean.parseBoolean(System.getProperty("helidon.jta.immediateEnlistment", "false")); + Instance i = objects.select(ExceptionConverter.class); + this.exceptionConverter = i.isUnsatisfied() ? null : i.get(); + } + + + /* + * Instance methods. + */ + + + /** + * Supplies a {@link DataSource}. + * + *

This method may return {@code null}.

+ * + * @param jta if {@code true}, the {@link DataSource} that is returned will be enrolled in JTA 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} that will be enrolled in JTA 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} in edge cases + * + * @see PersistenceUnitInfoBean#getJtaDataSource() + * + * @see PersistenceUnitInfoBean#getNonJtaDataSource() + */ + @Override // PersistenceUnitInfoBean.DataSourceProvider + public DataSource getDataSource(boolean jta, boolean useDefaultJta, String dataSourceName) { + if (jta) { + if (dataSourceName == null) { + return useDefaultJta ? this.getDefaultJtaDataSource() : null; + } + return this.getNamedJtaDataSource(dataSourceName); + } + return dataSourceName == null ? null : this.getNamedNonJtaDataSource(dataSourceName); + } + + private JtaAdaptingDataSource getDefaultJtaDataSource() { + return + jtaDataSourcesByName.computeIfAbsent(NULL_DATASOURCE_NAME, + n -> new JtaAdaptingDataSource(this.transactionManager::getTransaction, + this.tsr, + this.interposedSynchronizations, + this.exceptionConverter, + this.objects.select(DataSource.class).get(), + this.immediateEnlistment)); + } + + private JtaAdaptingDataSource getNamedJtaDataSource(String name) { + return + jtaDataSourcesByName.computeIfAbsent(name, + n -> new JtaAdaptingDataSource(this.transactionManager::getTransaction, + this.tsr, + this.interposedSynchronizations, + this.exceptionConverter, + this.objects.select(DataSource.class, + NamedLiteral.of(n)).get(), + this.immediateEnlistment)); + } + + private DataSource getNamedNonJtaDataSource(String name) { + return this.objects.select(DataSource.class, NamedLiteral.of(name)).get(); + } + + @PreDestroy + private void clear() { + this.jtaDataSourcesByName.clear(); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaEntityManager.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaEntityManager.java new file mode 100644 index 00000000000..511f7c26b8c --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaEntityManager.java @@ -0,0 +1,934 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +import io.helidon.integrations.cdi.jpa.TransactionRegistry.CompletionStatus; + +import jakarta.persistence.EntityGraph; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.EntityTransaction; +import jakarta.persistence.FlushModeType; +import jakarta.persistence.LockModeType; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.Query; +import jakarta.persistence.StoredProcedureQuery; +import jakarta.persistence.SynchronizationType; +import jakarta.persistence.TransactionRequiredException; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.metamodel.Metamodel; + +import static jakarta.persistence.SynchronizationType.SYNCHRONIZED; +import static jakarta.persistence.SynchronizationType.UNSYNCHRONIZED; + +class JtaEntityManager extends DelegatingEntityManager { + + private static final Logger LOGGER = Logger.getLogger(JtaEntityManager.class.getName()); + + private static final ThreadLocal> AT_EMS = + ThreadLocal.withInitial(() -> new HashMap<>(5)); + + private final BiFunction, ? extends EntityManager> emf; + + private final SynchronizationType syncType; + + private final Map properties; + + private final BooleanSupplier activeTransaction; + + private final Consumer> completionListeners; + + private final Function transactionalResourceGetter; + + private final BiConsumer transactionalResourceSetter; + + JtaEntityManager(BooleanSupplier activeTransaction, + Consumer> completionListeners, + Function transactionalResourceGetter, + BiConsumer transactionalResourceSetter, + BiFunction, ? extends EntityManager> emf, + SynchronizationType syncType, + Map properties) { + super(); + this.activeTransaction = Objects.requireNonNull(activeTransaction, "activeTransaction"); + this.completionListeners = Objects.requireNonNull(completionListeners, "completionListeners"); + this.transactionalResourceGetter = Objects.requireNonNull(transactionalResourceGetter, "transactionalResourceGetter"); + this.transactionalResourceSetter = Objects.requireNonNull(transactionalResourceSetter, "transactionalResourceSetter"); + this.emf = Objects.requireNonNull(emf, "emf"); + // JPA permits null SynchronizationType and properties. + this.syncType = syncType; + if (syncType == null) { + this.properties = properties == null ? null : Map.copyOf(properties); + } else if (properties == null || properties.isEmpty()) { + this.properties = Map.of(SynchronizationType.class.getName(), syncType); + } else { + Map m = new LinkedHashMap<>(properties); + m.put(SynchronizationType.class.getName(), syncType); + this.properties = Collections.unmodifiableMap(m); + } + } + + void dispose() { + AbsentTransactionEntityManager em = AT_EMS.get().remove(this); + if (em != null) { + em.closeDelegate(); + } + } + + @Override + EntityManager acquireDelegate() { + try { + return + this.activeTransaction.getAsBoolean() + ? this.computeIfAbsentForActiveTransaction() + : this.computeIfAbsentForNoTransaction(); + } catch (PersistenceException e) { + throw e; + } catch (RuntimeException e) { + throw new PersistenceException(e.getMessage(), e); + } + } + + private ActiveTransactionEntityManager computeIfAbsentForActiveTransaction() { + ActiveTransactionEntityManager em = (ActiveTransactionEntityManager) this.transactionalResourceGetter.apply(this); + if (em == null) { + ActiveTransactionEntityManager newEm = + new ActiveTransactionEntityManager(this.emf.apply(this.syncType, this.properties)); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "computeIfAbsentForActiveTransaction", + "Created ActiveTransactionEntityManager delegate ({0})", newEm); + } + em = newEm; + Object thread = Thread.currentThread(); + try { + this.completionListeners.accept((Consumer) cts -> { + // Remember, this can be invoked asynchronously. + if (Thread.currentThread() == thread) { + newEm.closeDelegate(); + } else { + newEm.closePending = true; // volatile write + } + }); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "computeIfAbsentForActiveTransaction", + "Registered listener to close delegate ({0}) upon transaction completion", newEm); + } + this.transactionalResourceSetter.accept(this, em); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "computeIfAbsentForActiveTransaction", + "Registered delegate ({0})", em); + } + } catch (RuntimeException | Error e) { + try { + em.closeDelegate(); + } catch (RuntimeException | Error e2) { + e.addSuppressed(e2); + } + throw e; + } + } else if ((this.syncType == null || this.syncType == SYNCHRONIZED) && synchronizationType(em) == UNSYNCHRONIZED) { + // Check for mixed synchronization types per section 7.6.4.1 + // (https://jakarta.ee/specifications/persistence/3.1/jakarta-persistence-spec-3.1.html#a11820): + // + // "If there is a persistence context of type SynchronizationType.UNSYNCHRONIZED associated with the JTA + // transaction and the target component specifies a persistence context of type + // SynchronizationType.SYNCHRONIZED, the IllegalStateException is thrown by the container." + throw new IllegalStateException("SynchronizationType.UNSYNCHRONIZED EntityManager already associated"); + } + return em; + } + + private AbsentTransactionEntityManager computeIfAbsentForNoTransaction() { + Map ems = AT_EMS.get(); + AbsentTransactionEntityManager em = ems.get(this); + if (em == null) { + // This AbsentTransactionEntityManager is closed by the dispose() method above. + em = new AbsentTransactionEntityManager(this.emf.apply(syncType, properties)); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "computeIfAbsentForNoTransaction", + "Created AbsentTransactionEntityManager delegate ({0})", em); + } + ems.put(this, em); + } + return em; + } + + + /* + * Static methods. + */ + + + private static SynchronizationType synchronizationType(EntityManager em) { + Map properties = em.getProperties(); + return properties == null ? null : (SynchronizationType) properties.get(SynchronizationType.class.getName()); + } + + + /* + * Inner and nested classes. + */ + + + private static final class ActiveTransactionEntityManager extends DelegatingEntityManager { + + private volatile boolean closePending; + + private ActiveTransactionEntityManager(EntityManager delegate) { + super(Objects.requireNonNull(delegate, "delegate")); + } + + private void closeIfPending() { + if (this.closePending) { + super.close(); + } + } + + @Override + public void persist(Object entity) { + this.closeIfPending(); + super.persist(entity); + } + + @Override + public T merge(T entity) { + this.closeIfPending(); + return super.merge(entity); + } + + @Override + public void remove(Object entity) { + this.closeIfPending(); + super.remove(entity); + } + + @Override + public T find(Class entityClass, Object primaryKey) { + this.closeIfPending(); + return super.find(entityClass, primaryKey); + } + + @Override + public T find(Class entityClass, Object primaryKey, Map properties) { + this.closeIfPending(); + return super.find(entityClass, primaryKey, properties); + } + + @Override + public T find(Class entityClass, Object primaryKey, LockModeType lockMode) { + this.closeIfPending(); + return super.find(entityClass, primaryKey, lockMode); + } + + @Override + public T find(Class entityClass, Object primaryKey, LockModeType lockMode, Map properties) { + this.closeIfPending(); + return super.find(entityClass, primaryKey, lockMode, properties); + } + + @Override + public T getReference(Class entityClass, Object primaryKey) { + this.closeIfPending(); + return super.getReference(entityClass, primaryKey); + } + + @Override + public void flush() { + this.closeIfPending(); + super.flush(); + } + + @Override + public void setFlushMode(FlushModeType flushMode) { + this.closeIfPending(); + super.setFlushMode(flushMode); + } + + @Override + public FlushModeType getFlushMode() { + this.closeIfPending(); + return super.getFlushMode(); + } + + @Override + public void lock(Object entity, LockModeType lockMode) { + this.closeIfPending(); + super.lock(entity, lockMode); + } + + @Override + public void lock(Object entity, LockModeType lockMode, Map properties) { + this.closeIfPending(); + super.lock(entity, lockMode, properties); + } + + @Override + public void refresh(Object entity) { + this.closeIfPending(); + super.refresh(entity); + } + + @Override + public void refresh(Object entity, Map properties) { + this.closeIfPending(); + super.refresh(entity, properties); + } + + @Override + public void refresh(Object entity, LockModeType lockMode) { + this.closeIfPending(); + super.refresh(entity, lockMode); + } + + @Override + public void refresh(Object entity, LockModeType lockMode, Map properties) { + this.closeIfPending(); + super.refresh(entity, lockMode, properties); + } + + @Override + public void clear() { + this.closeIfPending(); + super.clear(); + } + + @Override + public void detach(Object entity) { + this.closeIfPending(); + super.detach(entity); + } + + @Override + public boolean contains(Object entity) { + this.closeIfPending(); + return super.contains(entity); + } + + @Override + public LockModeType getLockMode(Object entity) { + this.closeIfPending(); + return super.getLockMode(entity); + } + + @Override + public void setProperty(String propertyName, Object propertyValue) { + this.closeIfPending(); + super.setProperty(propertyName, propertyValue); + } + + @Override + public Map getProperties() { + this.closeIfPending(); + return super.getProperties(); + } + + @Override + public Query createQuery(String qlString) { + this.closeIfPending(); + return super.createQuery(qlString); + } + + @Override + public TypedQuery createQuery(CriteriaQuery criteriaQuery) { + this.closeIfPending(); + return super.createQuery(criteriaQuery); + } + + @Override + @SuppressWarnings("rawtypes") + public Query createQuery(CriteriaUpdate criteriaUpdate) { + this.closeIfPending(); + return super.createQuery(criteriaUpdate); + } + + @Override + @SuppressWarnings("rawtypes") + public Query createQuery(CriteriaDelete criteriaDelete) { + this.closeIfPending(); + return super.createQuery(criteriaDelete); + } + + @Override + public TypedQuery createQuery(String qlString, Class resultClass) { + this.closeIfPending(); + return super.createQuery(qlString, resultClass); + } + + @Override + public Query createNamedQuery(String sqlString) { + this.closeIfPending(); + return super.createNamedQuery(sqlString); + } + + @Override + public TypedQuery createNamedQuery(String sqlString, Class resultClass) { + this.closeIfPending(); + return super.createNamedQuery(sqlString, resultClass); + } + + @Override + public Query createNativeQuery(String sqlString) { + this.closeIfPending(); + return super.createNativeQuery(sqlString); + } + + @Override + @SuppressWarnings("rawtypes") + public Query createNativeQuery(String sqlString, Class resultClass) { + this.closeIfPending(); + return super.createNativeQuery(sqlString, resultClass); + } + + @Override + public Query createNativeQuery(String sqlString, String resultSetMapping) { + this.closeIfPending(); + return super.createNativeQuery(sqlString, resultSetMapping); + } + + @Override + public StoredProcedureQuery createNamedStoredProcedureQuery(String procedureName) { + this.closeIfPending(); + return super.createNamedStoredProcedureQuery(procedureName); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName) { + this.closeIfPending(); + return super.createStoredProcedureQuery(procedureName); + } + + @Override + @SuppressWarnings("rawtypes") + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class... resultClasses) { + this.closeIfPending(); + return super.createStoredProcedureQuery(procedureName, resultClasses); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings) { + this.closeIfPending(); + return super.createStoredProcedureQuery(procedureName, resultSetMappings); + } + + @Override + public void joinTransaction() { + this.closeIfPending(); + super.joinTransaction(); + } + + @Override + public boolean isJoinedToTransaction() { + this.closeIfPending(); + return super.isJoinedToTransaction(); + } + + @Override + public T unwrap(Class c) { + this.closeIfPending(); + if (c != null && c.isInstance(this)) { + return c.cast(this); + } + return super.unwrap(c); + } + + @Override + public Object getDelegate() { + this.closeIfPending(); + return super.getDelegate(); + } + + private void closeDelegate() { + super.close(); + this.closePending = false; + } + + @Override + public void close() { + if (this.isOpen()) { + throw new IllegalStateException("close() cannot be called on a container-managed EntityManager"); + } + super.close(); + } + + @Override + public boolean isOpen() { + this.closeIfPending(); + return super.isOpen(); + } + + @Override + public EntityTransaction getTransaction() { + this.closeIfPending(); + return super.getTransaction(); + } + + @Override + public EntityManagerFactory getEntityManagerFactory() { + this.closeIfPending(); + return super.getEntityManagerFactory(); + } + + @Override + public CriteriaBuilder getCriteriaBuilder() { + this.closeIfPending(); + return super.getCriteriaBuilder(); + } + + @Override + public Metamodel getMetamodel() { + this.closeIfPending(); + return super.getMetamodel(); + } + + @Override + public EntityGraph createEntityGraph(Class rootType) { + this.closeIfPending(); + return super.createEntityGraph(rootType); + } + + @Override + public EntityGraph createEntityGraph(String graphName) { + this.closeIfPending(); + return super.createEntityGraph(graphName); + } + + @Override + public EntityGraph getEntityGraph(String graphName) { + this.closeIfPending(); + return super.getEntityGraph(graphName); + } + + @Override + public List> getEntityGraphs(Class entityClass) { + this.closeIfPending(); + return super.getEntityGraphs(entityClass); + } + + } + + private static final class ClearingQuery extends DelegatingQuery { + + private final Runnable clearer; + + private ClearingQuery(Runnable persistenceContextClearer, Query delegate) { + super(delegate); + this.clearer = Objects.requireNonNull(persistenceContextClearer, "persistenceContextClearer"); + } + + @Override + @SuppressWarnings("rawtypes") + public List getResultList() { + try { + return super.getResultList(); + } finally { + this.clearer.run(); + } + } + + @Override + public Object getSingleResult() { + try { + return super.getSingleResult(); + } finally { + this.clearer.run(); + } + } + + } + + private static final class ClearingStoredProcedureQuery extends DelegatingStoredProcedureQuery { + + private final Runnable clearer; + + private ClearingStoredProcedureQuery(Runnable persistenceContextClearer, StoredProcedureQuery delegate) { + super(delegate); + this.clearer = Objects.requireNonNull(persistenceContextClearer, "persistenceContextClearer"); + } + + @Override + @SuppressWarnings("rawtypes") + public List getResultList() { + try { + return super.getResultList(); + } finally { + this.clearer.run(); + } + } + + @Override + public Object getSingleResult() { + try { + return super.getSingleResult(); + } finally { + this.clearer.run(); + } + } + + } + + private static final class ClearingTypedQuery extends DelegatingTypedQuery { + + private final Runnable clearer; + + private ClearingTypedQuery(Runnable persistenceContextClearer, TypedQuery delegate) { + super(delegate); + this.clearer = Objects.requireNonNull(persistenceContextClearer, "persistenceContextClearer"); + } + + @Override + public List getResultList() { + try { + return super.getResultList(); + } finally { + this.clearer.run(); + } + } + + @Override + public X getSingleResult() { + try { + return super.getSingleResult(); + } finally { + this.clearer.run(); + } + } + + } + + private static final class AbsentTransactionEntityManager extends DelegatingEntityManager { + + AbsentTransactionEntityManager(EntityManager delegate) { + super(Objects.requireNonNull(delegate, "delegate")); + } + + @Override + public void close() { + if (this.isOpen()) { + throw new IllegalStateException("close() cannot be called on a container-managed EntityManager"); + } + super.close(); + } + + private void closeDelegate() { + super.close(); + } + + @Override + public TypedQuery createNamedQuery(String name, Class resultClass) { + return new ClearingTypedQuery<>(this::clear, super.createNamedQuery(name, resultClass)); + } + + @Override + public TypedQuery createQuery(CriteriaQuery criteriaQuery) { + return new ClearingTypedQuery<>(this::clear, super.createQuery(criteriaQuery)); + } + + @Override + public TypedQuery createQuery(String jpql, Class resultClass) { + return new ClearingTypedQuery<>(this::clear, super.createQuery(jpql, resultClass)); + } + + @Override + public T find(Class entityClass, Object primaryKey, Map properties) { + try { + return super.find(entityClass, primaryKey, properties); + } finally { + this.clear(); + } + } + + @Override + public T find(Class entityClass, Object primaryKey) { + try { + return super.find(entityClass, primaryKey); + } finally { + this.clear(); + } + } + + @Override + public Query createNamedQuery(String name) { + return new ClearingQuery(this::clear, super.createNamedQuery(name)); + } + + @Override + @SuppressWarnings("rawtypes") + public Query createNativeQuery(String sql, Class resultClass) { + return new ClearingQuery(this::clear, super.createNativeQuery(sql, resultClass)); + } + + @Override + public Query createNativeQuery(String sql, String resultSetMapping) { + return new ClearingQuery(this::clear, super.createNativeQuery(sql, resultSetMapping)); + } + + @Override + public Query createNativeQuery(String sql) { + return new ClearingQuery(this::clear, super.createNativeQuery(sql)); + } + + @Override + public Query createQuery(String jpql) { + return new ClearingQuery(this::clear, super.createQuery(jpql)); + } + + @Override + public T getReference(Class entityClass, Object primaryKey) { + try { + return super.getReference(entityClass, primaryKey); + } finally { + this.clear(); + } + } + + @Override + public StoredProcedureQuery createNamedStoredProcedureQuery(String name) { + return new ClearingStoredProcedureQuery(this::clear, super.createNamedStoredProcedureQuery(name)); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName) { + return new ClearingStoredProcedureQuery(this::clear, super.createStoredProcedureQuery(procedureName)); + } + + @Override + @SuppressWarnings("rawtypes") + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class... resultClasses) { + return new ClearingStoredProcedureQuery(this::clear, super.createStoredProcedureQuery(procedureName, resultClasses)); + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings) { + return + new ClearingStoredProcedureQuery(this::clear, super.createStoredProcedureQuery(procedureName, resultSetMappings)); + } + + /** + * Returns {@code false} when invoked. + * + * @return {@code false} in all cases + */ + @Override + public boolean isJoinedToTransaction() { + return false; + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void joinTransaction() { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void persist(Object entity) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public T merge(T entity) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void remove(Object entity) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void refresh(Object entity) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @param properties ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void refresh(Object entity, Map properties) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @param lockMode ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void refresh(Object entity, LockModeType lockMode) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @param lockMode ignored + * + * @param properties ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void refresh(Object entity, LockModeType lockMode, Map properties) { + throw new TransactionRequiredException(); + } + + @Override + public T find(Class entityClass, Object primaryKey, LockModeType lockMode) { + if (lockMode != null && lockMode != LockModeType.NONE) { + throw new TransactionRequiredException(); + } + try { + return super.find(entityClass, primaryKey, lockMode); + } finally { + this.clear(); + } + } + + @Override + public T find(Class entityClass, Object primaryKey, LockModeType lockMode, Map properties) { + if (lockMode != null && !lockMode.equals(LockModeType.NONE)) { + throw new TransactionRequiredException(); + } + try { + return super.find(entityClass, primaryKey, lockMode, properties); + } finally { + this.clear(); + } + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @param lockMode ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void lock(Object entity, LockModeType lockMode) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @param lockMode ignored + * + * @param properties ignored + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void lock(Object entity, LockModeType lockMode, Map properties) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @param entity ignored + * + * @return nothing + * + * @exception TransactionRequiredException when invoked + */ + @Override + public LockModeType getLockMode(Object entity) { + throw new TransactionRequiredException(); + } + + /** + * Throws a {@link TransactionRequiredException} when invoked. + * + * @exception TransactionRequiredException when invoked + */ + @Override + public void flush() { + // See + // https://github.com/javaee/glassfish/blob/f9e1f6361dcc7998cacccb574feef5b70bf84e23/appserver/common/container-common/src/main/java/com/sun/enterprise/container/common/impl/EntityManagerWrapper.java#L429-L430 + // but also note that Wildfly does *not* do this: + // https://github.com/wildfly/wildfly/blob/cb3f5429e4bb5423236564c1f3afd8b4a2430ec0/jpa/subsystem/src/main/java/org/jboss/as/jpa/container/AbstractEntityManager.java#L454-L466. + // We follow the reference application (Glassfish). + throw new TransactionRequiredException(); + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaExtendedEntityManager.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaExtendedEntityManager.java new file mode 100644 index 00000000000..67821e897f4 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaExtendedEntityManager.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceException; + +/** + * A {@link DelegatingEntityManager} created to support extended + * persistence contexts. + */ +final class JtaExtendedEntityManager extends DelegatingEntityManager { + + private final EntityManager delegate; + + private final boolean isSynchronized; + + private final BooleanSupplier activeTransaction; + + private final Function transactionalResourceGetter; + + private final BiConsumer transactionalResourceSetter; + + JtaExtendedEntityManager(BooleanSupplier activeTransaction, + Function transactionalResourceGetter, + BiConsumer transactionalResourceSetter, + EntityManager delegate, + boolean isSynchronized) { + super(); + this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.activeTransaction = Objects.requireNonNull(activeTransaction, "activeTransaction"); + this.transactionalResourceGetter = Objects.requireNonNull(transactionalResourceGetter, "transactionalResourceGetter"); + this.transactionalResourceSetter = Objects.requireNonNull(transactionalResourceSetter, "transactionalResourceSetter"); + this.isSynchronized = isSynchronized; + } + + @Override + protected EntityManager acquireDelegate() { + try { + if (this.activeTransaction.getAsBoolean()) { + Object emf = this.delegate.getEntityManagerFactory(); + Object extantEm = this.transactionalResourceGetter.apply(emf); + if (extantEm == null) { + this.transactionalResourceSetter.accept(emf, this); + if (this.isSynchronized) { + this.delegate.joinTransaction(); + } + } else if (extantEm != this) { + throw new PersistenceException(); + } + } + } catch (PersistenceException e) { + throw e; + } catch (RuntimeException e) { + throw new PersistenceException(e.getMessage(), e); + } + return this.delegate; + } + + @Override + public void close() { + if (this.isOpen()) { + throw new IllegalStateException("close() cannot be called on a container-managed EntityManager"); + } + super.close(); + } + + void dispose() { + this.delegate.close(); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionRegistry.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionRegistry.java new file mode 100644 index 00000000000..23566895c10 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionRegistry.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.Objects; +import java.util.function.Consumer; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.transaction.Status; +import jakarta.transaction.TransactionSynchronizationRegistry; + +@Singleton +final class JtaTransactionRegistry implements TransactionRegistry { + + private final TransactionSynchronizationRegistry tsr; + + @Inject + JtaTransactionRegistry(TransactionSynchronizationRegistry tsr) { + super(); + this.tsr = Objects.requireNonNull(tsr, "tsr"); + } + + @Override + public boolean active() { + return this.tsr.getTransactionStatus() == Status.STATUS_ACTIVE; + } + + @Override + public void addCompletionListener(Consumer cl) { + this.tsr.registerInterposedSynchronization(new AfterCompletionSynchronization(cs -> { + switch (cs) { + case Status.STATUS_COMMITTED: + cl.accept(CompletionStatus.COMMITTED); + break; + case Status.STATUS_ROLLEDBACK: + cl.accept(CompletionStatus.ROLLED_BACK); + break; + default: + throw new AssertionError(); + }})); + } + + @Override + public Object get(Object k) { + return this.tsr.getResource(k); + } + + @Override + public void put(Object k, Object v) { + this.tsr.putResource(k, v); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionScoped.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionScoped.java new file mode 100644 index 00000000000..9b2bd83df61 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/JtaTransactionScoped.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +/** + * A {@link Qualifier} indicating that the qualified bean's instances + * are associated with a JTA transaction with very specific semantics. + * + *

This qualifier must not be combined with {@link Extended}.

+ */ +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +@Target({}) // can only be programmatically added +@interface JtaTransactionScoped { + + /** + * An {@link AnnotationLiteral} that implements {@link + * JtaTransactionScoped}. + */ + final class Literal extends AnnotationLiteral implements JtaTransactionScoped { + + /** + * The version of this class for serialization purposes. + */ + private static final long serialVersionUID = 1L; + + /** + * The sole instance of this class. + */ + static final JtaTransactionScoped INSTANCE = new Literal(); + + /** + * Creates a new {@link Literal}. + */ + private Literal() { + super(); + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/NoTransactionRegistry.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/NoTransactionRegistry.java new file mode 100644 index 00000000000..84f0602135f --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/NoTransactionRegistry.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.function.Consumer; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +@Singleton +final class NoTransactionRegistry implements TransactionRegistry { + + @Inject + NoTransactionRegistry() { + super(); + } + + @Override + public boolean active() { + return false; + } + + @Override + public void addCompletionListener(Consumer cl) { + throw new IllegalStateException(); + } + + @Override + public Object get(Object k) { + throw new IllegalStateException(); + } + + @Override + public void put(Object k, Object v) { + throw new IllegalStateException(); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java new file mode 100644 index 00000000000..31767003a1d --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/PersistenceExtension.java @@ -0,0 +1,1705 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.security.CodeSource; +import java.security.ProtectionDomain; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; + +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 jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.CreationException; +import jakarta.enterprise.inject.InjectionException; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Vetoed; +import jakarta.enterprise.inject.literal.InjectLiteral; +import jakarta.enterprise.inject.literal.NamedLiteral; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.AfterTypeDiscovery; +import jakarta.enterprise.inject.spi.Annotated; +import jakarta.enterprise.inject.spi.AnnotatedCallable; +import jakarta.enterprise.inject.spi.AnnotatedType; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanAttributes; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.BeforeBeanDiscovery; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.ProcessAnnotatedType; +import jakarta.enterprise.inject.spi.ProcessBeanAttributes; +import jakarta.enterprise.inject.spi.ProcessInjectionPoint; +import jakarta.enterprise.inject.spi.WithAnnotations; +import jakarta.enterprise.inject.spi.configurator.AnnotatedFieldConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedMethodConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedParameterConfigurator; +import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator; +import jakarta.enterprise.util.TypeLiteral; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.persistence.Converter; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceProperty; +import jakarta.persistence.PersistenceUnit; +import jakarta.persistence.SynchronizationType; +import jakarta.persistence.spi.PersistenceProvider; +import jakarta.persistence.spi.PersistenceProviderResolver; +import jakarta.persistence.spi.PersistenceProviderResolverHolder; +import jakarta.persistence.spi.PersistenceUnitInfo; +import jakarta.xml.bind.JAXBContext; +import jakarta.xml.bind.JAXBException; +import jakarta.xml.bind.Unmarshaller; + +import static jakarta.interceptor.Interceptor.Priority.LIBRARY_AFTER; +import static jakarta.persistence.PersistenceContextType.EXTENDED; +import static jakarta.persistence.SynchronizationType.SYNCHRONIZED; +import static jakarta.persistence.SynchronizationType.UNSYNCHRONIZED; + +/** + * An {@link Extension} that integrates container-mode Jakarta Persistence + * 3.0 into CDI SE 3.0-based + * applications. + */ +public final class PersistenceExtension implements Extension { + + + /* + * Static fields. + */ + + + private static final TypeLiteral> BEAN_ENTITYMANAGERFACTORY_TYPELITERAL = new TypeLiteral<>() {}; + + private static final TypeLiteral> BEAN_JTAEXTENDEDENTITYMANAGER_TYPELITERAL = + new TypeLiteral<>() {}; + + private static final TypeLiteral> BEAN_JTAENTITYMANAGER_TYPELITERAL = new TypeLiteral<>() {}; + + /** + * The name used to designate the only persistence unit in the environment, when there is exactly one persistence + * unit in the environment, and there is at least one {@link PersistenceContext @PersistenceContext}-annotated + * injection point that does not specify a value for the {@link PersistenceContext#unitName() unitName} element. + * + *

In such a case, the injection point will be effectively rewritten such that it will appear to the CDI + * container as though there were a value specified for the {@link PersistenceContext#unitName() unitName} + * element—namely this field's value. Additionally, a bean identical to the existing solitary {@link + * PersistenceUnitInfo}-typed bean will be added with this field's value as the {@linkplain Named#value() value of + * its Named qualifier}, thus serving as a kind of alias for the "real" bean.

+ * + *

This is necessary because the empty string ({@code ""}) as the value of the {@link Named#value()} element can + * have special + * semantics, so cannot be used to designate an unnamed or otherwise default persistence unit.

+ * + *

The value of this field is subject to change without prior notice at any point. In general the mechanics + * around injection point rewriting are also subject to change without prior notice at any point.

+ */ + static final String DEFAULT_PERSISTENCE_UNIT_NAME = "__DEFAULT__"; + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private static final Logger LOGGER = Logger.getLogger(PersistenceExtension.class.getName()); + + + /* + * Instance fields. + */ + + + /** + * Whether or not this extension will do anything. + * + *

This field's value is {@code true} by default.

+ */ + private final boolean enabled; + + /** + * 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.

+ * + *

After this portable extension finishes processing, this {@link Map} will be {@linkplain Map#isEmpty() + * empty}.

+ * + * @see #gatherImplicitPersistenceUnits(ProcessAnnotatedType, 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}). + * + *

After this portable extension finishes processing, this {@link Map} will be {@linkplain Map#isEmpty() + * empty}.

+ * + *

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>> unlistedManagedClassesByUnitNames; + + /** + * A {@link Set} of {@link Set}s of CDI qualifiers annotating CDI injection points related to JPA. + * + *

This field is never {@code null}.

+ * + *

These qualifiers are built up as this portable extension {@linkplain ProcessInjectionPoint discovers {@code + * EntityManager}-typed InjectionPoints}. After this portable extension finishes processing, this + * {@link Set} will be {@linkplain Set#isEmpty() empty}.

+ * + * @see #saveContainerManagedEntityManagerQualifiers(ProcessInjectionPoint) + */ + private final Set> containerManagedEntityManagerQualifiers; + + /** + * A {@link Set} of {@link Set}s of CDI qualifiers for which container-managed {@link EntityManagerFactory} beans + * may be created. + * + *

This field is never {@code null}.

+ * + *

These qualifiers are built up as this portable extension {@linkplain ProcessInjectionPoint discovers {@code + * EntityManagerFactory}-typed InjectionPoints}. After this portable extension finishes processing, + * this {@link Set} will be {@linkplain Set#isEmpty() empty}.

+ * + * @see #saveContainerManagedEntityManagerFactoryQualifiers(ProcessInjectionPoint) + */ + private final Set> entityManagerFactoryQualifiers; + + /** + * Indicates if JTA transactions can be supported. + * + * @see #addAllocatorBeanAndInstallTransactionSupport(AfterTypeDiscovery) + */ + private boolean transactionsSupported; + + /** + * A {@link Set} of {@link Set}s of CDI qualifiers that serves as a kind of cache, preventing more than one {@link + * ContainerManaged}-qualified {@link EntityManagerFactory}-typed bean from being added for the same set of + * qualifiers. + * + *

This field is never {@code null}.

+ * + *

After this portable extension finishes processing, this {@link Set} will be {@linkplain Set#isEmpty() + * empty}.

+ * + * @see #addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery, Set) + */ + private final Set> containerManagedEntityManagerFactoryQualifiers; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link PersistenceExtension}. + * + * @deprecated For invocation by CDI only. + */ + @Deprecated // For invocation by CDI only. + public PersistenceExtension() { + super(); + this.enabled = Boolean.parseBoolean(System.getProperty(this.getClass().getName() + ".enabled", "false")); + this.containerManagedEntityManagerQualifiers = new HashSet<>(); + this.containerManagedEntityManagerFactoryQualifiers = new HashSet<>(); + this.entityManagerFactoryQualifiers = new HashSet<>(); + this.implicitPersistenceUnits = new HashMap<>(); + this.transactionsSupported = false; + this.unlistedManagedClassesByUnitNames = new HashMap<>(); + } + + + /* + * Observer methods. + */ + + + private void addTypes(@Observes AfterTypeDiscovery event) { + if (!this.enabled) { + return; + } + event.addAnnotatedType(ReferenceCountingProducer.class, ReferenceCountingProducer.class.getName()); + try { + Class.forName("jakarta.transaction.TransactionSynchronizationRegistry", + false, + Thread.currentThread().getContextClassLoader()); + event.addAnnotatedType(JtaTransactionRegistry.class, JtaTransactionRegistry.class.getName()); + event.addAnnotatedType(JtaAdaptingDataSourceProvider.class, JtaAdaptingDataSourceProvider.class.getName()); + this.transactionsSupported = true; + } catch (ClassNotFoundException e) { + event.addAnnotatedType(NoTransactionRegistry.class, NoTransactionRegistry.class.getName()); + event.addAnnotatedType(JtaAbsentDataSourceProvider.class, JtaAbsentDataSourceProvider.class.getName()); + this.transactionsSupported = false; + } + } + + private void makePersistencePropertyARepeatableQualifier(@Observes BeforeBeanDiscovery event) { + if (!this.enabled) { + return; + } + event.addQualifier(PersistenceProperty.class); + } + + /** + * {@linkplain ProcessBeanAttributes#veto() Vetoes} any bean whose bean types includes the deprecated {@link + * JtaDataSourceProvider} class, since it is replaced by {@link JtaAdaptingDataSourceProvider}. + * + * @param event the {@link ProcessBeanAttributes} event in question; must not be {@code null} + * + * @exception NullPointerException if {@code event} is {@code null} + * + * @see JtaAdaptingDataSourceProvider + */ + private void vetoDeprecatedJtaDataSourceProvider(@Observes ProcessBeanAttributes event) { + if (!this.enabled) { + return; + } + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "vetoDeprecatedJtaDataSourceProvider", + "Vetoing BeanAttributes {0} representing " + + JtaDataSourceProvider.class + + " because it is deprecated and " + + JtaAdaptingDataSourceProvider.class + + " replaces it", event.getBeanAttributes()); + } + event.veto(); + } + + private void rewriteJpaAnnotations(@Observes + @WithAnnotations({PersistenceContext.class, PersistenceUnit.class}) + ProcessAnnotatedType event) { + if (!this.enabled) { + return; + } + AnnotatedTypeConfigurator atc = event.configureAnnotatedType(); + atc.filterFields(PersistenceExtension::isEligiblePersistenceContextAnnotated) + .forEach(this::rewritePersistenceContextFieldAnnotations); + atc.filterFields(PersistenceExtension::isEligiblePersistenceUnitAnnotated) + .forEach(this::rewritePersistenceUnitFieldAnnotations); + atc.filterMethods(PersistenceExtension::isEligiblePersistenceContextAnnotated) + .forEach(this::rewritePersistenceContextInitializerMethodAnnotations); + atc.filterMethods(PersistenceExtension::isEligiblePersistenceUnitAnnotated) + .forEach(this::rewritePersistenceUnitInitializerMethodAnnotations); + } + + /** + * 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; must not be {@code null} + * + * @param bm the {@link BeanManager} in effect; must not be {@code null} + * + * @exception NullPointerException if either {@code event} or {@code bm} is {@code null} + * + * @see PersistenceContext + * + * @see PersistenceProperty + * + * @see PersistenceUnitInfoBean + */ + private void gatherImplicitPersistenceUnits(@Observes + // yes, @PersistenceContext, not @PersistenceUnit + @WithAnnotations(PersistenceContext.class) + ProcessAnnotatedType event, + BeanManager bm) { + if (!this.enabled) { + return; + } + AnnotatedType at = event.getAnnotatedType(); + if (at.isAnnotationPresent(Vetoed.class)) { + return; + } + for (PersistenceContext pc : at.getAnnotations(PersistenceContext.class)) { + PersistenceProperty[] pps = pc.properties(); + if (pps.length > 0) { + String unitName = pc.unitName(); + if (unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + PersistenceUnitInfoBean pui = this.implicitPersistenceUnits.get(unitName); + if (pui == null) { + Properties properties = new Properties(); + for (PersistenceProperty pp : pps) { + String ppName = pp.name(); + if (!ppName.isBlank()) { + properties.setProperty(ppName, pp.value()); + } + } + pui = new PersistenceUnitInfoBean(unitName, + locationOf(at), + null, + () -> bm.createInstance().select(DataSourceProvider.class).get(), + properties); + this.implicitPersistenceUnits.put(unitName, pui); + } + } + } + } + + /** + * 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; must not be {@code null} + * + * @exception NullPointerException if {@code event} is {@code null} + * + * @see Converter + * + * @see Embeddable + * + * @see Entity + * + * @see MappedSuperclass + * + * @see PersistenceUnitInfo#excludeUnlistedClasses() + */ + private void discoverManagedClasses(@Observes + @WithAnnotations({ + Converter.class, + Embeddable.class, + Entity.class, + MappedSuperclass.class + }) + ProcessAnnotatedType event) { + if (!this.enabled) { + return; + } + AnnotatedType at = event.getAnnotatedType(); + if (!at.isAnnotationPresent(Vetoed.class)) { + this.assignManagedClassToPersistenceUnit(at.getAnnotations(PersistenceContext.class), + at.getAnnotations(PersistenceUnit.class), + at.getJavaClass()); + } + event.veto(); // managed classes can't be beans + } + + /** + * Stores {@link Set}s of qualifiers that annotate container-managed {@link EntityManagerFactory}-typed injection + * points. + * + *

{@link EntityManagerFactory}-typed beans will be added for each such valid {@link Set}.

+ * + * @param e a {@link ProcessInjectionPoint} container lifecycle event; must not be {@code null} + * + * @exception NullPointerException if {@code e} is {@code null} + */ + @SuppressWarnings("checkstyle:LineLength") + private void saveContainerManagedEntityManagerFactoryQualifiers(@Observes ProcessInjectionPoint e) { + if (!this.enabled) { + return; + } + boolean add = false; + Set qualifiers = e.getInjectionPoint().getQualifiers(); + for (Annotation qualifier : qualifiers) { + if (qualifier == ContainerManaged.Literal.INSTANCE) { + if (!add) { + add = true; + } + } else if (SyntheticJpaQualifiers.INSTANCE.contains(qualifier)) { + if (add) { + add = false; + } + e.addDefinitionError(new InjectionException("Invalid injection point; reserved qualifier used: " + qualifier)); + } + } + if (add) { + this.entityManagerFactoryQualifiers.add(qualifiers); + } + } + + /** + * Stores {@link Set}s of qualifiers that annotate {@link EntityManager}-typed injection points. + * + *

{@link EntityManager}-typed beans will be added for each such {@link Set}.

+ * + * @param e a {@link ProcessInjectionPoint} container lifecycle event; must not be {@code null} + * + * @exception NullPointerException if {@code e} is {@code null} + */ + private void saveContainerManagedEntityManagerQualifiers(@Observes ProcessInjectionPoint e) { + if (!this.enabled) { + return; + } + Set qualifiers = e.getInjectionPoint().getQualifiers(); + if (qualifiers.contains(ContainerManaged.Literal.INSTANCE)) { + if (qualifiers.contains(JtaTransactionScoped.Literal.INSTANCE) && qualifiers.contains(Extended.Literal.INSTANCE) + || qualifiers.contains(Synchronized.Literal.INSTANCE) && qualifiers.contains(Unsynchronized.Literal.INSTANCE)) { + e.addDefinitionError(new InjectionException("Invalid injection point; some qualifiers are mutually exclusive: " + + qualifiers)); + } else { + this.containerManagedEntityManagerQualifiers.add(qualifiers); + } + } + } + + /** + * Adds various beans that integrate container-mode JPA into CDI SE. + * + *

This method first 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.

+ * + *

Next, this method adds beans to produce {@link EntityManager}s and {@link EntityManagerFactory} instances in + * accordance with the JPA specification.

+ * + * @param event the {@link AfterBeanDiscovery} event describing the fact that bean discovery has been performed; + * must not be {@code null} + * + * @param bm the {@link BeanManager} currently in effect; must not be {@code null} + * + * @see PersistenceUnitInfo + * + * @see #addPersistenceProviderBeansIfAbsent(AfterBeanDiscoveryEvent, BeanManager, Set, Iterable) + * + * @see #processPersistenceXmls(AfterBeanDiscoveryEvent, BeanManager, ClassLoader, Enumeration, Iterable, boolean) + * + * @see #processImplicitPersistenceUnits(AfterBeanDiscoveryEvent, Iterable) + * + * @see #addContainerManagedJpaBeans(AfterBeanDiscovery) + */ + private void addSyntheticBeans(@Observes @Priority(LIBRARY_AFTER) AfterBeanDiscovery event, BeanManager bm) { + if (!this.enabled) { + return; + } + Iterable 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.) + Set> preexistingPuiBeans = bm.getBeans(PersistenceUnitInfo.class, Any.Literal.INSTANCE); + if (!preexistingPuiBeans.isEmpty()) { + processImplicits = false; + this.addPersistenceProviderBeansIfAbsent(event, bm, preexistingPuiBeans, 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). + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration urls; + try { + urls = classLoader.getResources("META-INF/persistence.xml"); + } catch (IOException e) { + event.addDefinitionError(e); + processImplicits = false; + urls = Collections.emptyEnumeration(); + } + if (urls.hasMoreElements()) { + processImplicits = false; + this.processPersistenceXmls(event, + bm, + classLoader, + urls, + providers, + !preexistingPuiBeans.isEmpty()); + } + + // 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); + } + + // Add beans to support JPA. In some cases, JTA must be present (see JPA section 7.5, for example: "A + // container-managed entity manager must be a JTA entity manager."). + this.addContainerManagedJpaBeans(event); + + // Clear out no-longer-needed-or-used collections to save memory. + this.containerManagedEntityManagerFactoryQualifiers.clear(); + this.containerManagedEntityManagerQualifiers.clear(); + this.implicitPersistenceUnits.clear(); + this.unlistedManagedClassesByUnitNames.clear(); + } + + + /* + * Other instance methods. + */ + + + /** + * Reconfigures annotations on an {@linkplain #isEligiblePersistenceContextAnnotated(Annotated) eligible + * PersistenceContext-annotated Annotated} such that the resulting {@link Annotated} is a + * true CDI injection point representing all the same information. + * + *

The original {@link PersistenceContext} annotation is removed.

+ * + * @param fc the {@link AnnotatedFieldConfigurator} that allows the field to be re-annotated; must not be {@code + * null} + * + * @exception NullPointerException if {@code fc} is {@code null} + */ + private void rewritePersistenceContextFieldAnnotations(AnnotatedFieldConfigurator fc) { + this.rewrite(fc, + PersistenceContext.class, + EntityManager.class, + PersistenceContext::unitName, + (f, pc) -> { + f.add(pc.type() == EXTENDED + ? Extended.Literal.INSTANCE + : JtaTransactionScoped.Literal.INSTANCE) + .add(pc.synchronization() == UNSYNCHRONIZED + ? Unsynchronized.Literal.INSTANCE + : Synchronized.Literal.INSTANCE); + for (PersistenceProperty pp : pc.properties()) { + String ppName = pp.name(); + if (!ppName.isBlank()) { + f.add(pp); + } + } + }); + } + + /** + * Reconfigures annotations on an {@linkplain #isEligiblePersistenceUnitAnnotated(Annotated) eligible + * PersistenceUnit-annotated Annotated} such that the resulting {@link Annotated} is a + * true CDI injection point representing all the same information. + * + *

The original {@link PersistenceUnit} annotation is removed.

+ * + * @param fc the {@link AnnotatedFieldConfigurator} that allows the field to be re-annotated; must not be {@code + * null} + * + * @exception NullPointerException if {@code fc} is {@code null} + */ + private void rewritePersistenceUnitFieldAnnotations(AnnotatedFieldConfigurator fc) { + this.rewrite(fc, PersistenceUnit.class, EntityManagerFactory.class, PersistenceUnit::unitName); + } + + private void rewritePersistenceContextInitializerMethodAnnotations(AnnotatedMethodConfigurator mc) { + this.rewrite(mc, + PersistenceContext.class, + EntityManager.class, + PersistenceContext::unitName, + (p, pc) -> { + p.add(pc.type() == EXTENDED + ? Extended.Literal.INSTANCE + : JtaTransactionScoped.Literal.INSTANCE) + .add(pc.synchronization() == UNSYNCHRONIZED + ? Unsynchronized.Literal.INSTANCE + : Synchronized.Literal.INSTANCE); + for (PersistenceProperty pp : pc.properties()) { + p.add(pp); + } + }); + } + + private void rewritePersistenceUnitInitializerMethodAnnotations(AnnotatedMethodConfigurator mc) { + this.rewrite(mc, PersistenceUnit.class, EntityManagerFactory.class, PersistenceUnit::unitName); + } + + private void rewrite(AnnotatedFieldConfigurator fc, + Class ac, + Class c, + Function unitNameFunction) { + this.rewrite(fc, ac, c, unitNameFunction, PersistenceExtension::sink); + } + + private void rewrite(AnnotatedFieldConfigurator fc, + Class ac, + Class c, + Function unitNameFunction, + BiConsumer, ? super A> adder) { + Annotated f = fc.getAnnotated(); + if (!f.isAnnotationPresent(Inject.class) && f.getBaseType() instanceof Class c2 && c.isAssignableFrom(c2)) { + A a = fc.getAnnotated().getAnnotation(ac); + if (a != null) { + // Rewrite: + // + // @PersistenceContext(properties = { @PersistenceProperty(name = "a", value = "b"), + // @PersistenceProperty(name = "c", value = "d") }, + // synchronization = SynchronizationType.SYNCHRONIZED, + // type = PersistenceContextType.TRANSACTION, + // unitName = "xyz") + // private EntityManager em; + // + // @PersistenceUnit(unitName = "xyz") + // private EntityManagerFactory emf; + // + // ...to: + // + // @Inject + // @ContainerManaged + // @JtaTransactionScoped + // @PersistenceProperty(name = "a", value = "b") + // @PersistenceProperty(name = "c", value = "d") + // @Named("xyz") + // @Synchronized + // private EntityManager em; + // + // @Inject + // @ContainerManaged + // @Named("xyz") + // private EntityManagerFactory emf; + // + fc.add(InjectLiteral.INSTANCE); + fc.add(ContainerManaged.Literal.INSTANCE); + String unitName = unitNameFunction.apply(a); + if (unitName == null || unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + fc.add(NamedLiteral.of(unitName)); + adder.accept(fc, a); + fc.remove(fa -> fa == a); + } + } + } + + private void rewrite(AnnotatedMethodConfigurator mc, + Class ac, + Class c, + Function unitNameFunction) { + this.rewrite(mc, ac, c, unitNameFunction, PersistenceExtension::sink); + } + + private void rewrite(AnnotatedMethodConfigurator mc, + Class ac, + Class c, + Function unitNameFunction, + BiConsumer, ? super A> adder) { + Annotated m = mc.getAnnotated(); + if (!m.isAnnotationPresent(Inject.class)) { + A a = m.getAnnotation(ac); + if (a != null) { + boolean observerMethod = false; + for (AnnotatedParameterConfigurator apc : mc.params()) { + Annotated p = apc.getAnnotated(); + if (p.isAnnotationPresent(Observes.class)) { + if (!observerMethod) { + observerMethod = true; + } + } else if (p.getBaseType() instanceof Class pc && c.isAssignableFrom(pc)) { + // Rewrite: + // + // @PersistenceContext(properties = { @PersistenceProperty(name = "a", value = "b"), + // @PersistenceProperty(name = "c", value = "d") }, + // synchronization = SynchronizationType.SYNCHRONIZED, + // type = PersistenceContextType.TRANSACTION, + // unitName = "xyz") + // private void frob(EntityManager em) {} + // + // @PersistenceUnit(unitName = "xyz") + // private void frob(EntityManagerFactory emf) {} + // + // ...to: + // + // @Inject + // private void frob(@ContainerManaged + // @JtaTransactionScoped + // @Named("xyz") + // @PersistenceProperty(name = "a", value = "b") + // @PersistenceProperty(name = "c", value = "d") + // @Synchronized + // EntityManager em) {} + // + // @Inject + // private void frob(@ContainerManaged + // @Named("xyz") + // EntityManagerFactory emf) {} + // + apc.add(ContainerManaged.Literal.INSTANCE); + String unitName = unitNameFunction.apply(a); + if (unitName == null || unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + apc.add(NamedLiteral.of(unitName)); + adder.accept(apc, a); + } + } + mc.remove(ma -> ma == a); + if (!observerMethod) { + mc.add(InjectLiteral.INSTANCE); + } + } + } + } + + /** + * 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 pcs 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 pus 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 + * + * @exception NullPointerException if either {@code pcs}, {@code pus} or {@code c} is {@code null} + * + * @see PersistenceContext + * + * @see PersistenceUnit + */ + private void assignManagedClassToPersistenceUnit(Set pcs, + Set pus, + Class c) { + boolean processed = false; + for (PersistenceContext pc : pcs) { + if (!processed) { + processed = true; + } + String unitName = pc.unitName(); + if (unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + addUnlistedManagedClass(unitName, c); + } + for (PersistenceUnit pu : pus) { + if (!processed) { + processed = true; + } + String unitName = pu.unitName(); + if (unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + addUnlistedManagedClass(unitName, c); + } + if (!processed) { + addUnlistedManagedClass(DEFAULT_PERSISTENCE_UNIT_NAME, c); + } + } + + /** + * 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 managed classes. + * + * @param unitName the name of the persistence unit in question; may be {@code null} + * + * @param mc the {@link Class} to associate; may be {@code null} in which case no action will be taken + * + * @exception NullPointerException if either {@code unitName} or {@code mc} is {@code null} + * + * @see PersistenceUnitInfo#getManagedClassNames() + */ + private void addUnlistedManagedClass(String unitName, Class mc) { + this.unlistedManagedClassesByUnitNames.computeIfAbsent(unitName.isBlank() ? DEFAULT_PERSISTENCE_UNIT_NAME : unitName, + k -> new HashSet<>()) + .add(mc); + } + + private static Iterable addPersistenceProviderBeans(AfterBeanDiscovery e) { + PersistenceProviderResolver resolver = PersistenceProviderResolverHolder.getPersistenceProviderResolver(); + + // Provide support for, e.g.: + // @Inject + // private PersistenceProviderResolver ppr; + e.addBean() + .addTransitiveTypeClosure(PersistenceProviderResolver.class) + .scope(Singleton.class) + .createWith(cc -> resolver); + Collection providers = resolver.getPersistenceProviders(); + for (PersistenceProvider provider : providers) { + // Provide support for, e.g.: + // @Inject + // private MyPersistenceProviderSubclassMaybeFromPersistenceXml ppr; + e.addBean() + .addTransitiveTypeClosure(provider.getClass()) + .scope(Singleton.class) + .createWith(cc -> provider); + } + return providers; + } + + private void addPersistenceProviderBeansIfAbsent(AfterBeanDiscovery event, + BeanManager bm, + Set> preexistingPuiBeans, + Iterable providers) { + for (Object bean : preexistingPuiBeans) { + @SuppressWarnings("unchecked") + Bean preexistingPuiBean = (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. + CreationalContext cc = bm.createCreationalContext(null); + try { + PersistenceUnitInfo pui = preexistingPuiBean.create(cc); + try { + this.addPersistenceProviderBeanIfAbsent(event, pui, providers); + } finally { + preexistingPuiBean.destroy(pui, cc); + } + } finally { + cc.release(); + } + } + } + + /** + * 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 the supplied {@link Collection} of {@link PersistenceProvider}s does not + * contain an instance of it. + * + * @param event the {@link AfterBeanDiscovery} event that will do the actual bean addition; must not be {@code null} + * + * @param pui the {@link PersistenceUnitInfo} whose {@linkplain + * PersistenceUnitInfo#getPersistenceProviderClassName() associated persistence provider} will be beanified; must + * not be {@code null} + * + * @param providers an {@link Iterable} 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 pui} is {@code null} + * + * @exception ReflectiveOperationException if an error occurs during reflection + */ + private void addPersistenceProviderBeanIfAbsent(AfterBeanDiscovery event, + PersistenceUnitInfo pui, + Iterable providers) { + String providerClassName = pui.getPersistenceProviderClassName(); + if (providerClassName == null) { + return; + } + + for (final PersistenceProvider provider : providers) { + if (provider.getClass().getName().equals(providerClassName)) { + return; + } + } + + // The PersistenceProvider class in question is not one we already loaded. Add a bean for it too. + String unitName = pui.getPersistenceUnitName(); + if (unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + + // Provide support for, e.g.: + // @Inject + // @Named("test") + // private PersistenceProvider providerProbablyReferencedFromAPersistenceXml; + event.addBean() + .addTransitiveTypeClosure(PersistenceProvider.class) + .scope(Singleton.class) + .qualifiers(NamedLiteral.of(unitName)) + .createWith(cc -> { + try { + ClassLoader classLoader = pui.getClassLoader(); + if (classLoader == null) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + return Class.forName(providerClassName, true, classLoader).getDeclaredConstructor().newInstance(); + } catch (final ReflectiveOperationException e) { + throw new CreationException(e.getMessage(), e); + } + }); + } + + private void processPersistenceXmls(AfterBeanDiscovery event, + BeanManager bm, + ClassLoader classLoader, + Enumeration persistenceXmlUrls, + Iterable providers, + boolean userSuppliedPuiBeans) { + if (!persistenceXmlUrls.hasMoreElements()) { + return; + } + // We use StAX for XML loading because it is the same XML parsing strategy used by all known CDI + // implementations. If the end user wants to customize the StAX implementation then we want that customization + // to apply here as well. + XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); + // See + // https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.md#xmlinputfactory-a-stax-parser + xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + xmlInputFactory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); + Unmarshaller unmarshaller; + try { + unmarshaller = JAXBContext.newInstance(Persistence.class.getPackage().getName()).createUnmarshaller(); + } catch (JAXBException e) { + event.addDefinitionError(e); + return; + } + Supplier dataSourceProviderSupplier = + () -> bm.createInstance().select(DataSourceProvider.class).get(); + PersistenceUnitInfo solePui = null; + Supplier tempClassLoaderSupplier = + classLoader instanceof URLClassLoader ucl ? () -> new URLClassLoader(ucl.getURLs()) : () -> classLoader; + for (int puCount = 0; persistenceXmlUrls.hasMoreElements();) { + URL persistenceXmlUrl = persistenceXmlUrls.nextElement(); + Persistence persistence = null; + try (InputStream inputStream = new BufferedInputStream(persistenceXmlUrl.openStream())) { + XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(inputStream); + try { + persistence = (Persistence) unmarshaller.unmarshal(reader); + } finally { + reader.close(); + } + } catch (IOException | JAXBException | XMLStreamException e) { + event.addDefinitionError(e); + continue; + } + for (Persistence.PersistenceUnit pu : persistence.getPersistenceUnit()) { + PersistenceUnitInfoBean pui; + try { + pui = + PersistenceUnitInfoBean.fromPersistenceUnit(pu, + classLoader, + tempClassLoaderSupplier, + new URL(persistenceXmlUrl, ".."), // i.e. META-INF/.. + unlistedManagedClassesByUnitNames, + dataSourceProviderSupplier); + } catch (MalformedURLException e) { + event.addDefinitionError(e); + continue; + } + String unitName = pui.getPersistenceUnitName(); + if (unitName == null || unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + // Provide support for, e.g.: + // @Inject + // @Named("test") + // private PersistenceUnitInfo persistenceUnitInfo; + event.addBean() + .beanClass(PersistenceUnitInfoBean.class) + .addTransitiveTypeClosure(PersistenceUnitInfoBean.class) + .scope(Singleton.class) + .qualifiers(NamedLiteral.of(unitName)) + .createWith(cc -> pui); + addPersistenceProviderBeanIfAbsent(event, pui, providers); + if (puCount == 0) { + solePui = pui; + } else if (solePui != null) { + solePui = null; + } + puCount++; + } + } + if (!userSuppliedPuiBeans && solePui != null) { + String soleUnitName = solePui.getPersistenceUnitName(); + assert soleUnitName != null; + assert !soleUnitName.isBlank(); + if (!soleUnitName.equals(DEFAULT_PERSISTENCE_UNIT_NAME)) { + PersistenceUnitInfo pui = solePui; + // Provide support for, e.g.: + // @Inject + // @Named("__DEFAULT__")) + // private PersistenceUnitInfo persistenceUnitInfo; + event.addBean() + .beanClass(PersistenceUnitInfoBean.class) + .addTransitiveTypeClosure(PersistenceUnitInfoBean.class) + .scope(Singleton.class) + .qualifiers(NamedLiteral.of(DEFAULT_PERSISTENCE_UNIT_NAME)) + .createWith(cc -> pui); + } + } + } + + private void processImplicitPersistenceUnits(AfterBeanDiscovery event, Iterable providers) { + PersistenceUnitInfoBean solePui = null; + int puCount = 0; + for (PersistenceUnitInfoBean pui : this.implicitPersistenceUnits.values()) { + String unitName = pui.getPersistenceUnitName(); + if (unitName == null || unitName.isBlank()) { + unitName = DEFAULT_PERSISTENCE_UNIT_NAME; + } + if (!pui.excludeUnlistedClasses()) { + Collection> unlistedManagedClasses = this.unlistedManagedClassesByUnitNames.get(unitName); + if (unlistedManagedClasses != null) { + for (Class unlistedManagedClass : unlistedManagedClasses) { + pui.addManagedClassName(unlistedManagedClass.getName()); + } + } + } + // Provide support for, e.g.: + // @Inject + // @Named("test") + // private PersistenceUnitInfo persistenceUnitInfo; + event.addBean() + .beanClass(PersistenceUnitInfoBean.class) + .addTransitiveTypeClosure(PersistenceUnitInfoBean.class) + .scope(Singleton.class) + .qualifiers(NamedLiteral.of(unitName)) + .createWith(cc -> pui); + addPersistenceProviderBeanIfAbsent(event, pui, providers); + if (puCount == 0) { + solePui = pui; + } else if (solePui != null) { + solePui = null; + } + puCount++; + } + if (solePui != null) { + // Add a bean for the DEFAULT_PERSISTENCE_UNIT_NAME qualifier too. + String soleUnitName = solePui.getPersistenceUnitName(); + assert soleUnitName != null; + assert !soleUnitName.isBlank(); + if (!soleUnitName.equals(DEFAULT_PERSISTENCE_UNIT_NAME)) { + PersistenceUnitInfoBean pui = solePui; + // Provide support for, e.g.: + // @Inject + // @Named("__DEFAULT__") + // private PersistenceUnitInfo persistenceUnitInfo; + event.addBean() + .beanClass(PersistenceUnitInfoBean.class) + .addTransitiveTypeClosure(PersistenceUnitInfoBean.class) + .scope(Singleton.class) + .qualifiers(NamedLiteral.of(DEFAULT_PERSISTENCE_UNIT_NAME)) + .createWith(cc -> pui); + } + } + } + + /** + * Adds certain beans to support injection of {@link EntityManagerFactory} and {@link EntityManager} instances + * according to the JPA specification. + * + * @param event an {@link AfterBeanDiscovery} container lifecycle event; must not be {@code null} + * + * @exception NullPointerException if {@code event} is {@code null} + * + * @see #addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery, Set) + * + * @see #addExtendedEntityManagerBeans(AfterBeanDiscovery, Set) + * + * @see #addJtaTransactionScopedEntityManagerBeans(AfterBeanDiscovery, Set) + */ + private void addContainerManagedJpaBeans(AfterBeanDiscovery event) { + for (Iterator> i = this.entityManagerFactoryQualifiers.iterator(); i.hasNext();) { + Set qualifiers = i.next(); + i.remove(); + if (qualifiers.contains(ContainerManaged.Literal.INSTANCE)) { + addContainerManagedEntityManagerFactoryBeans(event, qualifiers); + } + } + assert this.entityManagerFactoryQualifiers.isEmpty(); + if (this.transactionsSupported) { + for (Set qualifiers : this.containerManagedEntityManagerQualifiers) { + if (qualifiers.contains(ContainerManaged.Literal.INSTANCE)) { + // Note that each add* method invoked below is responsible for ensuring that it adds beans only once + // if at all, i.e. for validating and de-duplicating the qualifiers that it is supplied with if + // necessary. + addContainerManagedEntityManagerFactoryBeans(event, qualifiers); + if (qualifiers.contains(Extended.Literal.INSTANCE)) { + assert !qualifiers.contains(JtaTransactionScoped.Literal.INSTANCE); + addExtendedEntityManagerBeans(event, qualifiers); + } else { + assert qualifiers.contains(JtaTransactionScoped.Literal.INSTANCE); + addJtaTransactionScopedEntityManagerBeans(event, qualifiers); + } + } + } + } else { + for (Set qualifiers : this.containerManagedEntityManagerQualifiers) { + if (qualifiers.contains(ContainerManaged.Literal.INSTANCE)) { + // Note that each add* method invoked below is responsible for ensuring that it adds beans only once + // if at all, i.e. for validating the qualifiers that it is supplied with. + addContainerManagedEntityManagerFactoryBeans(event, qualifiers); + } + } + } + } + + private void addContainerManagedEntityManagerFactoryBeans(AfterBeanDiscovery event, + Set suppliedQualifiers) { + // Provide support for, e.g.: + // @Inject + // @ContainerManaged + // @Named("test") + // private final EntityManagerFactory emf; + Set qualifiers = new HashSet<>(suppliedQualifiers); + qualifiers.removeIf(e -> e != ContainerManaged.Literal.INSTANCE && SyntheticJpaQualifiers.INSTANCE.contains(e)); + if (this.containerManagedEntityManagerFactoryQualifiers.add(qualifiers)) { + event.addBean() + .addTransitiveTypeClosure(EntityManagerFactory.class) + .scope(Singleton.class) + .qualifiers(qualifiers) + .produceWith(PersistenceExtension::produceEntityManagerFactory) + .disposeWith(PersistenceExtension::disposeEntityManagerFactory); + } + } + + private void addExtendedEntityManagerBeans(AfterBeanDiscovery event, Set suppliedQualifiers) { + if (!this.transactionsSupported) { + event.addDefinitionError(new IllegalStateException("Transactions are not supported")); + return; + } + // Provide support for, e.g.: + // @Inject + // @ContainerManaged + // @Extended + // @PersistenceProperty(name = "a", value = "b") + // @PersistenceProperty(name = "c", value = "d") + // @Synchronized // or @Unsynchronized, or none + // @Named("test") + // private final EntityManager extendedEm; + Set qualifiers = new HashSet<>(suppliedQualifiers); + qualifiers.removeIf(e -> e == JtaTransactionScoped.Literal.INSTANCE); + qualifiers.add(ContainerManaged.Literal.INSTANCE); + qualifiers.add(Extended.Literal.INSTANCE); + event.addBean() + .addTransitiveTypeClosure(JtaExtendedEntityManager.class) + .scope(Dependent.class) // critical: must be Dependent scope + .qualifiers(qualifiers) + .produceWith(PersistenceExtension::produceJtaExtendedEntityManager) + .disposeWith(PersistenceExtension::disposeJtaExtendedEntityManager); + } + + private void addJtaTransactionScopedEntityManagerBeans(AfterBeanDiscovery event, Set suppliedQualifiers) { + if (!this.transactionsSupported) { + event.addDefinitionError(new IllegalStateException("Transactions are not supported")); + return; + } + // Provide support for, e.g.: + // @Inject + // @ContainerManaged + // @JtaTransactionScoped + // @PersistenceProperty(name = "a", value = "b") + // @PersistenceProperty(name = "c", value = "d") + // @Synchronized // or @Unsynchronized, or none + // @Named("test") + // private final EntityManager jtaTransactionScopedEm; + Set qualifiers = new HashSet<>(suppliedQualifiers); + qualifiers.removeIf(e -> e == Extended.Literal.INSTANCE); + qualifiers.add(ContainerManaged.Literal.INSTANCE); + qualifiers.add(JtaTransactionScoped.Literal.INSTANCE); + event.addBean() + .addTransitiveTypeClosure(JtaEntityManager.class) + .scope(Dependent.class) // critical: must be Dependent scope + .qualifiers(qualifiers) + .produceWith(PersistenceExtension::produceJtaEntityManager) + .disposeWith(PersistenceExtension::disposeJtaEntityManager); + } + + + /* + * Static methods. + */ + + + private static JtaExtendedEntityManager produceJtaExtendedEntityManager(Instance instance) { + BeanAttributes ba = instance.select(BEAN_JTAEXTENDEDENTITYMANAGER_TYPELITERAL).get(); + Set containerManagedSelectionQualifiers = new HashSet<>(); + containerManagedSelectionQualifiers.add(ContainerManaged.Literal.INSTANCE); + Set selectionQualifiers = new HashSet<>(); + SynchronizationType syncType = null; + Map properties = new HashMap<>(); + for (Annotation beanQualifier : ba.getQualifiers()) { + if (beanQualifier == Any.Literal.INSTANCE) { + continue; + } else if (beanQualifier == Unsynchronized.Literal.INSTANCE) { + if (syncType == null) { + syncType = UNSYNCHRONIZED; + } + } else if (beanQualifier instanceof PersistenceProperty pp) { + containerManagedSelectionQualifiers.add(pp); + String ppName = pp.name(); + if (!ppName.isBlank()) { + properties.put(ppName, pp.value()); + } + } else if (beanQualifier != Any.Literal.INSTANCE && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + containerManagedSelectionQualifiers.add(beanQualifier); + selectionQualifiers.add(beanQualifier); + } + } + SynchronizationType finalSyncType = syncType == null ? SYNCHRONIZED : syncType; + return instance.select(ReferenceCountingProducer.class) + .get() + .produce(() -> { + TransactionRegistry tr = + getOrDefault(instance.select(TransactionRegistry.class), + selectionQualifiers.toArray(EMPTY_ANNOTATION_ARRAY)); + return + new JtaExtendedEntityManager(tr::active, + tr::get, + tr::put, + instance.select(EntityManagerFactory.class, + containerManagedSelectionQualifiers + .toArray(EMPTY_ANNOTATION_ARRAY)) + .get() + .createEntityManager(finalSyncType, properties), + finalSyncType == SYNCHRONIZED); + }, + JtaExtendedEntityManager::dispose, + JtaExtendedEntityManager.class, + containerManagedSelectionQualifiers); + } + + private static void disposeJtaExtendedEntityManager(JtaExtendedEntityManager em, Instance instance) { + Set containerManagedSelectionQualifiers = new HashSet<>(); + for (Annotation beanQualifier : instance.select(BEAN_JTAEXTENDEDENTITYMANAGER_TYPELITERAL).get().getQualifiers()) { + if (beanQualifier == ContainerManaged.Literal.INSTANCE + || beanQualifier != Any.Literal.INSTANCE + && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + containerManagedSelectionQualifiers.add(beanQualifier); + } + } + instance.select(ReferenceCountingProducer.class) + .get() + .dispose(JtaExtendedEntityManager.class, + containerManagedSelectionQualifiers); + } + + private static EntityManagerFactory produceEntityManagerFactory(Instance instance) { + BeanAttributes ba = instance.select(BEAN_ENTITYMANAGERFACTORY_TYPELITERAL).get(); + Set selectionQualifiers = new HashSet<>(); + Set namedSelectionQualifiers = new HashSet<>(); + for (Annotation beanQualifier: ba.getQualifiers()) { + if (beanQualifier == Any.Literal.INSTANCE) { + continue; + } else if (beanQualifier instanceof Named) { + namedSelectionQualifiers.add(beanQualifier); + } else if (!SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + namedSelectionQualifiers.add(beanQualifier); + selectionQualifiers.add(beanQualifier); + } + } + PersistenceUnitInfo pui = + getOrDefault(instance.select(PersistenceUnitInfo.class), namedSelectionQualifiers); + PersistenceProvider pp = + getOrDefault(instance.select(PersistenceProvider.class), selectionQualifiers); + Map properties = new HashMap<>(5); + properties.put("jakarta.persistence.bean.manager", instance.select(BeanManager.class).get()); + try { + Instance vf = + instance.select(Class.forName("jakarta.validation.ValidatorFactory", + false, + Thread.currentThread().getContextClassLoader())); + if (!vf.isUnsatisfied()) { + properties.put("jakarta.persistence.validation.factory", vf.get()); + } + } catch (final ClassNotFoundException classNotFoundException) { + + } + return pp.createContainerEntityManagerFactory(pui, properties); + } + + private static void disposeEntityManagerFactory(EntityManagerFactory emf, Instance instance) { + emf.close(); + } + + private static JtaEntityManager produceJtaEntityManager(Instance instance) { + Set containerManagedSelectionQualifiers = new HashSet<>(); + containerManagedSelectionQualifiers.add(ContainerManaged.Literal.INSTANCE); + Set selectionQualifiers = new HashSet<>(); + SynchronizationType syncType = null; + Map properties = new HashMap<>(); + for (Annotation beanQualifier : instance.select(BEAN_JTAENTITYMANAGER_TYPELITERAL).get().getQualifiers()) { + if (beanQualifier == Any.Literal.INSTANCE) { + continue; + } else if (beanQualifier == Unsynchronized.Literal.INSTANCE) { + if (syncType == null) { + syncType = UNSYNCHRONIZED; + } + } else if (beanQualifier instanceof PersistenceProperty pp) { + String ppName = pp.name(); + if (!ppName.isBlank()) { + containerManagedSelectionQualifiers.add(pp); + properties.put(ppName, pp.value()); + } + } else if (!SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + containerManagedSelectionQualifiers.add(beanQualifier); + selectionQualifiers.add(beanQualifier); + } + } + SynchronizationType finalSyncType = syncType == null ? SYNCHRONIZED : syncType; + return instance.select(ReferenceCountingProducer.class) + .get() + .produce(() -> { + TransactionRegistry tr = getOrDefault(instance.select(TransactionRegistry.class), selectionQualifiers); + return + new JtaEntityManager(tr::active, + tr::addCompletionListener, + tr::get, + tr::put, + instance.select(EntityManagerFactory.class, + containerManagedSelectionQualifiers.toArray(EMPTY_ANNOTATION_ARRAY)) + .get()::createEntityManager, + finalSyncType, + properties); + }, + JtaEntityManager::dispose, + JtaEntityManager.class, + containerManagedSelectionQualifiers); + } + + private static void disposeJtaEntityManager(JtaEntityManager em, Instance instance) { + Set containerManagedSelectionQualifiers = new HashSet<>(); + for (Annotation beanQualifier : instance.select(BEAN_JTAENTITYMANAGER_TYPELITERAL).get().getQualifiers()) { + if (beanQualifier == ContainerManaged.Literal.INSTANCE + || beanQualifier != Any.Literal.INSTANCE + && !SyntheticJpaQualifiers.INSTANCE.contains(beanQualifier)) { + containerManagedSelectionQualifiers.add(beanQualifier); + } + } + instance.select(ReferenceCountingProducer.class) + .get() + .dispose(JtaEntityManager.class, + containerManagedSelectionQualifiers); + } + + /** + * Returns {@code true} if the supplied {@link Annotated} is annotated with {@link PersistenceContext}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManager}. + * + * @param a the {@link Annotated} in question; must not be {@code null} + * + * @return {@code true} if the supplied {@link Annotated} is annotated with {@link PersistenceContext}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManager}; {@code false} in all other + * cases + * + * @exception NullPointerException if {@code a} is {@code null} + */ + private static boolean isEligiblePersistenceContextAnnotated(Annotated a) { + return isRewriteEligible(a, PersistenceContext.class, EntityManager.class); + } + + /** + * Returns {@code true} if the supplied {@link Annotated} is annotated with {@link PersistenceUnit}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManagerFactory}. + * + * @param a the {@link Annotated} in question; must not be {@code null} + * + * @return {@code true} if the supplied {@link Annotated} is annotated with {@link PersistenceUnit}, is not + * annotated with {@link Inject} and has a type assignable to {@link EntityManagerFactory}; {@code false} in all + * other cases + * + * @exception NullPointerException if {@code a} is {@code null} + */ + private static boolean isEligiblePersistenceUnitAnnotated(Annotated a) { + return isRewriteEligible(a, PersistenceUnit.class, EntityManagerFactory.class); + } + + private static boolean isRewriteEligible(Annotated a, Class ac, Class c) { + if (a instanceof AnnotatedCallable am) { + return isRewriteEligible(am, ac, c); + } + return + a.isAnnotationPresent(ac) && !isInjectionPoint(a) && a.getBaseType() instanceof Class bt && c.isAssignableFrom(bt); + } + + private static boolean isRewriteEligible(AnnotatedCallable a, Class ac, Class c) { + if (a.isAnnotationPresent(ac) && !isInjectionPoint(a)) { + for (Annotated ap : a.getParameters()) { + if (ap.getBaseType() instanceof Class bt && c.isAssignableFrom(bt)) { + return true; + } + } + } + return false; + } + + private static boolean isInjectionPoint(Annotated a) { + return a.isAnnotationPresent(Inject.class); + } + + private static URL locationOf(AnnotatedType a) { + return locationOf(a.getJavaClass()); + } + + private static URL locationOf(Class c) { + return locationOf(c.getProtectionDomain()); + } + + private static URL locationOf(ProtectionDomain pd) { + return pd == null ? null : locationOf(pd.getCodeSource()); + } + + private static URL locationOf(CodeSource cs) { + return cs == null ? null : cs.getLocation(); + } + + private static T getOrDefault(Instance i, Set q) { + return getOrDefault(i, q.toArray(EMPTY_ANNOTATION_ARRAY)); + } + + private static T getOrDefault(Instance i, Annotation... q) { + if (q != null && q.length > 0) { + Instance i2 = i.select(q); + if (!i2.isUnsatisfied()) { + return i2.get(); + } + } + return i.get(); + } + + private static void sink(Object o1, Object o2) { + + } + + private static final class SyntheticJpaQualifiers { + + private static final Set INSTANCE; + + static { + Set q = new HashSet<>(); + q.add(ContainerManaged.Literal.INSTANCE); + q.add(Extended.Literal.INSTANCE); + q.add(JtaTransactionScoped.Literal.INSTANCE); + q.add(Synchronized.Literal.INSTANCE); + q.add(Unsynchronized.Literal.INSTANCE); + INSTANCE = Set.copyOf(q); + } + + } + + @Singleton + private static final class ReferenceCountingProducer { + + private static final Logger LOGGER = Logger.getLogger(ReferenceCountingProducer.class.getName()); + + private static final ThreadLocal>>> TL = + ThreadLocal.withInitial(HashMap::new); + + @Inject + private ReferenceCountingProducer() { + super(); + } + + private T produce(Supplier supplier, + Consumer disposer, + Class type, + Set qualifiers) { + return this.produce(supplier, disposer, Set.of(type), qualifiers); + } + + private T produce(Supplier supplier, + Consumer disposer, + Class type, + Annotation... qualifiers) { + return this.produce(supplier, disposer, Set.of(type), Set.copyOf(Arrays.asList(qualifiers))); + } + + private T produce(Supplier supplier, + Consumer disposer, + TypeLiteral type, + Set qualifiers) { + return this.produce(supplier, disposer, Set.of(type.getType()), qualifiers); + } + + private T produce(Supplier supplier, + Consumer disposer, + TypeLiteral type, + Annotation... qualifiers) { + return this.produce(supplier, disposer, Set.of(type.getType()), Set.copyOf(Arrays.asList(qualifiers))); + } + + private T produce(Supplier supplier, + Consumer disposer, + Set types, + Annotation... qualifiers) { + return this.produce(supplier, disposer, types, Set.copyOf(Arrays.asList(qualifiers))); + } + + private T produce(Supplier supplier, + Consumer disposer, + Set types, + Set qualifiers) { + Objects.requireNonNull(supplier, "supplier"); + Objects.requireNonNull(disposer, "disposer"); + @SuppressWarnings("unchecked") + Production production = (Production) TL.get() + .computeIfAbsent(this, k -> new HashMap<>()) + .computeIfAbsent(new ProductionId(Set.copyOf(types), Set.copyOf(qualifiers)), + k -> new Production<>(supplier.get(), disposer)); + int rc = production.reference(); + assert rc > 0; + return production.object; + } + + private void dispose(Class type, Set qualifiers) { + this.dispose(Set.of(type), qualifiers); + } + + private void dispose(Class type, Annotation... qualifiers) { + this.dispose(Set.of(type), Set.copyOf(Arrays.asList(qualifiers))); + } + + private void dispose(TypeLiteral type, Set qualifiers) { + this.dispose(Set.of(type.getType()), qualifiers); + } + + private void dispose(TypeLiteral type, Annotation... qualifiers) { + this.dispose(Set.of(type.getType()), Set.copyOf(Arrays.asList(qualifiers))); + } + + private void dispose(Set types, Annotation... qualifiers) { + this.dispose(types, Set.copyOf(Arrays.asList(qualifiers))); + } + + private void dispose(Set types, Set qualifiers) { + Map>> tlMap = TL.get(); + Map> map = tlMap.get(this); + if (map != null) { + ProductionId id = new ProductionId(Set.copyOf(types), Set.copyOf(qualifiers)); + @SuppressWarnings("unchecked") + Production production = (Production) map.get(id); + if (production != null) { + int rc = production.unreference(); + if (rc == 0) { + map.remove(id); + if (map.isEmpty()) { + tlMap.remove(this); + } + } else { + assert rc > 0; + } + } + } + } + + @PreDestroy + private void preDestroy() { + Map> map = TL.get().remove(this); + if (map != null) { + // This map shouldn't exist. If it does exist, it should at least be empty. We shouldn't be here. Ensure + // that everything is disposed properly nonetheless and log the problem. + RuntimeException re = null; + for (Entry> entry : map.entrySet()) { + if (LOGGER.isLoggable(Level.WARNING)) { + LOGGER.logp(Level.WARNING, this.getClass().getName(), "preDestroy", + "Disposing {0} but it should have already been disposed!", entry); + } + try { + entry.getValue().dispose(); + } catch (RuntimeException e) { + if (re == null) { + re = e; + } else { + re.addSuppressed(e); + } + } + } + if (re != null) { + throw re; + } + } + } + + private static final class Production { + + private T object; + + private final Consumer disposer; + + private int rc; + + private Production(T object, Consumer disposer) { + super(); + this.object = object; + this.disposer = Objects.requireNonNull(disposer, "disposer"); + } + + private int reference() { + return ++this.rc; + } + + private int unreference() { + int rc = --this.rc; + if (rc == 0) { + this.disposeIfUnreferenced(); + } else if (rc < 0) { + throw new AssertionError("Unexpected negative reference count: " + rc); + } + return rc; + } + + private boolean disposeIfUnreferenced() { + if (this.rc == 0) { + this.dispose(); + return true; + } else { + assert this.rc > 0 : "Unexpected negative reference count: " + this.rc; + } + return false; + } + + void dispose() { + T t = this.object; + this.object = null; + this.disposer.accept(t); + } + + } + + private static final record ProductionId(Set types, Set qualifiers) {} + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/TransactionRegistry.java b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/TransactionRegistry.java new file mode 100644 index 00000000000..7c063aabd15 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/main/java/io/helidon/integrations/cdi/jpa/TransactionRegistry.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.util.function.Consumer; + +interface TransactionRegistry { + + boolean active(); + + void addCompletionListener(Consumer cl); + + Object get(Object k); + + void put(Object k, Object v); + + enum CompletionStatus { + + COMMITTED, + ROLLED_BACK; + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/main/java/module-info.java b/integrations/cdi/jpa-cdi/src/main/java/module-info.java index ea11b63a913..1ffb60ce51f 100644 --- a/integrations/cdi/jpa-cdi/src/main/java/module-info.java +++ b/integrations/cdi/jpa-cdi/src/main/java/module-info.java @@ -60,5 +60,6 @@ exports io.helidon.integrations.cdi.jpa; exports io.helidon.integrations.cdi.jpa.jaxb; - provides jakarta.enterprise.inject.spi.Extension with io.helidon.integrations.cdi.jpa.JpaExtension; + provides jakarta.enterprise.inject.spi.Extension with + io.helidon.integrations.cdi.jpa.JpaExtension, io.helidon.integrations.cdi.jpa.PersistenceExtension; } diff --git a/integrations/cdi/jpa-cdi/src/main/resources/io/helidon/integrations/cdi/jpa/Messages.properties b/integrations/cdi/jpa-cdi/src/main/resources/io/helidon/integrations/cdi/jpa/Messages.properties index 36a68d28d05..fb564770309 100644 --- a/integrations/cdi/jpa-cdi/src/main/resources/io/helidon/integrations/cdi/jpa/Messages.properties +++ b/integrations/cdi/jpa-cdi/src/main/resources/io/helidon/integrations/cdi/jpa/Messages.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2019, 2022 Oracle and/or its affiliates. +# Copyright (c) 2019, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ closedStatus = \ closed (status: {0}) closedNotActive = \ closedNotActive ({0}) +jpaExtensionDisabled = \ + {0} has been disabled. jpaTransactionScopedEntityManagerClose = \ You cannot close a container-managed EntityManager. jpaTransactionScopedEntityManagerGetTransaction = \ diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java new file mode 100644 index 00000000000..6f94ae737e0 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/TestPersistenceExtension.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +class TestPersistenceExtension { + + private SeContainer sec; + + private TestPersistenceExtension() { + super(); + } + + @BeforeEach + @SuppressWarnings("unchecked") + final void initializeCdiContainer() { + System.setProperty(JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(PersistenceExtension.class.getName() + ".enabled", "true"); + Class cdiSeJtaPlatformClass; + try { + // Load it dynamically because Hibernate won't be on the classpath when we're testing with Eclipselink + cdiSeJtaPlatformClass = + Class.forName("io.helidon.integrations.cdi.hibernate.CDISEJtaPlatform", + false, + Thread.currentThread().getContextClassLoader()); + } catch (ClassNotFoundException e) { + cdiSeJtaPlatformClass = null; + } + SeContainerInitializer i = SeContainerInitializer.newInstance() + .disableDiscovery() + .addExtensions(PersistenceExtension.class, + com.arjuna.ats.jta.cdi.TransactionExtension.class, + io.helidon.integrations.datasource.hikaricp.cdi.HikariCPBackedDataSourceExtension.class) + .addBeanClasses(Caturgiator.class, + Frobnicator.class); + if (cdiSeJtaPlatformClass != null) { + i = i.addBeanClasses(cdiSeJtaPlatformClass); + } + this.sec = i.initialize(); + } + + @AfterEach + final void closeCdiContainer() { + if (this.sec != null) { + this.sec.close(); + } + System.setProperty(PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(JpaExtension.class.getName() + ".enabled", "true"); + } + + // @Disabled + @Test + final void testSpike() { + Instance fi = sec.select(Frobnicator.class); + Frobnicator f = fi.get(); + assertThat(f.em.isOpen(), is(true)); + assertThat(f.em, instanceOf(JtaEntityManager.class)); + + Instance ci = sec.select(Caturgiator.class); + Caturgiator c = ci.get(); + assertThat(c.em, is(f.em)); + + fi.destroy(f); + assertThat(c.em.isOpen(), is(true)); + ci.destroy(c); + } + + @DataSourceDefinition( + name = "test", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestPersistenceExtension", + serverName = "", + properties = { + "user=sa" + } + ) + @Dependent + private static class Frobnicator { + + @PersistenceContext(unitName = "test") + private EntityManager em; + + @Inject + Frobnicator() { + super(); + } + + } + + @Dependent + private static class Caturgiator { + + @PersistenceContext(unitName = "test") + private EntityManager em; + + @Inject + Caturgiator() { + super(); + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp/Microblog.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp/Microblog.java index a948f951992..9b298eb0414 100644 --- a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp/Microblog.java +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp/Microblog.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2022 Oracle and/or its affiliates. + * Copyright (c) 2019, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -95,6 +95,7 @@ public class Microblog implements Serializable { mappedBy = "microblog", targetEntity = Chirp.class ) + @SuppressWarnings("serial") private List chirps; @Deprecated diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Author.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Author.java new file mode 100644 index 00000000000..a8162526ed0 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Author.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import java.io.Serializable; +import java.util.Objects; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Access(value = AccessType.FIELD) +@Entity(name = "Author") +@Table(name = "AUTHOR") +public class Author implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @Column(name = "ID", + insertable = true, + nullable = false, + updatable = false) + Integer id; + + @Basic(optional = false) + @Column(name = "NAME", + insertable = true, + nullable = false, + unique = true, + updatable = true) + private String name; + + @Deprecated + protected Author() { + super(); + } + + public Author(int id, String name) { + super(); + this.id = id; + this.setName(name); + } + + public Integer getId() { + return id; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = Objects.requireNonNull(name); + } + + @Override + public int hashCode() { + final Object name = this.getName(); + return name == null ? 0 : name.hashCode(); + } + + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; + } else if (other instanceof Author) { + final Author her = (Author) other; + final Object name = this.getName(); + if (name == null) { + if (her.getName() != null) { + return false; + } + } else if (!name.equals(her.getName())) { + return false; + } + return true; + } else { + return false; + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Chirp.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Chirp.java new file mode 100644 index 00000000000..15974a34b97 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Chirp.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import java.io.Serializable; +import java.util.Objects; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Access(value = AccessType.FIELD) +@Entity(name = "Chirp") +@Table(name = "CHIRP") +public class Chirp implements Serializable { + + private static final long serialVersionUID = 1L; + + @Column( + insertable = true, + name = "ID", + nullable = false, + updatable = false + ) + @Id + private Integer id; + + @JoinColumn( + insertable = true, + name = "MICROBLOG_ID", + nullable = false, + referencedColumnName = "ID", + updatable = false + ) + @ManyToOne( + optional = false, + targetEntity = Microblog.class + ) + private Microblog microblog; + + @Basic(optional = false) + @Column( + name = "CONTENT", + insertable = true, + nullable = false, + updatable = true) + private String contents; + + /** + * This constructor exists to fulfil the requirement that all JPA + * entities have a zero-argument constructor and for no other + * purpose. + * + * @deprecated Please use the {@link #Chirp(int, Microblog, String)} + * constructor instead. + */ + @Deprecated + protected Chirp() { + super(); + } + + public Chirp(int id, Microblog microblog, String contents) { + super(); + this.id = id; + this.setMicroblog(microblog); + this.setContents(contents); + } + + public Integer getId() { + return this.id; + } + + public String getContents() { + return this.contents; + } + + public void setContents(final String contents) { + this.contents = Objects.requireNonNull(contents); + } + + public Microblog getMicroblog() { + return this.microblog; + } + + void setMicroblog(final Microblog microblog) { + this.microblog = Objects.requireNonNull(microblog); + } + + @Override + public int hashCode() { + int hashCode = 17; + + final Object contents = this.getContents(); + int c = contents == null ? 0 : contents.hashCode(); + hashCode = 37 * hashCode + c; + + return hashCode; + } + + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; + } else if (other instanceof Chirp) { + final Chirp her = (Chirp) other; + final Object contents = this.getContents(); + if (contents == null) { + if (her.getContents() != null) { + return false; + } + } else if (!contents.equals(her.getContents())) { + return false; + } + + return true; + } else { + return false; + } + } + + @Override + public String toString() { + return this.getContents(); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Microblog.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Microblog.java new file mode 100644 index 00000000000..e2118a41c4f --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/Microblog.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +/** + * A simple collection of small blog articles that will not scale to + * any appreciable volume and is useful only for unit tests. + */ +@Access(value = AccessType.FIELD) +@Entity(name = "Microblog") +@Table( + name = "MICROBLOG", + uniqueConstraints = { + @UniqueConstraint( + columnNames = { + "NAME", + "AUTHOR_ID" + } + ) + } +) +public class Microblog implements Serializable { + + private static final long serialVersionUID = 1L; + + @Column( + insertable = true, + name = "ID", + updatable = false + ) + @Id + private Integer id; + + @JoinColumn( + insertable = true, + name = "AUTHOR_ID", + referencedColumnName = "ID", + updatable = false + ) + @ManyToOne( + cascade = { + CascadeType.PERSIST + }, + optional = false, + targetEntity = Author.class + ) + private Author author; + + @Basic(optional = false) + @Column( + insertable = true, + name = "NAME", + updatable = false + ) + private String name; + + @OneToMany( + cascade = CascadeType.ALL, + fetch = FetchType.LAZY, + mappedBy = "microblog", + targetEntity = Chirp.class + ) + @SuppressWarnings("serial") + private List chirps; + + @Deprecated + protected Microblog() { + super(); + } + + public Microblog(int id, Author author, String name) { + super(); + this.id = id; + this.author = Objects.requireNonNull(author); + this.name = Objects.requireNonNull(name); + } + + public Integer getId() { + return this.id; + } + + public Author getAuthor() { + return this.author; + } + + public String getName() { + return this.name; + } + + public List getChirps() { + return this.chirps; + } + + public void addChirp(final Chirp chirp) { + if (chirp != null) { + List chirps = this.chirps; + if (chirps == null) { + chirps = new ArrayList<>(); + this.chirps = chirps; + } + chirps.add(chirp); + chirp.setMicroblog(this); + } + } + + @Override + public int hashCode() { + int hashCode = 17; + + final Object name = this.getName(); + int c = name == null ? 0 : name.hashCode(); + hashCode = 37 * hashCode + c; + + final Object author = this.getAuthor(); + c = author == null ? 0 : name.hashCode(); + hashCode = 37 * hashCode + c; + + return hashCode; + } + + @Override + public boolean equals(final Object other) { + if (other == this) { + return true; + } else if (other instanceof Microblog) { + final Microblog her = (Microblog) other; + final Object name = this.getName(); + if (name == null) { + if (her.getName() != null) { + return false; + } + } else if (!name.equals(her.getName())) { + return false; + } + + final Object author = this.getAuthor(); + if (author == null) { + if (her.getAuthor() != null) { + return false; + } + } else if (!author.equals(her.getAuthor())) { + return false; + } + + return true; + } else { + return false; + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestCascadePersist2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestCascadePersist2.java new file mode 100644 index 00000000000..59d91e62831 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestCascadePersist2.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.SynchronizationType; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestCascadePersist2;DB_CLOSE_DELAY=-1;MODE=LEGACY;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'\\;", + serverName = "", + properties = { + "user=sa" + } +) +class TestCascadePersist2 { + + private SeContainer cdiContainer; + + @Inject + private TransactionManager transactionManager; + + @PersistenceContext( + type = PersistenceContextType.TRANSACTION, + synchronization = SynchronizationType.SYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager em; + + + /* + * Constructors. + */ + + + TestCascadePersist2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainerAndRunDDL() throws SQLException { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(this.getClass()); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + TestCascadePersist2 self() { + return this.cdiContainer.select(TestCascadePersist2.class).get(); + } + + EntityManager getEntityManager() { + return this.em; + } + + TransactionManager getTransactionManager() { + return this.transactionManager; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) final Object event, + final TransactionManager tm) throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + + + /* + * Test methods. + */ + + + @Test + void testCascadePersist() + throws HeuristicMixedException, + HeuristicRollbackException, + InterruptedException, + NotSupportedException, + RollbackException, + SQLException, + SystemException + { + + // Get a CDI contextual reference to this test instance. It + // is important to use "self" in this test instead of "this". + final TestCascadePersist2 self = self(); + assertThat(self, notNullValue()); + + // Get the EntityManager that is synchronized with and scoped + // to a JTA transaction. + final EntityManager em = self.getEntityManager(); + assertThat(em, notNullValue()); + assertThat(em.isOpen(), is(true)); + + // We haven't started any kind of transaction yet and we + // aren't testing anything using + // the @jakarta.transaction.Transactional annotation so there is + // no transaction in effect so the EntityManager cannot be + // joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Get the TransactionManager that normally is behind the + // scenes and use it to start a Transaction. This simulates + // entering a method annotated + // with @Transactional(TxType.REQUIRES_NEW) or similar. + final TransactionManager tm = self.getTransactionManager(); + tm.setTransactionTimeout(20 * 60); // 20 minutes for debugging + tm.begin(); + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + + // Now magically our EntityManager should be joined to it. + assertThat(em.isJoinedToTransaction(), is(true)); + + // Create an author but don't persist him explicitly. + Author author = new Author(1, "Abraham Lincoln"); + assertThat(author.getId(), is(1)); + + // Set up a blog for that Author. + Microblog blog = new Microblog(1, author, "Gettysburg Address Draft 1"); + + // Persist the blog. The Author should be persisted too. + em.persist(blog); + assertThat(em.contains(blog), is(true)); + assertThat(em.contains(author), is(true)); + + // Commit the transaction. + tm.commit(); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(author.getId(), is(1)); + assertThat(blog.getId(), is(1)); + + // We're no longer in a transaction. + assertThat(em.isJoinedToTransaction(), is(false)); + + // The persistence context should be cleared. + assertThat(em.contains(blog), is(false)); + assertThat(em.contains(author), is(false)); + + // Let's check the database directly. + final DataSource ds = this.cdiContainer.select(DataSource.class).get(); + try (final Connection connection = ds.getConnection(); + final Statement statement = connection.createStatement()) { + ResultSet rs = statement.executeQuery("SELECT COUNT(1) FROM MICROBLOG"); + try { + assertThat(rs.next(), is(true)); + assertThat(rs.getInt(1), is(1)); + } finally { + rs.close(); + } + rs = statement.executeQuery("SELECT COUNT(1) FROM AUTHOR"); + try { + assertThat(rs.next(), is(true)); + assertThat(rs.getInt(1), is(1)); + } finally { + rs.close(); + } + } + + // Start a new transaction. + tm.begin(); + + assertThat(em.contains(blog), is(false)); + final Microblog newBlog = em.find(Microblog.class, Integer.valueOf(1)); + assertThat(newBlog, notNullValue()); + assertThat(em.contains(newBlog), is(true)); + + assertThat(newBlog.getId(), is(blog.getId())); + blog = newBlog; + + // Now let's have our author write some stuff. + final Chirp chirp1 = new Chirp(1, blog, "Four score and seven years ago"); + final Chirp chirp2 = new Chirp(2, blog, "our fathers brought forth on this continent,"); + final Chirp chirp3 = new Chirp(3, blog, "a new nation, conceived in Liberty, " + + "and dedicated to the proposition that all men are created " + + "equal. Now we are engaged in a great civil war, testing " + + "whether that nation, or any nation so conceived and so " + + "dedicated, can long endure."); + blog.addChirp(chirp1); + assertThat(chirp1.getMicroblog(), sameInstance(blog)); + blog.addChirp(chirp2); + assertThat(chirp2.getMicroblog(), sameInstance(blog)); + blog.addChirp(chirp3); + assertThat(chirp3.getMicroblog(), sameInstance(blog)); + + // Commit the transaction. The changes should be propagated. + // However, this will fail, because the third chirp above is + // (deliberately) too long. The transaction should roll back. + try { + tm.commit(); + fail("Commit was able to happen"); + } catch (final RollbackException expected) { + + } + + // Now the question is: were any chirps written? They + // should not have been written (i.e. the rollback should have + // functioned properly. Let's make sure. + try (final Connection connection = ds.getConnection(); + final Statement statement = connection.createStatement()) { + assertThat(connection.getTransactionIsolation(), is(Connection.TRANSACTION_READ_COMMITTED)); + ResultSet rs = statement.executeQuery("SELECT COUNT(1) FROM CHIRP"); + try { + assertThat(rs.next(), is(true)); + // XXX This fails from time to time, returning 1 or 2. + assertThat(rs.getInt(1), is(0)); + } finally { + rs.close(); + } + } + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedSynchronizedEntityManager2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedSynchronizedEntityManager2.java new file mode 100644 index 00000000000..696061460da --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedSynchronizedEntityManager2.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.SynchronizationType; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestExtendedSynchronizedEntityManager2;MODE=LEGACY;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'", + serverName = "", + properties = { + "user=sa" + } +) +class TestExtendedSynchronizedEntityManager2 { + + static { + System.setProperty("jpaAnnotationRewritingEnabled", "true"); + } + + private SeContainer cdiContainer; + + @Inject + private TransactionManager transactionManager; + + @PersistenceContext( + type = PersistenceContextType.EXTENDED, + synchronization = SynchronizationType.SYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager extendedSynchronizedEntityManager; + + + /* + * Constructors. + */ + + + TestExtendedSynchronizedEntityManager2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainer() { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(this.getClass()); + assertThat(initializer, notNullValue()); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + /** + * A "business method" providing access to one of this {@link + * TestJpaTransactionScopedEntityManager}'s {@link EntityManager} + * instances for use by {@link Test}-annotated methods. + * + * @return a non-{@code null} {@link EntityManager} + */ + EntityManager getExtendedSynchronizedEntityManager() { + return this.extendedSynchronizedEntityManager; + } + + TransactionManager getTransactionManager() { + return this.transactionManager; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event, + TransactionManager tm) throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + + + /* + * Test methods. + */ + + + @Test + void testExtendedSynchronizedEntityManager() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException + { + + // Get a CDI contextual reference to this test instance. It + // is important to use "self" in this test instead of "this". + TestExtendedSynchronizedEntityManager2 self = + this.cdiContainer.select(TestExtendedSynchronizedEntityManager2.class).get(); + assertThat(self, notNullValue()); + + // Get the EntityManager that is synchronized with but whose + // persistence context extends past a single JTA transaction. + EntityManager em = self.getExtendedSynchronizedEntityManager(); + assertThat(em, notNullValue()); + assertThat(em.isOpen(), is(true)); + + // We haven't started any kind of transaction yet and we + // aren't testing anything using + // the @jakarta.transaction.Transactional annotation so there is + // no transaction in effect so the EntityManager cannot be + // joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Create a JPA entity and try to insert it. Should be just + // fine. + Author author = new Author(1, "Abraham Lincoln"); + + // With an EXTENDED EntityManager, persisting outside of a + // transaction is OK. + em.persist(author); + + // Our PersistenceContextType is EXTENDED, not TRANSACTION, so + // the underlying persistence context spans transactions. + assertThat(em.contains(author), is(true)); + + // Get the TransactionManager that normally is behind the + // scenes and use it to start a Transaction. + TransactionManager tm = self.getTransactionManager(); + tm.begin(); + + // Now magically our EntityManager should be joined to it. + assertThat(em.isJoinedToTransaction(), is(true)); + + // Roll the transaction back and note that our EntityManager + // is no longer joined to it. + tm.rollback(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author), is(false)); + + // Start another transaction and persist our Author. + tm.begin(); + + try { + // See + // https://www.baeldung.com/hibernate-detached-entity-passed-to-persist#trying-to-persist-a-detached-entity + // and + // https://hibernate.atlassian.net/browse/HHH-15738. Eclipselink + // handles all this just fine. + em.persist(author); + + assertThat(em.contains(author), is(true)); + tm.commit(); + + // The transaction is over, so our EntityManager is not + // joined to one anymore. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Our PersistenceContextType is EXTENDED, not + // TRANSACTION, so the underlying persistence context + // spans transactions. + assertThat(em.contains(author), is(true)); + } catch (PersistenceException hhh15738) { + assertThat(tm.getStatus(), is(Status.STATUS_MARKED_ROLLBACK)); + tm.rollback(); + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedUnsynchronizedEntityManager2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedUnsynchronizedEntityManager2.java new file mode 100644 index 00000000000..575d17440a5 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestExtendedUnsynchronizedEntityManager2.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.PersistenceException; +import jakarta.persistence.SynchronizationType; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestExtendedUnsynchronizedEntityManager2;MODE=LEGACY;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'", + serverName = "", + properties = { + "user=sa" + } +) +class TestExtendedUnsynchronizedEntityManager2 { + + static { + System.setProperty("jpaAnnotationRewritingEnabled", "true"); + } + + private SeContainer cdiContainer; + + @Inject + private TransactionManager transactionManager; + + @PersistenceContext( + type = PersistenceContextType.EXTENDED, + synchronization = SynchronizationType.UNSYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager extendedUnsynchronizedEntityManager; + + + /* + * Constructors. + */ + + + TestExtendedUnsynchronizedEntityManager2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainer() { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(this.getClass()); + assertThat(initializer, notNullValue()); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + /** + * A "business method" providing access to one of this {@link + * TestJpaTransactionScopedEntityManager}'s {@link EntityManager} + * instances for use by {@link Test}-annotated methods. + * + * @return a non-{@code null} {@link EntityManager} + */ + EntityManager getExtendedUnsynchronizedEntityManager() { + return this.extendedUnsynchronizedEntityManager; + } + + TransactionManager getTransactionManager() { + return this.transactionManager; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event, + TransactionManager tm) throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + + /* + * Test methods. + */ + + + @Test + void testExtendedUnsynchronizedEntityManager() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException + { + + // Get a CDI contextual reference to this test instance. It + // is important to use "self" in this test instead of "this". + TestExtendedUnsynchronizedEntityManager2 self = + this.cdiContainer.select(TestExtendedUnsynchronizedEntityManager2.class).get(); + assertThat(self, notNullValue()); + + // Get the EntityManager that is not synchronized with and + // whose persistence context extends past a single JTA + // transaction. + EntityManager em = self.getExtendedUnsynchronizedEntityManager(); + assertThat(em, notNullValue()); + assertThat(em.isOpen(), is(true)); + + // We haven't started any kind of transaction yet and we + // aren't testing anything using + // the @jakarta.transaction.Transactional annotation so there is + // no transaction in effect so the EntityManager cannot be + // joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Create a JPA entity and try to insert it. Should be just + // fine. + Author author = new Author(1, "Abraham Lincoln"); + + // With an EXTENDED EntityManager, persisting outside of a + // transaction is OK. + em.persist(author); + + // Get the TransactionManager that normally is behind the + // scenes and use it to start a Transaction. + TransactionManager tm = self.getTransactionManager(); + assertThat(tm, notNullValue()); + tm.begin(); + + // Because we're UNSYNCHRONIZED, no automatic joining takes place. + assertThat(em.isJoinedToTransaction(), is(false)); + + // We can join manually. + em.joinTransaction(); + + // Now we should be joined to it. + assertThat(em.isJoinedToTransaction(), is(true)); + + // Roll the transaction back and note that our EntityManager + // is no longer joined to it. + tm.rollback(); + assertThat(em.isJoinedToTransaction(), is(false)); + + // Start another transaction and persist our Author. But note + // that joining the transaction must be manual. + tm.begin(); + assertThat(em.isJoinedToTransaction(), is(false)); + + try { + // See + // https://www.baeldung.com/hibernate-detached-entity-passed-to-persist#trying-to-persist-a-detached-entity + // and + // https://hibernate.atlassian.net/browse/HHH-15738. Eclipselink + // handles all this just fine. + em.persist(author); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author), is(true)); + + // (Remember, we weren't ever joined to this transaction.) + tm.commit(); + + // The transaction is over, and our EntityManager is STILL + // not joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Our PersistenceContextType is EXTENDED, not + // TRANSACTION, so the underlying persistence context + // spans transactions. + assertThat(em.contains(author), is(true)); + } catch (PersistenceException hhh15738) { + assertThat(tm.getStatus(), is(Status.STATUS_MARKED_ROLLBACK)); + tm.rollback(); + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedSynchronizedEntityManager2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedSynchronizedEntityManager2.java new file mode 100644 index 00000000000..994e167a71f --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedSynchronizedEntityManager2.java @@ -0,0 +1,442 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.context.spi.Context; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.SynchronizationType; +import jakarta.persistence.TransactionRequiredException; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionScoped; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestJpaTransactionScopedSynchronizedEntityManager2;" + + "MODE=LEGACY;" + + "INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'", + serverName = "", + properties = { + "user=sa" + } +) +class TestJpaTransactionScopedSynchronizedEntityManager2 { + + /* + static { + System.setProperty("jpaAnnotationRewritingEnabled", "true"); + } + */ + + private SeContainer cdiContainer; + + @Inject + private TransactionManager transactionManager; + + @Inject + @Named("chirp2") + private DataSource dataSource; + + @PersistenceContext( + type = PersistenceContextType.TRANSACTION, + synchronization = SynchronizationType.SYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager jpaTransactionScopedSynchronizedEntityManager; + + + /* + * Constructors. + */ + + + TestJpaTransactionScopedSynchronizedEntityManager2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainer() { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(this.getClass()); + assertThat(initializer, notNullValue()); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + /** + * A "business method" providing access to one of this {@link + * TestJpaTransactionScopedEntityManager}'s {@link EntityManager} + * instances for use by {@link Test}-annotated methods. + * + * @return a non-{@code null} {@link EntityManager} + */ + EntityManager getJpaTransactionScopedSynchronizedEntityManager() { + return this.jpaTransactionScopedSynchronizedEntityManager; + } + + TransactionManager getTransactionManager() { + return this.transactionManager; + } + + DataSource getDataSource() { + return this.dataSource; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) final Object event, + final TransactionManager tm) throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + + + /* + * Test methods. + */ + + + @Test + void testJpaTransactionScopedSynchronizedEntityManager() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SQLException, + SystemException + { + + // Get a BeanManager for later use. + final BeanManager beanManager = this.cdiContainer.getBeanManager(); + assertThat(beanManager, notNullValue()); + + // Get a CDI contextual reference to this test instance. It + // is important to use "self" in this test instead of "this". + final TestJpaTransactionScopedSynchronizedEntityManager2 self = + this.cdiContainer.select(TestJpaTransactionScopedSynchronizedEntityManager2.class).get(); + assertThat(self, notNullValue()); + + // Get the EntityManager that is synchronized with and scoped + // to a JTA transaction. + final EntityManager em = self.getJpaTransactionScopedSynchronizedEntityManager(); + assertThat(em, notNullValue()); + assertThat(em.isOpen(), is(true)); + + // Get a DataSource for JPA-independent testing and assertions. + final DataSource dataSource = self.getDataSource(); + assertThat(dataSource, notNullValue()); + + // We haven't started any kind of transaction yet and we + // aren't testing anything using + // the @jakarta.transaction.Transactional annotation so there is + // no transaction in effect so the EntityManager cannot be + // joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Create a JPA entity and try to insert it. This should fail + // because according to JPA a TransactionRequiredException + // will be thrown. + Author author1 = new Author(1, "Abraham Lincoln"); + try { + em.persist(author1); + fail("A TransactionRequiredException should have been thrown"); + } catch (final TransactionRequiredException expected) { + + } + assertThat(em.contains(author1), is(false)); + assertThat(author1.getId(), is(1)); + + // Get the TransactionManager that normally is behind the + // scenes and use it to start a Transaction. + final TransactionManager tm = self.getTransactionManager(); + tm.setTransactionTimeout(60 * 20); // Set to 20 minutes for debugging purposes only + + // Create a new transaction. + tm.begin(); + + // Grab the TransactionScoped context while the transaction is + // active. We want to make sure it's active at various + // points. + final Context transactionScopedContext = beanManager.getContext(TransactionScoped.class); + assertThat(transactionScopedContext, notNullValue()); + assertThat(transactionScopedContext.isActive(), is(true)); + + // Now magically our EntityManager should be joined to it. + assertThat(em.isJoinedToTransaction(), is(true)); + + // Roll the transaction back and note that our EntityManager + // is no longer joined to it. + tm.rollback(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + assertThat(em.contains(author1), is(false)); + assertThat(author1.getId(), is(1)); + + // Start another transaction. + tm.begin(); + assertThat(transactionScopedContext.isActive(), is(true)); + assertThat(em.isJoinedToTransaction(), is(true)); + + // Persist our Author. + assertThat(author1.getId(), is(1)); + em.persist(author1); + assertThat(em.contains(author1), is(true)); + + // Commit the transaction and flush changes to the database. + tm.commit(); + + // After the transaction commits, a flush should happen, and + // the Author is managed, so we should see his ID. + assertThat(author1.getId(), is(1)); + + // Make sure the database contains the changes. + try (final Connection connection = dataSource.getConnection(); + final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement.executeQuery("SELECT ID, NAME FROM AUTHOR WHERE ID = 1");) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + assertThat(resultSet.getInt(1), is(1)); + assertThat(resultSet.getString(2), is("Abraham Lincoln")); + assertThat(resultSet.next(), is(false)); + } + + // The Author, however, is detached, because the transaction + // is over, and because our PersistenceContextType is + // TRANSACTION, not EXTENDED, the underlying persistence + // context dies with the transaction. + assertThat(em.contains(author1), is(false)); + + // The transaction is over, so our EntityManager is not joined + // to one anymore. + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + + // Start a new transaction. + tm.begin(); + assertThat(transactionScopedContext.isActive(), is(true)); + assertThat(em.isJoinedToTransaction(), is(true)); + + // Remove the Author we successfully committed before. We + // have to merge because author1 became detached a few lines + // above. + author1 = em.merge(author1); + assertThat(author1.getId(), is(1)); + assertThat(em.contains(author1), is(true)); + em.remove(author1); + assertThat(em.contains(author1), is(false)); + assertThat(author1.getId(), is(1)); + + // Commit and flush the removal. + tm.commit(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + + // Note that its ID is still 1. + assertThat(author1.getId(), is(1)); + + // After all this activity we should have no rows in any + // tables. + assertTableRowCount(dataSource, "AUTHOR", 0); + + // Start a new transaction, merge our detached author1, and + // commit. + tm.begin(); + assertThat(em.isJoinedToTransaction(), is(true)); + assertThat(transactionScopedContext.isActive(), is(true)); + + // Actually, this really should throw an + // IllegalArgumentException, since author1 was + // removed. Neither Eclipselink nor Hibernate throws an + // exception here. + author1 = em.merge(author1); + + assertThat(em.contains(author1), is(true)); + assertThat(author1.getId(), is(1)); + + tm.commit(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author1), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + assertThat(author1.getId(), is(1)); + + // Make sure the database contains the changes. + try (final Connection connection = dataSource.getConnection(); + final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement.executeQuery("SELECT ID, NAME FROM AUTHOR");) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + assertThat(resultSet.getInt(1), is(1)); + assertThat(resultSet.getString(2), is("Abraham Lincoln")); + assertThat(resultSet.next(), is(false)); + } + + // Discard author1 in this unit test so we'll get a + // NullPointerException if we try to use him again. + author1 = null; + + // Let's find the new author that got merged in. We'll use a + // transaction just for kicks. + tm.begin(); + assertThat(em.isJoinedToTransaction(), is(true)); + assertThat(transactionScopedContext.isActive(), is(true)); + + Author author2 = em.find(Author.class, Integer.valueOf(1)); + assertThat(author2, notNullValue()); + assertThat(em.contains(author2), is(true)); + assertThat(author2.getId(), is(1)); + assertThat(author2.getName(), is("Abraham Lincoln")); + + // No need, really, but it's what a @Transactional method + // would do. + tm.commit(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author2), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + + // New transaction. Let's change the name. + tm.begin(); + assertThat(em.isJoinedToTransaction(), is(true)); + assertThat(transactionScopedContext.isActive(), is(true)); + + author2 = em.find(Author.class, Integer.valueOf(1)); + assertThat(author2, notNullValue()); + + // Remember that finding an entity causes it to become + // managed. + assertThat(em.contains(author2), is(true)); + + assertThat(author2.getId(), is(1)); + assertThat(author2.getName(), is("Abraham Lincoln")); + + author2.setName("Abe Lincoln"); + assertThat(author2.getId(), is(1)); + assertThat(author2.getName(), is("Abe Lincoln")); + + tm.commit(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author2), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + + // Make sure the database contains the changes. + try (final Connection connection = dataSource.getConnection(); + final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement.executeQuery("SELECT ID, NAME FROM AUTHOR");) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + assertThat(resultSet.getInt(1), is(1)); + assertThat(resultSet.getString(2), is("Abe Lincoln")); + assertThat(resultSet.next(), is(false)); + } + + // Let's go find him again. + tm.begin(); + assertThat(em.isJoinedToTransaction(), is(true)); + assertThat(transactionScopedContext.isActive(), is(true)); + + author2 = em.find(Author.class, Integer.valueOf(1)); + assertThat(author2, notNullValue()); + assertThat(em.contains(author2), is(true)); + assertThat(author2.getId(), is(1)); + assertThat(author2.getName(), is("Abe Lincoln")); + + // No need, really, but it's what a @Transactional method + // would do. + tm.commit(); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author2), is(false)); + assertThat(transactionScopedContext.isActive(), is(false)); + + } + + private static final void assertTableRowCount(final DataSource dataSource, + final String upperCaseTableName, + final int expectedCount) + throws SQLException { + try (final Connection connection = dataSource.getConnection(); + final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM " + upperCaseTableName);) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + assertThat(resultSet.getInt(1), is(expectedCount)); + } + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedUnsynchronizedEntityManager2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedUnsynchronizedEntityManager2.java new file mode 100644 index 00000000000..8119f238aaf --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestJpaTransactionScopedUnsynchronizedEntityManager2.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.SynchronizationType; +import jakarta.persistence.TransactionRequiredException; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestJpaTransactionScopedUnsynchronizedEntityManager2;" + + "MODE=LEGACY;" + + "INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'", + serverName = "", + properties = { + "user=sa" + } +) +class TestJpaTransactionScopedUnsynchronizedEntityManager2 { + + static { + System.setProperty("jpaAnnotationRewritingEnabled", "true"); + } + + private SeContainer cdiContainer; + + @Inject + private TransactionManager transactionManager; + + @PersistenceContext( + type = PersistenceContextType.TRANSACTION, + synchronization = SynchronizationType.UNSYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager jpaTransactionScopedUnsynchronizedEntityManager; + + + /* + * Constructors. + */ + + + TestJpaTransactionScopedUnsynchronizedEntityManager2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainer() { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + final SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(this.getClass()); + assertThat(initializer, notNullValue()); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + /** + * A "business method" providing access to one of this {@link + * TestJpaTransactionScopedEntityManager}'s {@link EntityManager} + * instances for use by {@link Test}-annotated methods. + * + * @return a non-{@code null} {@link EntityManager} + */ + EntityManager getJpaTransactionScopedUnsynchronizedEntityManager() { + return this.jpaTransactionScopedUnsynchronizedEntityManager; + } + + TransactionManager getTransactionManager() { + return this.transactionManager; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) final Object event, + final TransactionManager tm) throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + + + /* + * Test methods. + */ + + + @Test + void testJpaTransactionScopedUnsynchronizedEntityManager() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException + { + + // Get a CDI contextual reference to this test instance. It + // is important to use "self" in this test instead of "this". + final TestJpaTransactionScopedUnsynchronizedEntityManager2 self = + this.cdiContainer.select(TestJpaTransactionScopedUnsynchronizedEntityManager2.class).get(); + assertThat(self, notNullValue()); + + // Get the EntityManager that is not synchronized with but is + // scoped to a JTA transaction. + final EntityManager em = self.getJpaTransactionScopedUnsynchronizedEntityManager(); + assertThat(em, notNullValue()); + assertThat(em.isOpen(), is(true)); + + // We haven't started any kind of transaction yet and we + // aren't testing anything using + // the @jakarta.transaction.Transactional annotation so there is + // no transaction in effect so the EntityManager cannot be + // joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Create a JPA entity and try to insert it. This should fail + // because according to JPA a TransactionRequiredException + // will be thrown. + final Author author = new Author(1, "Abraham Lincoln"); + try { + em.persist(author); + fail("A TransactionRequiredException should have been thrown"); + } catch (final TransactionRequiredException expected) { + + } + + // Get the TransactionManager that normally is behind the + // scenes and use it to start a Transaction. + final TransactionManager tm = self.getTransactionManager(); + assertThat(tm, notNullValue()); + tm.begin(); + + // Because we're UNSYNCHRONIZED, no automatic joining takes place. + assertThat(em.isJoinedToTransaction(), is(false)); + + // We can join manually. + em.joinTransaction(); + + // Now we should be joined to it. + assertThat(em.isJoinedToTransaction(), is(true)); + + // Roll the transaction back and note that our EntityManager + // is no longer joined to it. + tm.rollback(); + assertThat(em.isJoinedToTransaction(), is(false)); + + // Start another transaction and persist our Author. But note + // that joining the transaction must be manual. + tm.begin(); + assertThat(em.isJoinedToTransaction(), is(false)); + em.persist(author); + assertThat(em.isJoinedToTransaction(), is(false)); + assertThat(em.contains(author), is(true)); + + // (Remember, we weren't ever joined to this transaction.) + tm.commit(); + + // The transaction is over, and our EntityManager is STILL not joined + // to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Now the weird part. Our EntityManager was of type + // PersistenceContextType.TRANSACTION, but + // SynchronizationType.UNSYNCHRONIZED. So it never joins + // transactions automatically, but its backing persistence + // context does NOT span transactions. + assertThat(em.contains(author), is(false)); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestRollbackScenarios2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestRollbackScenarios2.java new file mode 100644 index 00000000000..a19da894cf6 --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestRollbackScenarios2.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.SynchronizationType; +import jakarta.persistence.TransactionRequiredException; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestRollbackScenarios2;MODE=LEGACY;INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'", + serverName = "", + properties = { + "user=sa" + } +) +class TestRollbackScenarios2 { + + private SeContainer cdiContainer; + + @Inject + private TransactionManager transactionManager; + + @PersistenceContext( + type = PersistenceContextType.TRANSACTION, + synchronization = SynchronizationType.SYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager jpaTransactionScopedSynchronizedEntityManager; + + + /* + * Constructors. + */ + + + TestRollbackScenarios2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainer() { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + SeContainerInitializer initializer = SeContainerInitializer.newInstance() + .addBeanClasses(this.getClass()); + assertThat(initializer, notNullValue()); + this.cdiContainer = initializer.initialize(); + } + + @AfterEach + void shutDownCdiContainer() { + try { + + } finally { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + TestRollbackScenarios2 self() { + return this.cdiContainer.select(TestRollbackScenarios2.class).get(); + } + + /** + * A "business method" providing access to one of this {@link + * TestRollbackScenarios}' {@link EntityManager} + * instances for use by {@link Test}-annotated methods. + * + * @return a non-{@code null} {@link EntityManager} + */ + EntityManager getJpaTransactionScopedSynchronizedEntityManager() { + return this.jpaTransactionScopedSynchronizedEntityManager; + } + + TransactionManager getTransactionManager() { + return this.transactionManager; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event, TransactionManager tm) + throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + try { + this.jpaTransactionScopedSynchronizedEntityManager.clear(); + this.jpaTransactionScopedSynchronizedEntityManager.getEntityManagerFactory().getCache().evictAll(); + } finally { + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + } + + + /* + * Test methods. + */ + + + @Test + void testRollbackScenarios() + throws HeuristicMixedException, + HeuristicRollbackException, + InterruptedException, + NotSupportedException, + RollbackException, + SystemException + { + + // Get a CDI contextual reference to this test instance. It + // is important to use "self" in this test instead of "this". + TestRollbackScenarios2 self = self(); + assertThat(self, notNullValue()); + + // Get the EntityManager that is synchronized with and scoped + // to a JTA transaction. + EntityManager em = self.getJpaTransactionScopedSynchronizedEntityManager(); + assertThat(em, notNullValue()); + assertThat(em.isOpen(), is(true)); + + // We haven't started any kind of transaction yet and we + // aren't testing anything using + // the @jakarta.transaction.Transactional annotation so there is + // no transaction in effect so the EntityManager cannot be + // joined to one. + assertThat(em.isJoinedToTransaction(), is(false)); + + // Get the TransactionManager that normally is behind the + // scenes and use it to start a Transaction. + TransactionManager tm = self.getTransactionManager(); + assertThat(tm, notNullValue()); + tm.setTransactionTimeout(20 * 60); // 20 minutes for debugging + tm.begin(); + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + + // Now magically our EntityManager should be joined to it. + assertThat(em.isJoinedToTransaction(), is(true)); + + // Create a JPA entity and insert it. + Author author = new Author(1, "Abraham Lincoln"); + assertThat(author.getId(), is(1)); + + em.persist(author); + // assertThat(author.getId(), is(nullValue())); + + // Commit the transaction. Because we're relying on the + // default flush mode, this will cause a flush to the + // database, which, in turn, will result in author identifier + // generation. + tm.commit(); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(author.getId(), is(1)); + + // We're no longer in a transaction. + assertThat(em.isJoinedToTransaction(), is(false)); + + // The persistence context should be cleared. + assertThat(em.contains(author), is(false)); + + // Ensure transaction statuses are what we think they are. + tm.begin(); + tm.setRollbackOnly(); + try { + assertThat(tm.getStatus(), is(Status.STATUS_MARKED_ROLLBACK)); + } finally { + tm.rollback(); + } + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + + // We can do non-transactional things. + assertThat(em.isOpen(), is(true)); + author = em.find(Author.class, Integer.valueOf(1)); + assertThat(author, notNullValue()); + + // Note that because we've invoked this somehow outside of a + // transaction everything it touches is detached, per section + // 7.6.2 of the JPA 2.2 specification. + assertThat(em.contains(author), is(false)); + + // Remove everything. + tm.begin(); + author = em.merge(author); + assertThat(author, notNullValue()); + assertThat(em.contains(author), is(true)); + em.remove(author); + tm.commit(); + assertThat(em.contains(author), is(false)); + + // Create a new unmanaged Author. + author = new Author(2, "John Kennedy"); + assertThat(author.getId(), is(2)); + + tm.begin(); + em.persist(author); + + // This assertion depends on the ID generation strategy, + // sadly, and will not necessarily work across JPA providers + // for identity column ID generation. + assertThat(author.getId(), is(2)); + + assertThat(em.contains(author), is(true)); + + // Perform a rollback "in the middle" of a sequence of + // operations and observe that the EntityManager is in the + // proper state throughout. + tm.rollback(); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(em.contains(author), is(false)); + + assertThat(author.getId(), is(2)); + + // Try to remove the now-detached author outside of a + // transaction. Should fail. + try { + em.remove(author); + fail("remove() was allowed to complete without a transaction"); + } catch (IllegalArgumentException | TransactionRequiredException expected) { + // The javadocs say only that either of these exceptions may + // be thrown in this case but do not indicate which one is + // preferred. EclipseLink 2.7.4 throws a + // TransactionRequiredException here. It probably should + // throw an IllegalArgumentException; see + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=553117 + // which is related. + } + + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(em.contains(author), is(false)); + assertThat(author.getId(), is(2)); + + // Start a transaction. + tm.begin(); + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + assertThat(em.isJoinedToTransaction(), is(true)); + + // author is still detached + assertThat(em.contains(author), is(false)); + em.detach(author); // redundant; just making a point + assertThat(em.contains(author), is(false)); + assertThat(author.getId(), is(2)); + + // Try again to remove the detached author, but this time in a + // transaction. Shouldn't matter; should also fail. + try { + em.remove(author); + // We shouldn't get here because author is detached but with + // EclipseLink 2.7.4 we do. See + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=553117. + // fail("remove() was allowed to accept a detached object"); + } catch (IllegalArgumentException expected) { + + } + tm.rollback(); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(em.contains(author), is(false)); + assertThat(author.getId(), is(2)); + + // Remove the author properly. + tm.begin(); + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + assertThat(em.isJoinedToTransaction(), is(true)); + assertThat(em.contains(author), is(false)); + author = em.merge(author); + assertThat(em.contains(author), is(true)); + em.remove(author); + tm.commit(); + assertThat(em.contains(author), is(false)); + + // Cause a timeout-tripped rollback. + tm.setTransactionTimeout(1); // 1 second + author = new Author(3, "Woodrow Wilson"); + tm.begin(); + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + Thread.sleep(1500L); // 1.5 seconds (arbitrarily greater than 1 second) + assertThat(tm.getStatus(), is(Status.STATUS_ROLLEDBACK)); + try { + em.persist(author); + fail("Transaction rolled back but persist still happened"); + } catch (TransactionRequiredException expected) { + + } + tm.rollback(); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + + tm.setTransactionTimeout(0); // set the timeout back to the default (that's what 0 means (!)) + + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestWithTransactionalInterceptors2.java b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestWithTransactionalInterceptors2.java new file mode 100644 index 00000000000..acd9b33213d --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/java/io/helidon/integrations/cdi/jpa/chirp2/TestWithTransactionalInterceptors2.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.chirp2; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import jakarta.annotation.sql.DataSourceDefinition; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.BeforeDestroyed; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.se.SeContainer; +import jakarta.enterprise.inject.se.SeContainerInitializer; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.PersistenceContextType; +import jakarta.persistence.SynchronizationType; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +@ApplicationScoped +@DataSourceDefinition( + name = "chirp2", + className = "org.h2.jdbcx.JdbcDataSource", + url = "jdbc:h2:mem:TestWithTransactionalInterceptors2;" + + "MODE=LEGACY;" + + "INIT=SET TRACE_LEVEL_FILE=4\\;RUNSCRIPT FROM 'classpath:chirp2.ddl'", + serverName = "", + properties = { + "user=sa" + } +) +class TestWithTransactionalInterceptors2 { + + private SeContainer cdiContainer; + + private TestWithTransactionalInterceptors2 self; + + @Inject + private TransactionManager tm; + + @Inject + @Named("chirp2") + private DataSource dataSource; + + @PersistenceContext( + type = PersistenceContextType.TRANSACTION, + synchronization = SynchronizationType.SYNCHRONIZED, + unitName = "chirp2" + ) + private EntityManager em; + + + /* + * Constructors. + */ + + + TestWithTransactionalInterceptors2() { + super(); + } + + + /* + * Setup and teardown methods. + */ + + + @BeforeEach + void startCdiContainer() throws SQLException, SystemException { + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "true"); + assertThat(this.dataSource, nullValue()); + assertThat(this.em, nullValue()); + assertThat(this.tm, nullValue()); + assertThat(this.self, nullValue()); + + SeContainerInitializer initializer = SeContainerInitializer.newInstance().addBeanClasses(this.getClass()); + assertThat(initializer, notNullValue()); + this.cdiContainer = initializer.initialize(); + assertThat(this.cdiContainer, notNullValue()); + + this.self = this.cdiContainer.select(this.getClass()).get(); + assertThat(this.self, notNullValue()); + + this.em = this.self.getEntityManager(); + assertThat(this.em, notNullValue()); + + this.tm = this.self.getTransactionManager(); + assertThat(this.tm, notNullValue()); + + this.dataSource = this.self.getDataSource(); + assertThat(this.dataSource, notNullValue()); + + assertAuthorTableIsEmpty(); + assertNoTransaction(); + } + + @AfterEach + void shutDownCdiContainer() throws Exception { + try { + this.em.clear(); + this.em.getEntityManagerFactory().getCache().evictAll(); + } finally { + if (this.cdiContainer != null) { + this.cdiContainer.close(); + } + } + System.setProperty(io.helidon.integrations.cdi.jpa.PersistenceExtension.class.getName() + ".enabled", "false"); + System.setProperty(io.helidon.integrations.cdi.jpa.JpaExtension.class.getName() + ".enabled", "true"); + } + + + /* + * Support methods. + */ + + + public EntityManager getEntityManager() { + return this.em; + } + + public DataSource getDataSource() { + return this.dataSource; + } + + public TransactionManager getTransactionManager() { + return this.tm; + } + + private void onShutdown(@Observes @BeforeDestroyed(ApplicationScoped.class) Object event, + TransactionManager tm) throws SystemException { + // If an assertion fails, or some other error happens in the + // CDI container, there may be a current transaction that has + // neither been committed nor rolled back. Because the + // Narayana transaction engine is fundamentally static, this + // means that a transaction affiliation with the main thread + // may "leak" into another JUnit test (since JUnit, by + // default, forks once, and then runs all tests in the same + // JVM). CDI, thankfully, will fire an event for the + // application context shutting down, even in the case of + // errors. + if (tm.getStatus() != Status.STATUS_NO_TRANSACTION) { + tm.rollback(); + } + } + + + /* + * Test methods. + */ + + + @Test + void runTestInsertAndVerifyResults() throws Exception { + + // Test a simple insert. Note testInsert() is annotated + // with @Transactional so we invoke it through our "self" + // proxy. + self.testInsert(); + + // The transaction should have committed so is no longer + // active. + assertNoTransaction(); + + // Make sure the operation worked. + try (Connection connection = this.dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT ID FROM AUTHOR");) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + // assertThat(resultSet.getInt(1), is(1)); + assertThat(resultSet.next(), is(false)); + } + + } + + @Test + void runTestFindAndUpdateAndVerifyResults() throws Exception { + // First (re-)run our runTestInsertAndVerifyResults() method, + // which will put an author with ID 1 in the database and + // verify that that worked. + this.runTestInsertAndVerifyResults(); + + // Find him and change his name. Note testFindAndUpdate() is + // annotated with @Transactional so we invoke it through our + // "self" proxy. + self.testFindAndUpdate(); + + // The transaction should have committed so is no longer + // active. + assertNoTransaction(); + + // Make sure the operation worked. + try (Connection connection = this.dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT ID, NAME FROM AUTHOR");) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + // assertThat(resultSet.getInt(1), is(1)); + assertThat(resultSet.getString(2), is("Abe Lincoln")); + assertThat(resultSet.next(), is(false)); + } finally { + self.removeAbe(); + } + } + + + /* + * Transactional methods under test. + */ + + + @Transactional + public void testInsert() throws Exception { + assertActiveTransaction(); + + // Make sure there's nothing in there. + assertAuthorTableIsEmpty(); + + // Persist an Author. + Author author = new Author(1, "Abraham Lincoln"); + em.persist(author); + assertThat(em.contains(author), is(true)); + } + + @Transactional + public void testFindAndUpdate() throws Exception { + assertActiveTransaction(); + // Author author = this.em.find(Author.class, Integer.valueOf(1)); + Author author = (Author) this.em.createQuery("SELECT a FROM Author a WHERE a.name = 'Abraham Lincoln'").getResultList().get(0); + assertThat(author, notNullValue()); + // assertThat(author.getId(), is(1)); + assertThat(author.getName(), is("Abraham Lincoln")); + assertThat(this.em.contains(author), is(true)); + author.setName("Abe Lincoln"); + } + + @Transactional + public void removeAbe() throws Exception { + assertActiveTransaction(); + this.em.remove(this.em.getReference(Author.class, 1)); + } + + + /* + * Assertion-style methods. + */ + + + private void assertAuthorTableIsEmpty() throws SQLException { + assertThat(this.dataSource, notNullValue()); + try (Connection connection = this.dataSource.getConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) FROM AUTHOR");) { + assertThat(resultSet, notNullValue()); + assertThat(resultSet.next(), is(true)); + assertThat(resultSet.getInt(1), is(0)); + assertThat(resultSet.next(), is(false)); + } + } + + private void deleteAllFromAuthorTableAfterTest() throws SQLException, SystemException { + assertThat(this.dataSource, notNullValue()); + // assertNoTransaction(); + try (Connection connection = this.dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.executeUpdate("DELETE FROM AUTHOR"); + } + } + + private void resetAuthorIdentityColumn() throws SQLException { + try (Connection connection = this.dataSource.getConnection(); + Statement statement = connection.createStatement()) { + statement.executeUpdate("ALTER TABLE AUTHOR ALTER COLUMN ID RESTART WITH 1"); + } + } + + private void assertActiveTransaction() throws SystemException { + assertThat(this.tm, notNullValue()); + assertThat(this.tm.getStatus(), is(Status.STATUS_ACTIVE)); + assertThat(this.em, notNullValue()); + assertThat(this.em.isJoinedToTransaction(), is(true)); + } + + private void assertNoTransaction() throws SystemException { + assertThat(this.tm, notNullValue()); + assertThat(this.tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(this.em, notNullValue()); + assertThat(this.em.isJoinedToTransaction(), is(false)); + } + +} diff --git a/integrations/cdi/jpa-cdi/src/test/logging.properties b/integrations/cdi/jpa-cdi/src/test/logging.properties index 86a35652573..7d8a280b591 100644 --- a/integrations/cdi/jpa-cdi/src/test/logging.properties +++ b/integrations/cdi/jpa-cdi/src/test/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2019, 2022 Oracle and/or its affiliates. +# Copyright (c) 2019, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ # limitations under the License. # .level=INFO +com.arjuna.ats.level=WARNING +h2database.level=WARNING handlers=io.helidon.logging.jul.HelidonConsoleHandler - io.helidon.integrations.cdi.jpa.level=WARNING org.eclipse.persistence.level=WARNING +org.h2.level=WARNING org.jboss.weld.level=WARNING -h2database.level=WARNING diff --git a/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml b/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml index 9e245b07fed..e1698885042 100644 --- a/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml +++ b/integrations/cdi/jpa-cdi/src/test/resources/META-INF/persistence.xml @@ -92,4 +92,26 @@ + + A persistence unit to support the Chirp2 series of unit tests. + chirp2 + io.helidon.integrations.cdi.jpa.chirp2.Author + io.helidon.integrations.cdi.jpa.chirp2.Chirp + io.helidon.integrations.cdi.jpa.chirp2.Microblog + + + + + + + + + + + + + + + + diff --git a/integrations/cdi/jpa-cdi/src/test/resources/chirp2.ddl b/integrations/cdi/jpa-cdi/src/test/resources/chirp2.ddl new file mode 100644 index 00000000000..92037f0b6cd --- /dev/null +++ b/integrations/cdi/jpa-cdi/src/test/resources/chirp2.ddl @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS AUTHOR ( + ID INT NOT NULL PRIMARY KEY, + NAME VARCHAR(64) NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS MICROBLOG ( + ID INT NOT NULL PRIMARY KEY, + AUTHOR_ID INT NOT NULL, + NAME VARCHAR(64) NOT NULL, + FOREIGN KEY (AUTHOR_ID) REFERENCES AUTHOR(ID), + CONSTRAINT UNIQUE_NAME_AND_AUTHOR UNIQUE(NAME, AUTHOR_ID) +); + +CREATE TABLE IF NOT EXISTS CHIRP ( + ID INT NOT NULL PRIMARY KEY, + MICROBLOG_ID INT NOT NULL, + CONTENT VARCHAR(140) NOT NULL, + FOREIGN KEY (MICROBLOG_ID) REFERENCES MICROBLOG(ID) +); diff --git a/integrations/jdbc/jdbc/etc/spotbugs/exclude.xml b/integrations/jdbc/jdbc/etc/spotbugs/exclude.xml index 86271f9ff5e..e2d7650aa79 100644 --- a/integrations/jdbc/jdbc/etc/spotbugs/exclude.xml +++ b/integrations/jdbc/jdbc/etc/spotbugs/exclude.xml @@ -1,6 +1,6 @@ +--> - + - + - + diff --git a/integrations/jdbc/jdbc/pom.xml b/integrations/jdbc/jdbc/pom.xml index 9bb593beb9a..e2d7bf9f35e 100644 --- a/integrations/jdbc/jdbc/pom.xml +++ b/integrations/jdbc/jdbc/pom.xml @@ -34,5 +34,29 @@ etc/spotbugs/exclude.xml - + + + + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.hamcrest + hamcrest-all + test + + + + com.h2database + h2 + test + + + + diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractCommonDataSource.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractCommonDataSource.java index d9e47d6bf58..af0a9ba49d6 100644 --- a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractCommonDataSource.java +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractCommonDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,8 @@ import javax.sql.CommonDataSource; /** - * A skeletal implementation of the {@link CommonDataSource} - * interface. + * A JDBC + * 4.3-compliant skeletal implementation of the {@link CommonDataSource} interface. */ public abstract class AbstractCommonDataSource implements CommonDataSource { @@ -32,6 +32,9 @@ public abstract class AbstractCommonDataSource implements CommonDataSource { private PrintWriter logWriter; + /** + * Creates a new {@link AbstractCommonDataSource}. + */ protected AbstractCommonDataSource() { super(); } @@ -42,7 +45,7 @@ public PrintWriter getLogWriter() throws SQLException { } @Override - public void setLogWriter(final PrintWriter logWriter) throws SQLException { + public void setLogWriter(PrintWriter logWriter) throws SQLException { this.logWriter = logWriter; } @@ -52,7 +55,7 @@ public int getLoginTimeout() throws SQLException { } @Override - public void setLoginTimeout(final int loginTimeout) throws SQLException { + public void setLoginTimeout(int loginTimeout) throws SQLException { this.loginTimeout = loginTimeout; } diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractDataSource.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractDataSource.java index 6513ac52fea..da4c3d66d1a 100644 --- a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractDataSource.java +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/AbstractDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,21 +20,25 @@ import javax.sql.DataSource; /** - * A skeletal implementation of the {@link DataSource} interface. + * A JDBC + * 4.3-compliant, skeletal implementation of the {@link DataSource} interface. */ public abstract class AbstractDataSource extends AbstractCommonDataSource implements DataSource { + /** + * Creates a new {@link AbstractDataSource}. + */ protected AbstractDataSource() { super(); } @Override - public boolean isWrapperFor(final Class iface) throws SQLException { + public boolean isWrapperFor(Class iface) throws SQLException { return iface != null && iface.isInstance(this); } @Override - public T unwrap(final Class iface) throws SQLException { + public T unwrap(Class iface) throws SQLException { return iface.cast(this); } diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/ConditionallyCloseableConnection.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/ConditionallyCloseableConnection.java index 7a0889268d9..16cc88c0ad1 100644 --- a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/ConditionallyCloseableConnection.java +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/ConditionallyCloseableConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,24 +15,48 @@ */ package io.helidon.integrations.jdbc; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; import java.sql.SQLException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.ShardingKey; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; /** - * A {@link DelegatingConnection} whose {@link #close()} method - * performs a close only if the {@link #isCloseable()} method returns - * {@code true}. + * A JDBC + * 4.3-compliant {@link DelegatingConnection} whose {@link #close()} method may or may not close it depending on + * other partial state. * *

Thread Safety

* - *

Instances of this class are not necessarily safe for concurrent - * use by multiple threads.

+ *

Instances of this class are not necessarily safe for concurrent use by multiple threads because their {@link + * Connection} delegates may not be. JDBC 4.3 does not require thread safety from any JDBC construct.

+ * + * @see #isClosed() * * @see #isCloseable() * * @see #setCloseable(boolean) * * @see #close() + * + * @see #isClosePending() + * + * @see #ConditionallyCloseableConnection(Connection, boolean, boolean) */ public class ConditionallyCloseableConnection extends DelegatingConnection { @@ -43,14 +67,69 @@ public class ConditionallyCloseableConnection extends DelegatingConnection { /** - * Whether or not the {@link #close()} method will actually close - * this {@link DelegatingConnection}. + * A {@link SQLRunnable} representing the logic run by the {@link #checkOpen()} method. + * + *

This field is never {@code null}.

+ * + *

This field is set based on the value of the {@code strictClosedChecking} argument supplied to the {@link + * #ConditionallyCloseableConnection(Connection, boolean, boolean)} constructor. It may end up deliberately doing + * nothing.

+ * + * @see #ConditionallyCloseableConnection(Connection, boolean, boolean) + */ + private final SQLRunnable closedChecker; + + /** + * A {@link SQLBooleanSupplier} that is the effective body of the {@link #isClosed()} method. + * + *

This field is never {@code null}.

+ * + *

This field is set based on the value of the {@code strictClosedChecking} argument supplied to the {@link + * #ConditionallyCloseableConnection(Connection, boolean, boolean)} constructor. + * + * @see #isClosed() + * + * @see #ConditionallyCloseableConnection(Connection, boolean, boolean) + */ + private SQLBooleanSupplier isClosedFunction; + + /** + * The internal state of this {@link ConditionallyCloseableConnection}. + * + *

This field is never {@code null}.

+ * + * + * + * @see #isClosed() * * @see #isCloseable() * * @see #setCloseable(boolean) + * + * @see #close() + * + * @see #isClosePending() + * + * @see #ConditionallyCloseableConnection(Connection, boolean, boolean) */ - private boolean closeable; + private volatile State state; /* @@ -59,44 +138,93 @@ public class ConditionallyCloseableConnection extends DelegatingConnection { /** - * Creates a new {@link ConditionallyCloseableConnection} and - * {@linkplain #setCloseable(boolean) sets its closeable status to - * true}. + * Creates a new {@link ConditionallyCloseableConnection} and {@linkplain #setCloseable(boolean) sets its closeable + * status to true}. * - * @param delegate the {@link Connection} to wrap; must not be - * {@code null} + * @param delegate the {@link Connection} to wrap; must not be {@code null} * - * @exception NullPointerException if {@code delegate} is {@code - * null} + * @exception NullPointerException if {@code delegate} is {@code null} * - * @see #ConditionallyCloseableConnection(Connection, boolean) + * @see #ConditionallyCloseableConnection(Connection, boolean, boolean) * - * @see #setCloseable(boolean) + * @deprecated This constructor continues to exist for backwards compatibility only and its use is strongly + * discouraged. Please use the {@link #ConditionallyCloseableConnection(Connection, boolean, boolean)} + * constructor instead and consider supplying {@code true} for its {@code strictClosedChecking} + * parameter. In the future, this constructor may change, without prior notice, to cause new {@link + * ConditionallyCloseableConnection} instances created by it to behave as if they were created by invocations of the + * {@link #ConditionallyCloseableConnection(Connection, boolean, boolean)} constructor instead, with {@code true} + * supplied for its {@code strictClosedChecking} parameter. + */ + @Deprecated(since = "3.0.3") + public ConditionallyCloseableConnection(Connection delegate) { + this(delegate, true, false); + } + + /** + * Creates a new {@link ConditionallyCloseableConnection}. + * + * @param delegate the {@link Connection} to wrap; must not be {@code null} + * + * @param closeable the initial value for this {@link ConditionallyCloseableConnection}'s {@linkplain #isCloseable() + * closeable} status + * + * @exception NullPointerException if {@code delegate} is {@code null} + * + * @see ConditionallyCloseableConnection(Connection, boolean, boolean) + * + * @deprecated This constructor continues to exist for backwards compatibility only and its use is strongly + * discouraged. Please use the {@link #ConditionallyCloseableConnection(Connection, boolean, boolean)} + * constructor instead and consider supplying {@code true} for its {@code strictClosedChecking} + * parameter. In the future, this constructor may change, without prior notice, to cause new {@link + * ConditionallyCloseableConnection} instances created by it to behave as if they were created by invocations of the + * {@link #ConditionallyCloseableConnection(Connection, boolean, boolean)} constructor instead, with {@code true} + * supplied for its {@code strictClosedChecking} parameter. */ - public ConditionallyCloseableConnection(final Connection delegate) { - this(delegate, true); + @Deprecated(since = "3.0.3") + public ConditionallyCloseableConnection(Connection delegate, boolean closeable) { + this(delegate, closeable, false); } /** * Creates a new {@link ConditionallyCloseableConnection}. * - * @param delegate the {@link Connection} to wrap; must not be - * {@code null} + * @param delegate the {@link Connection} to wrap; must not be {@code null} * - * @param closeable the initial value for this {@link - * ConditionallyCloseableConnection}'s {@linkplain #isCloseable() + * @param closeable the initial value for this {@link ConditionallyCloseableConnection}'s {@linkplain #isCloseable() * closeable} status * - * @exception NullPointerException if {@code delegate} is {@code - * null} + * @param strictClosedChecking if {@code true}, then this {@link ConditionallyCloseableConnection}'s {@link + * #isClosed()} method will be invoked before every operation that cannot take place on a closed connection, and, if + * it returns {@code true}, the operation in question will fail with a {@link SQLException}; it is strongly + * recommended to supply {@code true} as the argument for this parameter ({@code false} is permitted for + * backwards compatibility reasons only) + * + * @exception NullPointerException if {@code delegate} is {@code null} + * + * @see #isCloseable() * * @see #setCloseable(boolean) * + * @see #close() + * + * @see #isClosed() + * + * @see #isClosePending() + * * @see DelegatingConnection#DelegatingConnection(Connection) */ - public ConditionallyCloseableConnection(final Connection delegate, final boolean closeable) { + public ConditionallyCloseableConnection(Connection delegate, + boolean closeable, + boolean strictClosedChecking) { super(delegate); - this.setCloseable(closeable); + if (strictClosedChecking) { + this.closedChecker = this::failWhenClosed; + this.isClosedFunction = this::strictIsClosed; + } else { + this.closedChecker = ConditionallyCloseableConnection::doNothing; + this.isClosedFunction = super::isClosed; + } + this.state = closeable ? State.CLOSEABLE : State.NOT_CLOSEABLE; } @@ -106,74 +234,742 @@ public ConditionallyCloseableConnection(final Connection delegate, final boolean /** - * Overrides the {@link DelegatingConnection#close()} method so - * that when it is invoked this {@link - * ConditionallyCloseableConnection} is {@linkplain - * Connection#close() closed} only if it {@linkplain - * #isCloseable() is closeable}. + * Overrides the {@link DelegatingConnection#close()} method so that when it is invoked this {@link + * ConditionallyCloseableConnection} is {@linkplain Connection#close() closed} only if it {@linkplain #isCloseable() + * is closeable}. + * + *

Subclasses that override this method must not directly or indirectly call {@link #failWhenClosed()} or + * undefined behavior may result.

* - *

Overrides should normally call {@code super.close()} as part - * of their implementation.

+ *

If {@code strictClosedChecking} was {@code true} {@linkplain #ConditionallyCloseableConnection(Connection, + * boolean, boolean) at construction time} (strongly recommended), then the following pre- and post-conditions + * apply:

+ * + *

If {@link #isCloseable()} returns {@code true} at the point of an invocation of this method, then after this + * method completes, successfully or not, {@link #isClosePending()} will return {@code false}.

+ * + *

If {@link #isCloseable()} returns {@code false} at the point of an invocation of this method, then after this + * method completes, successfully or not, {@link #isClosePending()} will return {@code true}.

+ * + *

Overrides should normally call {@code super.close()} as part of their implementation.

* * @exception SQLException if an error occurs * + * @see #isClosed() + * * @see #isCloseable() + * + * @see #setCloseable(boolean) + * + * @see #isClosePending() */ - @Override + @Override // DelegatingConnection public void close() throws SQLException { - if (this.isCloseable()) { - super.close(); + // this.checkOpen(); // Deliberately omitted per spec. + switch (this.state) { + case CLOSEABLE: + try { + super.close(); + } finally { + this.state = State.CLOSED; + this.onClose(); + } + break; + case NOT_CLOSEABLE: + this.state = State.CLOSE_PENDING; + break; + case CLOSE_PENDING: + break; + case CLOSED: + try { + super.close(); + } finally { + this.onClose(); + } + break; + default: + throw new AssertionError(); } } /** - * Returns {@code true} if a call to {@link #close()} will - * actually close this {@link ConditionallyCloseableConnection}. + * Called by the {@link #close()} method to perform work after an actual close operation has completed. + * + *

During an invocation of this method by the {@link #close()} method:

+ * + *
    + * + *
  • The {@link #isClosed()} method will return {@code true}.
  • + * + *
  • The {@link #isCloseable()} method will return {@code false}.
  • + * + *
  • The {@link #isClosePending()} method will return {@code false}.
  • * - *

    This method returns {@code true} when {@link - * #setCloseable(boolean)} has been called with a value of {@code - * true} and the {@link #isClosed()} method returns {@code - * false}.

    + *
* - * @return {@code true} if a call to {@link #close()} will - * actually close this {@link ConditionallyCloseableConnection}; - * {@code false} in all other cases + *

The default implementation of this method does nothing.

* - * @exception SQLException if {@link #isClosed()} throws a {@link - * SQLException} + *

Invoking this method directly may result in undefined behavior, depending on how it is overridden.

+ * + *

Overrides of this method must not call {@link #close()} or undefined behavior, such as an infinite loop, may + * result.

+ * + *

Overrides of this method must be idempotent.

+ * + * @exception SQLException if a database error occurs + * + * @see #isClosed() + * + * @see #isCloseable() * * @see #setCloseable(boolean) * + * @see #isClosePending() + * * @see #close() + */ + protected void onClose() throws SQLException { + + } + + /** + * Returns {@code true} if a call to {@link #close()} will actually close this {@link + * ConditionallyCloseableConnection}. + * + *

This method returns {@code true} when {@link #setCloseable(boolean)} has been called with a value of {@code + * true} and the {@link #isClosed()} method returns {@code false}.

+ * + *

Subclasses that override this method must not directly or indirectly call {@link #failWhenClosed()} or + * undefined behavior may result.

+ * + * @return {@code true} if a call to {@link #close()} will actually close this {@link + * ConditionallyCloseableConnection}; {@code false} in all other cases + * + * @exception SQLException if {@link #isClosed()} throws a {@link SQLException} * * @see #isClosed() + * + * @see #setCloseable(boolean) + * + * @see #close() + * + * @see #isClosePending() */ - public final boolean isCloseable() throws SQLException { - return this.closeable && !this.isClosed(); + public boolean isCloseable() throws SQLException { + // this.checkOpen(); // Deliberately omitted. + switch (this.state) { + case CLOSEABLE: + return !this.isClosed(); // reduces to !super.isClosed() which reduces to !this.delegate().isClosed() + case NOT_CLOSEABLE: + assert !this.isClosed(); // reduces to !super.isClosed() which reduces to !this.delegate().isClosed() + return false; + case CLOSE_PENDING: + // (Can't assert about isClosed() because its behavior depends on strictClosedChecking constructor parameter.) + return false; + case CLOSED: + assert this.isClosed(); // reduces to super.isClosed() which reduces to this.delegate().isClosed() + return false; + default: + throw new AssertionError(); + } } /** - * Sets the closeable status of this {@link - * ConditionallyCloseableConnection}. + * Sets the closeable status of this {@link ConditionallyCloseableConnection} and, if the supplied {@code closeable} + * agrument is {@code true}, sets the {@linkplain #isClosePending() close pending status} to {@code false}. + * + *

Subclasses that override this method must not directly or indirectly call {@link #failWhenClosed()} or + * undefined behavior may result.

+ * + *

Note that calling this method with a value of {@code true} does not necessarily mean that the {@link + * #isCloseable()} method will subsequently return {@code true}, since the {@link #isClosed()} method may return + * {@code true}.

+ * + *

Design Note

* - *

Note that calling this method with a value of {@code true} - * does not necessarily mean that the {@link #isCloseable()} - * method will subsequently return {@code true}, since the {@link - * #isClosed()} method may return {@code true}.

+ *

This method does not throw {@link SQLException} only because of an oversight in the design of the original + * version of this class. Callers should consider catching {@link UncheckedSQLException} where appropriate + * instead. The default implementation of this method does not throw any exceptions of any kind.

* - * @param closeable whether or not a call to {@link #close()} will - * actually close this {@link ConditionallyCloseableConnection} + * @param closeable whether or not a call to {@link #close()} will actually close this {@link + * ConditionallyCloseableConnection} + * + * @see #isClosed() * * @see #isCloseable() * * @see #close() * + * @see #isClosePending() + * * @see Connection#close() + */ + public void setCloseable(boolean closeable) { + // this.checkOpen(); // Deliberately omitted. + switch (this.state) { + case CLOSEABLE: + if (!closeable) { + this.state = State.NOT_CLOSEABLE; + } + break; + case NOT_CLOSEABLE: + case CLOSE_PENDING: + if (closeable) { + this.state = State.CLOSEABLE; + } + break; + case CLOSED: + break; + default: + throw new AssertionError(); + } + } + + /** + * Returns {@code true} if and only if this {@link ConditionallyCloseableConnection} is behaving as if the {@link + * #close()} method has been invoked while this {@link ConditionallyCloseableConnection} was {@linkplain + * #isCloseable() not closeable}. + * + *

Subclasses that override this method must not directly or indirectly call {@link #failWhenClosed()} or + * undefined behavior may result.

+ * + *

Subclasses that override this method must not directly or indirectly mutate the state of this {@link + * ConditionallyCloseableConnection} or undefined behavior may result.

+ * + * @return {@code true} if and only if a close operation is pending + * + * @see #isClosed() + * + * @see #isCloseable() + * + * @see #setCloseable(boolean) + * + * @see #close() + */ + public boolean isClosePending() { + // this.checkOpen(); // Deliberately omitted. + return this.state == State.CLOSE_PENDING; + } + + @Override // DelegatingConnection + public Statement createStatement() throws SQLException { + this.checkOpen(); + return super.createStatement(); + } + + @Override // DelegatingConnection + public PreparedStatement prepareStatement(String sql) throws SQLException { + this.checkOpen(); + return super.prepareStatement(sql); + } + + @Override // DelegatingConnection + public CallableStatement prepareCall(String sql) throws SQLException { + this.checkOpen(); + return super.prepareCall(sql); + } + + @Override // DelegatingConnection + public String nativeSQL(String sql) throws SQLException { + this.checkOpen(); + return super.nativeSQL(sql); + } + + @Override // DelegatingConnection + public void setAutoCommit(boolean autoCommit) throws SQLException { + this.checkOpen(); + super.setAutoCommit(autoCommit); + } + + @Override // DelegatingConnection + public boolean getAutoCommit() throws SQLException { + this.checkOpen(); + return super.getAutoCommit(); + } + + @Override // DelegatingConnection + public void commit() throws SQLException { + this.checkOpen(); + super.commit(); + } + + @Override // DelegatingConnection + public void rollback() throws SQLException { + this.checkOpen(); + super.rollback(); + } + + /** + * Returns {@code true} if and only if this {@link ConditionallyCloseableConnection} either is, or is to be + * considered to be, closed, such that operations which must throw a {@link SQLException} when invoked on a closed + * connection will do so. + * + *

If {@code true} was supplied for the {@code strictClosedChecking} parameter {@linkplain + * #ConditionallyCloseableConnection(Connection, boolean, boolean) at construction time} (strongly recommended), the + * default implementation of this method returns a value as if produced by the following implementation: {@code + * this.}{@link #isClosePending() isClosePending() }{@code || super.isClosed()}.

+ * + *

If {@code false} was supplied for the {@code strictClosedChecking} parameter {@linkplain + * #ConditionallyCloseableConnection(Connection, boolean, boolean) at construction time} (not recommended), the + * default implementation of this method returns a value as if produced by the following implementation: {@code + * super.isClosed()}.

+ * + *

Subclasses that override this method must not directly or indirectly call {@link #failWhenClosed()} or + * undefined behavior may result.

+ * + * @return {@code true} if and only if this {@link ConditionallyCloseableConnection} either is, or is to be + * considered to be, closed + * + * @exception SQLException if a database access error occurs + * + * @see #isCloseable() + * + * @see #setCloseable(boolean) + * + * @see #close() + * + * @see #isClosePending() + */ + @Override // DelegatingConnection + public boolean isClosed() throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec (and common sense). + return this.isClosedFunction.getAsBoolean(); + } + + // (Invoked by method reference only.) + private boolean strictIsClosed() throws SQLException { + return this.isClosePending() || super.isClosed(); + } + + @Override // DelegatingConnection + public DatabaseMetaData getMetaData() throws SQLException { + this.checkOpen(); + return super.getMetaData(); + } + + @Override // DelegatingConnection + public void setReadOnly(boolean readOnly) throws SQLException { + this.checkOpen(); + super.setReadOnly(readOnly); + } + + @Override // DelegatingConnection + public boolean isReadOnly() throws SQLException { + this.checkOpen(); + return super.isReadOnly(); + } + + @Override // DelegatingConnection + public void setCatalog(String catalog) throws SQLException { + this.checkOpen(); + super.setCatalog(catalog); + } + + @Override // DelegatingConnection + public String getCatalog() throws SQLException { + this.checkOpen(); + return super.getCatalog(); + } + + @Override // DelegatingConnection + public void setTransactionIsolation(int level) throws SQLException { + this.checkOpen(); + super.setTransactionIsolation(level); + } + + @Override // DelegatingConnection + public int getTransactionIsolation() throws SQLException { + this.checkOpen(); + return super.getTransactionIsolation(); + } + + @Override // DelegatingConnection + public SQLWarning getWarnings() throws SQLException { + this.checkOpen(); + return super.getWarnings(); + } + + @Override // DelegatingConnection + public void clearWarnings() throws SQLException { + this.checkOpen(); + super.clearWarnings(); + } + + @Override // DelegatingConnection + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + this.checkOpen(); + return super.createStatement(resultSetType, resultSetConcurrency); + } + + @Override // DelegatingConnection + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + this.checkOpen(); + return super.prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override // DelegatingConnection + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + this.checkOpen(); + return super.prepareCall(sql, resultSetType, resultSetConcurrency); + } + + @Override // DelegatingConnection + public Map> getTypeMap() throws SQLException { + this.checkOpen(); + return super.getTypeMap(); + } + + @Override // DelegatingConnection + public void setTypeMap(Map> map) throws SQLException { + this.checkOpen(); + super.setTypeMap(map); + } + + @Override // DelegatingConnection + public void setHoldability(int holdability) throws SQLException { + this.checkOpen(); + super.setHoldability(holdability); + } + + @Override // DelegatingConnection + public int getHoldability() throws SQLException { + this.checkOpen(); + return super.getHoldability(); + } + + @Override // DelegatingConnection + public Savepoint setSavepoint() throws SQLException { + this.checkOpen(); + return super.setSavepoint(); + } + + @Override // DelegatingConnection + public Savepoint setSavepoint(String name) throws SQLException { + this.checkOpen(); + return super.setSavepoint(name); + } + + @Override // DelegatingConnection + public void rollback(Savepoint savepoint) throws SQLException { + this.checkOpen(); + super.rollback(savepoint); + } + + @Override // DelegatingConnection + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + this.checkOpen(); + super.releaseSavepoint(savepoint); + } + + @Override // DelegatingConnection + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + this.checkOpen(); + return super.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override // DelegatingConnection + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + this.checkOpen(); + return super.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override // DelegatingConnection + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + this.checkOpen(); + return super.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override // DelegatingConnection + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + this.checkOpen(); + return super.prepareStatement(sql, autoGeneratedKeys); + } + + @Override // DelegatingConnection + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + this.checkOpen(); + return super.prepareStatement(sql, columnIndexes); + } + + @Override // DelegatingConnection + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + this.checkOpen(); + return super.prepareStatement(sql, columnNames); + } + + @Override // DelegatingConnection + public Clob createClob() throws SQLException { + this.checkOpen(); + return super.createClob(); + } + + @Override // DelegatingConnection + public Blob createBlob() throws SQLException { + this.checkOpen(); + return super.createBlob(); + } + + @Override // DelegatingConnection + public NClob createNClob() throws SQLException { + this.checkOpen(); + return super.createNClob(); + } + + @Override // DelegatingConnection + public SQLXML createSQLXML() throws SQLException { + this.checkOpen(); + return super.createSQLXML(); + } + + @Override // DelegatingConnection + public boolean isValid(int timeout) throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + return super.isValid(timeout); + } + + @Override // DelegatingConnection + public void setClientInfo(String name, String value) throws SQLClientInfoException { + try { + this.checkOpen(); + } catch (SQLClientInfoException e) { + throw e; + } catch (SQLException e) { + throw new SQLClientInfoException(e.getMessage(), e.getSQLState(), e.getErrorCode(), Map.of(), e); + } + super.setClientInfo(name, value); + } + + @Override // DelegatingConnection + public void setClientInfo(Properties properties) throws SQLClientInfoException { + try { + this.checkOpen(); + } catch (SQLClientInfoException e) { + throw e; + } catch (SQLException e) { + throw new SQLClientInfoException(e.getMessage(), e.getSQLState(), e.getErrorCode(), Map.of(), e); + } + super.setClientInfo(properties); + } + + @Override // DelegatingConnection + public String getClientInfo(String name) throws SQLException { + this.checkOpen(); + return super.getClientInfo(name); + } + + @Override // DelegatingConnection + public Properties getClientInfo() throws SQLException { + this.checkOpen(); + return super.getClientInfo(); + } + + @Override // DelegatingConnection + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + this.checkOpen(); + return super.createArrayOf(typeName, elements); + } + + @Override // DelegatingConnection + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + this.checkOpen(); + return super.createStruct(typeName, attributes); + } + + @Override // DelegatingConnection + public void setSchema(String schema) throws SQLException { + this.checkOpen(); + super.setSchema(schema); + } + + @Override // DelegatingConnection + public String getSchema() throws SQLException { + this.checkOpen(); + return super.getSchema(); + } + + @Override // DelegatingConnection + public void abort(Executor executor) throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + super.abort(executor); + } + + @Override // DelegatingConnection + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + this.checkOpen(); + super.setNetworkTimeout(executor, milliseconds); + } + + @Override // DelegatingConnection + public int getNetworkTimeout() throws SQLException { + this.checkOpen(); + return super.getNetworkTimeout(); + } + + @Override // DelegatingConnection + public void beginRequest() throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + super.beginRequest(); + } + + @Override // DelegatingConnection + public void endRequest() throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + super.endRequest(); + } + + @Override // DelegatingConnection + public boolean setShardingKeyIfValid(ShardingKey shardingKey, ShardingKey superShardingKey, int timeout) + throws SQLException { + this.checkOpen(); + return super.setShardingKeyIfValid(shardingKey, superShardingKey, timeout); + } + + @Override // DelegatingConnection + public boolean setShardingKeyIfValid(ShardingKey shardingKey, int timeout) throws SQLException { + this.checkOpen(); + return super.setShardingKeyIfValid(shardingKey, timeout); + } + + @Override // DelegatingConnection + public void setShardingKey(ShardingKey shardingKey, ShardingKey superShardingKey) throws SQLException { + this.checkOpen(); + super.setShardingKey(shardingKey, superShardingKey); + } + + @Override // DelegatingConnection + public void setShardingKey(ShardingKey shardingKey) throws SQLException { + this.checkOpen(); + super.setShardingKey(shardingKey); + } + + @Override // DelegatingConnection + public T unwrap(Class iface) throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + return super.unwrap(iface); + } + + @Override // DelegatingConnection + public boolean isWrapperFor(Class iface) throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + return super.isWrapperFor(iface); + } + + /** + * Ensures this {@link ConditionallyCloseableConnection} is {@linkplain #isClosed() not closed}, if {@linkplain + * #ConditionallyCloseableConnection(Connection, boolean, boolean) strict closed checking was enabled at + * construction time}, or simply returns if {@linkplain #ConditionallyCloseableConnection(Connection, boolean, + * boolean) strict closed checking was not enabled at construction time}. + * + *

This method is called from almost every method in this class.

+ * + * @exception SQLException if this {@link ConditionallyCloseableConnection} was {@linkplain + * #ConditionallyCloseableConnection(Connection, boolean, boolean) created with strict closed checking enabled} and + * an invocation of the {@link #isClosed()} method returns {@code true}, or if some other database access error + * occurs + * + * @see #closedChecker + */ + private void checkOpen() throws SQLException { + this.closedChecker.run(); + } + + /** + * Invokes the {@link #isClosed()} method, and, if it returns {@code true}, throws a new {@link SQLException} + * indicating that because the connection is closed the operation cannot proceed. + * + *

If this {@link ConditionallyCloseableConnection} was {@linkplain #ConditionallyCloseableConnection(Connection, + * boolean, boolean) created with strict closed checking enabled} (strongly recommended), then this method will be + * called where appropriate. Otherwise this method is not called internally by default implementations of the + * methods in the {@link ConditionallyCloseableConnection} class. Subclasses may, and often will, call this method + * directly for any reason.

+ * + * @exception SQLNonTransientConnectionException when an invocation of the {@link #isClosed()} method returns {@code + * true}; its {@linkplain SQLException#getSQLState() SQLState} will begin with {@code 08} + * + * @exception SQLException if {@link #isClosed()} throws a {@link SQLException} * * @see #isClosed() + * + * @see #isCloseable() + * + * @see #setCloseable(boolean) + * + * @see #close() */ - public final void setCloseable(final boolean closeable) { - this.closeable = closeable; + protected final void failWhenClosed() throws SQLException { + if (this.isClosed()) { + throw new SQLNonTransientConnectionException("Connection is closed", "08000"); + } + } + + + /* + * Static methods. + */ + + + /** + * Deliberately does nothing when invoked. + * + *

Used as a method reference only, and then only as a potential value for the {@link #closedChecker} field, and + * then only when the {@code strictClosedChecking} argument supplied to the {@link + * #ConditionallyCloseableConnection(Connection, boolean, boolean)} constructor was {@code false}.

+ * + * @see #closedChecker + * + * @see #ConditionallyCloseableConnection(Connection, boolean, boolean) + */ + // (Invoked by method reference only.) + private static void doNothing() { + + } + + + /* + * Inner and nested classes. + */ + + + /** + * A state that a {@link ConditionallyCloseableConnection} can have. + */ + private enum State { + + /** + * A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link + * ConditionallyCloseableConnection#close() close()} method will close {@linkplain + * ConditionallyCloseableConnection#delegate() its underlying delegate}. + */ + CLOSEABLE, + + /** + * A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link + * ConditionallyCloseableConnection#close() close()} method will place it into the {@link #CLOSE_PENDING} state. + * + * @see ConditionallyCloseableConnection#setCloseable(boolean) + */ + NOT_CLOSEABLE, + + /** + * A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link + * ConditionallyCloseableConnection#close() close()} method has placed it into this state, and actual closing of + * {@linkplain ConditionallyCloseableConnection#delegate() its underlying delegate} will need to be arranged. + * + * @see ConditionallyCloseableConnection#setCloseable(boolean) + */ + CLOSE_PENDING, + + /** + * A {@link State} indicating that an invocation of a {@link ConditionallyCloseableConnection}'s {@link + * ConditionallyCloseableConnection#close() close()} method has placed it into this state, and that {@linkplain + * ConditionallyCloseableConnection#delegate() its underlying delegate} has also been closed. + * + *

This is a terminal state.

+ */ + CLOSED; + } } diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingCallableStatement.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingCallableStatement.java new file mode 100644 index 00000000000..3db705af621 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingCallableStatement.java @@ -0,0 +1,808 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; + +/** + * A JDBC + * 4.3-compliant {@link CallableStatement} that delegates to another JDBC 4.3-compliant {@link CallableStatement}. + */ +public class DelegatingCallableStatement extends DelegatingPreparedStatement implements CallableStatement { + + /** + * Creates a new {@link DelegatingCallableStatement}. + * + * @param connection the {@link Connection} that created this {@link DelegatingCallableStatement}; must not be + * {@code null} + * + * @param delegate the {@link CallableStatement} instance to which all operations will be delegated; must not be + * {@code null} + * + * @param closeable the initial value for this {@link DelegatingCallableStatement}'s {@linkplain #isCloseable() + * closeable} status + * + * @param strictClosedChecking if {@code true}, then this {@link DelegatingCallableStatement}'s {@link + * #isClosed()} method will be invoked before every operation that cannot take place on a closed statement, and, if + * it returns {@code true}, the operation in question will fail with a {@link SQLException} + * @exception NullPointerException if either argument is {@code + * null} + * + * @see DelegatingStatement#isCloseable() + * + * @see DelegatingStatement#setCloseable(boolean) + * + * @see DelegatingStatement#close() + * + * @see DelegatingStatement#isClosed() + * + * @exception NullPointerException if either {@code connection} or + * {@code delegate} is {@code null} + */ + public DelegatingCallableStatement(Connection connection, + CallableStatement delegate, + boolean closeable, + boolean strictClosedChecking) { + super(connection, delegate, closeable, strictClosedChecking); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterIndex, sqlType); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterIndex, sqlType, scale); + } + + @Override + public boolean wasNull() throws SQLException { + checkOpen(); + return this.delegate().wasNull(); + } + + @Override + public String getString(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getString(parameterIndex); + } + + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getBoolean(parameterIndex); + } + + @Override + public byte getByte(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getByte(parameterIndex); + } + + @Override + public short getShort(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getShort(parameterIndex); + } + + @Override + public int getInt(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getInt(parameterIndex); + } + + @Override + public long getLong(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getLong(parameterIndex); + } + + @Override + public float getFloat(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getFloat(parameterIndex); + } + + @Override + public double getDouble(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getDouble(parameterIndex); + } + + @Deprecated + @Override + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(parameterIndex, scale); + } + + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getBytes(parameterIndex); + } + + @Override + public Date getDate(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getDate(parameterIndex); + } + + @Override + public Time getTime(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getTime(parameterIndex); + } + + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(parameterIndex); + } + + @Override + public Object getObject(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getObject(parameterIndex); + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(parameterIndex); + } + + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + checkOpen(); + return this.delegate().getObject(parameterIndex, map); + } + + @Override + public Ref getRef(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getRef(parameterIndex); + } + + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getBlob(parameterIndex); + } + + @Override + public Clob getClob(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getClob(parameterIndex); + } + + @Override + public Array getArray(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getArray(parameterIndex); + } + + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getDate(parameterIndex, cal); + } + + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTime(parameterIndex, cal); + } + + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(parameterIndex, cal); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterIndex, sqlType, typeName); + } + + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterName, sqlType); + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterName, sqlType, scale); + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterName, sqlType, typeName); + } + + @Override + public URL getURL(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getURL(parameterIndex); + } + + @Override + public void setURL(String parameterName, URL val) throws SQLException { + checkOpen(); + this.delegate().setURL(parameterName, val); + } + + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + checkOpen(); + this.delegate().setNull(parameterName, sqlType); + } + + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + checkOpen(); + this.delegate().setBoolean(parameterName, x); + } + + @Override + public void setByte(String parameterName, byte x) throws SQLException { + checkOpen(); + this.delegate().setByte(parameterName, x); + } + + @Override + public void setShort(String parameterName, short x) throws SQLException { + checkOpen(); + this.delegate().setShort(parameterName, x); + } + + @Override + public void setInt(String parameterName, int x) throws SQLException { + checkOpen(); + this.delegate().setInt(parameterName, x); + } + + @Override + public void setLong(String parameterName, long x) throws SQLException { + checkOpen(); + this.delegate().setLong(parameterName, x); + } + + @Override + public void setFloat(String parameterName, float x) throws SQLException { + checkOpen(); + this.delegate().setFloat(parameterName, x); + } + + @Override + public void setDouble(String parameterName, double x) throws SQLException { + checkOpen(); + this.delegate().setDouble(parameterName, x); + } + + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + checkOpen(); + this.delegate().setBigDecimal(parameterName, x); + } + + @Override + public void setString(String parameterName, String x) throws SQLException { + checkOpen(); + this.delegate().setString(parameterName, x); + } + + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + checkOpen(); + this.delegate().setBytes(parameterName, x); + } + + @Override + public void setDate(String parameterName, Date x) throws SQLException { + checkOpen(); + this.delegate().setDate(parameterName, x); + } + + @Override + public void setTime(String parameterName, Time x) throws SQLException { + checkOpen(); + this.delegate().setTime(parameterName, x); + } + + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + checkOpen(); + this.delegate().setTimestamp(parameterName, x); + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().setAsciiStream(parameterName, x, length); + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().setBinaryStream(parameterName, x, length); + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterName, x, targetSqlType, scale); + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterName, x, targetSqlType); + } + + @Override + public void setObject(String parameterName, Object x) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterName, x); + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException { + checkOpen(); + this.delegate().setCharacterStream(parameterName, reader, length); + } + + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + checkOpen(); + this.delegate().setDate(parameterName, x, cal); + } + + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + checkOpen(); + this.delegate().setTime(parameterName, x, cal); + } + + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + checkOpen(); + this.delegate().setTimestamp(parameterName, x, cal); + } + + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + checkOpen(); + this.delegate().setNull(parameterName, sqlType, typeName); + } + + @Override + public String getString(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getString(parameterName); + } + + @Override + public boolean getBoolean(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getBoolean(parameterName); + } + + @Override + public byte getByte(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getByte(parameterName); + } + + @Override + public short getShort(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getShort(parameterName); + } + + @Override + public int getInt(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getInt(parameterName); + } + + @Override + public long getLong(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getLong(parameterName); + } + + @Override + public float getFloat(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getFloat(parameterName); + } + + @Override + public double getDouble(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getDouble(parameterName); + } + + @Override + public byte[] getBytes(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getBytes(parameterName); + } + + @Override + public Date getDate(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getDate(parameterName); + } + + @Override + public Time getTime(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getTime(parameterName); + } + + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(parameterName); + } + + @Override + public Object getObject(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getObject(parameterName); + } + + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(parameterName); + } + + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + checkOpen(); + return this.delegate().getObject(parameterName, map); + } + + @Override + public Ref getRef(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getRef(parameterName); + } + + @Override + public Blob getBlob(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getBlob(parameterName); + } + + @Override + public Clob getClob(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getClob(parameterName); + } + + @Override + public Array getArray(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getArray(parameterName); + } + + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getDate(parameterName, cal); + } + + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTime(parameterName, cal); + } + + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(parameterName, cal); + } + + @Override + public URL getURL(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getURL(parameterName); + } + + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getRowId(parameterIndex); + } + + @Override + public RowId getRowId(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getRowId(parameterName); + } + + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + checkOpen(); + this.delegate().setRowId(parameterName, x); + } + + @Override + public void setNString(String parameterName, String value) throws SQLException { + checkOpen(); + this.delegate().setNString(parameterName, value); + } + + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { + checkOpen(); + this.delegate().setNCharacterStream(parameterName, value, length); + } + + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + checkOpen(); + this.delegate().setNClob(parameterName, value); + } + + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().setClob(parameterName, reader, length); + } + + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { + checkOpen(); + this.delegate().setBlob(parameterName, inputStream, length); + } + + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().setNClob(parameterName, reader, length); + } + + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getNClob(parameterIndex); + } + + @Override + public NClob getNClob(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getNClob(parameterName); + } + + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + checkOpen(); + this.delegate().setSQLXML(parameterName, xmlObject); + } + + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getSQLXML(parameterIndex); + } + + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getSQLXML(parameterName); + } + + @Override + public String getNString(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getNString(parameterIndex); + } + + @Override + public String getNString(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getNString(parameterName); + } + + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getNCharacterStream(parameterIndex); + } + + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getNCharacterStream(parameterName); + } + + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + checkOpen(); + return this.delegate().getCharacterStream(parameterIndex); + } + + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + checkOpen(); + return this.delegate().getCharacterStream(parameterName); + } + + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + checkOpen(); + this.delegate().setBlob(parameterName, x); + } + + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + checkOpen(); + this.delegate().setClob(parameterName, x); + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().setAsciiStream(parameterName, x, length); + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().setBinaryStream(parameterName, x, length); + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().setCharacterStream(parameterName, reader, length); + } + + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + checkOpen(); + this.delegate().setAsciiStream(parameterName, x); + } + + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + checkOpen(); + this.delegate().setBinaryStream(parameterName, x); + } + + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + checkOpen(); + this.delegate().setCharacterStream(parameterName, reader); + } + + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + checkOpen(); + this.delegate().setNCharacterStream(parameterName, value); + } + + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + checkOpen(); + this.delegate().setClob(parameterName, reader); + } + + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + checkOpen(); + this.delegate().setBlob(parameterName, inputStream); + } + + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + checkOpen(); + this.delegate().setNClob(parameterName, reader); + } + + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + checkOpen(); + return this.delegate().getObject(parameterIndex, type); + } + + @Override + public T getObject(String parameterName, Class type) throws SQLException { + checkOpen(); + return this.delegate().getObject(parameterName, type); + } + + @Override + public void setObject(String parameterName, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterName, x, targetSqlType, scaleOrLength); + } + + @Override + public void setObject(String parameterName, Object x, SQLType targetSqlType) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterName, x, targetSqlType); + } + + @Override + public void registerOutParameter(int parameterIndex, SQLType sqlType) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterIndex, sqlType); + } + + @Override + public void registerOutParameter(int parameterIndex, SQLType sqlType, int scale) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterIndex, sqlType, scale); + } + + @Override + public void registerOutParameter(int parameterIndex, SQLType sqlType, String typeName) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterIndex, sqlType, typeName); + } + + @Override + public void registerOutParameter(String parameterName, SQLType sqlType) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterName, sqlType); + } + + @Override + public void registerOutParameter(String parameterName, SQLType sqlType, int scale) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterName, sqlType, scale); + } + + @Override + public void registerOutParameter(String parameterName, SQLType sqlType, String typeName) throws SQLException { + checkOpen(); + this.delegate().registerOutParameter(parameterName, sqlType, typeName); + } + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingConnection.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingConnection.java index 913f61d8631..acd0fc19eb1 100644 --- a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingConnection.java +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,334 +37,399 @@ import java.util.concurrent.Executor; /** - * A {@link Connection} that delegates to another {@link Connection}. + * A JDBC + * 4.3-compliant {@link Connection} that delegates to another JDBC 4.3-compliant {@link Connection}. */ public class DelegatingConnection implements Connection { private final Connection delegate; - protected DelegatingConnection(final Connection delegate) { + /** + * Creates a new {@link DelegatingConnection}. + * + * @param delegate the {@link Connection} to which all operations will be delegated; must not be {@code null} + * + * @exception NullPointerException if {@code delegate} is {@code null} + */ + public DelegatingConnection(Connection delegate) { super(); - this.delegate = Objects.requireNonNull(delegate); + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + /** + * Returns this {@link DelegatingConnection}'s underlying {@link Connection}. + * + *

This method never returns {@code null}.

+ * + * @return this {@link DelegatingConnection}'s underlying {@link Connection}; never {@code null} + */ + public final Connection delegate() { + return this.delegate; } @Override public Statement createStatement() throws SQLException { - return this.delegate.createStatement(); + return + new DelegatingStatement<>(this, // NOTE + this.delegate().createStatement(), + true, + true); } @Override - public PreparedStatement prepareStatement(final String sql) throws SQLException { - return this.delegate.prepareStatement(sql); + public PreparedStatement prepareStatement(String sql) throws SQLException { + return + new DelegatingPreparedStatement<>(this, // NOTE + this.delegate().prepareStatement(sql), + true, + true); } @Override - public CallableStatement prepareCall(final String sql) throws SQLException { - return this.delegate.prepareCall(sql); + public CallableStatement prepareCall(String sql) throws SQLException { + return new DelegatingCallableStatement(this, // NOTE + this.delegate().prepareCall(sql), + true, + true); } @Override - public String nativeSQL(final String sql) throws SQLException { - return this.delegate.nativeSQL(sql); + public String nativeSQL(String sql) throws SQLException { + return this.delegate().nativeSQL(sql); } @Override public void setAutoCommit(boolean autoCommit) throws SQLException { - this.delegate.setAutoCommit(autoCommit); + this.delegate().setAutoCommit(autoCommit); } @Override public boolean getAutoCommit() throws SQLException { - return this.delegate.getAutoCommit(); + return this.delegate().getAutoCommit(); } @Override public void commit() throws SQLException { - this.delegate.commit(); + this.delegate().commit(); } @Override public void rollback() throws SQLException { - this.delegate.rollback(); + this.delegate().rollback(); } @Override public void close() throws SQLException { - this.delegate.close(); + // (No need to check isClosed().) + this.delegate().close(); } @Override public boolean isClosed() throws SQLException { - return this.delegate.isClosed(); + return this.delegate().isClosed(); } @Override public DatabaseMetaData getMetaData() throws SQLException { - return this.delegate.getMetaData(); + return + new DelegatingDatabaseMetaData(this, // NOTE + this.delegate().getMetaData()); } @Override public void setReadOnly(boolean readOnly) throws SQLException { - this.delegate.setReadOnly(readOnly); + this.delegate().setReadOnly(readOnly); } @Override public boolean isReadOnly() throws SQLException { - return this.delegate.isReadOnly(); + return this.delegate().isReadOnly(); } @Override public void setCatalog(String catalog) throws SQLException { - this.delegate.setCatalog(catalog); + this.delegate().setCatalog(catalog); } @Override public String getCatalog() throws SQLException { - return this.delegate.getCatalog(); + return this.delegate().getCatalog(); } @Override - public void setTransactionIsolation(final int level) throws SQLException { - this.delegate.setTransactionIsolation(level); + public void setTransactionIsolation(int level) throws SQLException { + this.delegate().setTransactionIsolation(level); } @Override public int getTransactionIsolation() throws SQLException { - return this.delegate.getTransactionIsolation(); + return this.delegate().getTransactionIsolation(); } @Override public SQLWarning getWarnings() throws SQLException { - return this.delegate.getWarnings(); + return this.delegate().getWarnings(); } @Override public void clearWarnings() throws SQLException { - this.delegate.clearWarnings(); + this.delegate().clearWarnings(); } @Override - public Statement createStatement(final int resultSetType, final int resultSetConcurrency) throws SQLException { - return this.delegate.createStatement(resultSetType, resultSetConcurrency); + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return + new DelegatingStatement<>(this, // NOTE + this.delegate().createStatement(resultSetType, resultSetConcurrency), + true, + true); } @Override - public PreparedStatement prepareStatement(final String sql, - final int resultSetType, - final int resultSetConcurrency) - throws SQLException { - return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency); + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return + new DelegatingPreparedStatement<>(this, // NOTE + this.delegate().prepareStatement(sql, resultSetType, resultSetConcurrency), + true, + true); } @Override - public CallableStatement prepareCall(final String sql, - final int resultSetType, - final int resultSetConcurrency) - throws SQLException { - return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency); + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return + new DelegatingCallableStatement(this, // NOTE + this.delegate().prepareCall(sql, resultSetType, resultSetConcurrency), + true, + true); } @Override public Map> getTypeMap() throws SQLException { - return this.delegate.getTypeMap(); + return this.delegate().getTypeMap(); } @Override public void setTypeMap(Map> map) throws SQLException { - this.delegate.setTypeMap(map); + this.delegate().setTypeMap(map); } @Override - public void setHoldability(final int holdability) throws SQLException { - this.delegate.setHoldability(holdability); + public void setHoldability(int holdability) throws SQLException { + this.delegate().setHoldability(holdability); } @Override public int getHoldability() throws SQLException { - return this.delegate.getHoldability(); + return this.delegate().getHoldability(); } @Override public Savepoint setSavepoint() throws SQLException { - return this.delegate.setSavepoint(); + return this.delegate().setSavepoint(); } @Override - public Savepoint setSavepoint(final String name) throws SQLException { - return this.delegate.setSavepoint(name); + public Savepoint setSavepoint(String name) throws SQLException { + return this.delegate().setSavepoint(name); } @Override - public void rollback(final Savepoint savepoint) throws SQLException { - this.delegate.rollback(savepoint); + public void rollback(Savepoint savepoint) throws SQLException { + this.delegate().rollback(savepoint); } @Override - public void releaseSavepoint(final Savepoint savepoint) throws SQLException { - this.delegate.releaseSavepoint(savepoint); + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + this.delegate().releaseSavepoint(savepoint); } @Override - public Statement createStatement(final int resultSetType, - final int resultSetConcurrency, - final int resultSetHoldability) - throws SQLException { - return this.delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + return + new DelegatingStatement<>(this, // NOTE + this.delegate().createStatement(resultSetType, + resultSetConcurrency, + resultSetHoldability), + true, + true); } @Override - public PreparedStatement prepareStatement(final String sql, final int resultSetType, - final int resultSetConcurrency, - final int resultSetHoldability) - throws SQLException { - return this.delegate.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return + new DelegatingPreparedStatement<>(this, // NOTE + this.delegate().prepareStatement(sql, + resultSetType, + resultSetConcurrency, + resultSetHoldability), + true, + true); } @Override - public CallableStatement prepareCall(final String sql, - final int resultSetType, - final int resultSetConcurrency, - final int resultSetHoldability) - throws SQLException { - return this.delegate.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + return + new DelegatingCallableStatement(this, // NOTE + this.delegate().prepareCall(sql, + resultSetType, + resultSetConcurrency, + resultSetHoldability), + true, + true); } @Override - public PreparedStatement prepareStatement(final String sql, final int autoGeneratedKeys) throws SQLException { - return this.delegate.prepareStatement(sql, autoGeneratedKeys); + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + return + new DelegatingPreparedStatement<>(this, // NOTE + this.delegate().prepareStatement(sql, autoGeneratedKeys), + true, + true); } @Override - public PreparedStatement prepareStatement(final String sql, final int[] columnIndexes) throws SQLException { - return this.delegate.prepareStatement(sql, columnIndexes); + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + return + new DelegatingPreparedStatement<>(this, // NOTE + this.delegate().prepareStatement(sql, columnIndexes), + true, + true); } @Override - public PreparedStatement prepareStatement(final String sql, final String[] columnNames) throws SQLException { - return this.delegate.prepareStatement(sql, columnNames); + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + return + new DelegatingPreparedStatement<>(this, // NOTE + this.delegate().prepareStatement(sql, columnNames), + true, + true); } @Override public Clob createClob() throws SQLException { - return this.delegate.createClob(); + return this.delegate().createClob(); } @Override public Blob createBlob() throws SQLException { - return this.delegate.createBlob(); + return this.delegate().createBlob(); } @Override public NClob createNClob() throws SQLException { - return this.delegate.createNClob(); + return this.delegate().createNClob(); } @Override public SQLXML createSQLXML() throws SQLException { - return this.delegate.createSQLXML(); + return this.delegate().createSQLXML(); } @Override - public boolean isValid(final int timeout) throws SQLException { - return this.delegate.isValid(timeout); + public boolean isValid(int timeout) throws SQLException { + // (No need to check isClosed().) + return this.delegate().isValid(timeout); } @Override - public void setClientInfo(final String name, final String value) throws SQLClientInfoException { - this.delegate.setClientInfo(name, value); + public void setClientInfo(String name, String value) throws SQLClientInfoException { + this.delegate().setClientInfo(name, value); } @Override - public void setClientInfo(final Properties properties) throws SQLClientInfoException { - this.delegate.setClientInfo(properties); + public void setClientInfo(Properties properties) throws SQLClientInfoException { + this.delegate().setClientInfo(properties); } @Override - public String getClientInfo(final String name) throws SQLException { - return this.delegate.getClientInfo(name); + public String getClientInfo(String name) throws SQLException { + return this.delegate().getClientInfo(name); } @Override public Properties getClientInfo() throws SQLException { - return this.delegate.getClientInfo(); + return this.delegate().getClientInfo(); } @Override - public Array createArrayOf(final String typeName, final Object[] elements) throws SQLException { - return this.delegate.createArrayOf(typeName, elements); + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + return this.delegate().createArrayOf(typeName, elements); } @Override - public Struct createStruct(final String typeName, final Object[] attributes) throws SQLException { - return this.delegate.createStruct(typeName, attributes); + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + return this.delegate().createStruct(typeName, attributes); } @Override - public void setSchema(final String schema) throws SQLException { - this.delegate.setSchema(schema); + public void setSchema(String schema) throws SQLException { + this.delegate().setSchema(schema); } @Override public String getSchema() throws SQLException { - return this.delegate.getSchema(); + return this.delegate().getSchema(); } @Override - public void abort(final Executor executor) throws SQLException { - this.delegate.abort(executor); + public void abort(Executor executor) throws SQLException { + // (No need to check isClosed().) + this.delegate().abort(executor); } @Override - public void setNetworkTimeout(final Executor executor, final int milliseconds) throws SQLException { - this.delegate.setNetworkTimeout(executor, milliseconds); + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + this.delegate().setNetworkTimeout(executor, milliseconds); } @Override public int getNetworkTimeout() throws SQLException { - return this.delegate.getNetworkTimeout(); + return this.delegate().getNetworkTimeout(); } @Override public void beginRequest() throws SQLException { - this.delegate.beginRequest(); + // (No need to check isClosed().) + this.delegate().beginRequest(); } @Override public void endRequest() throws SQLException { - this.delegate.endRequest(); + // (No need to check isClosed().) + this.delegate().endRequest(); } @Override - public boolean setShardingKeyIfValid(final ShardingKey shardingKey, - final ShardingKey superShardingKey, - final int timeout) - throws SQLException { - return this.delegate.setShardingKeyIfValid(shardingKey, superShardingKey, timeout); + public boolean setShardingKeyIfValid(ShardingKey shardingKey, ShardingKey superShardingKey, int timeout) + throws SQLException { + return this.delegate().setShardingKeyIfValid(shardingKey, superShardingKey, timeout); } @Override - public boolean setShardingKeyIfValid(final ShardingKey shardingKey, final int timeout) throws SQLException { - return this.delegate.setShardingKeyIfValid(shardingKey, timeout); + public boolean setShardingKeyIfValid(ShardingKey shardingKey, int timeout) throws SQLException { + return this.delegate().setShardingKeyIfValid(shardingKey, timeout); } @Override - public void setShardingKey(final ShardingKey shardingKey, final ShardingKey superShardingKey) throws SQLException { - this.delegate.setShardingKey(shardingKey, superShardingKey); + public void setShardingKey(ShardingKey shardingKey, ShardingKey superShardingKey) throws SQLException { + this.delegate().setShardingKey(shardingKey, superShardingKey); } @Override - public void setShardingKey(final ShardingKey shardingKey) throws SQLException { - this.delegate.setShardingKey(shardingKey); + public void setShardingKey(ShardingKey shardingKey) throws SQLException { + this.delegate().setShardingKey(shardingKey); } @Override - public T unwrap(final Class iface) throws SQLException { - return this.delegate.unwrap(iface); + public T unwrap(Class iface) throws SQLException { + return iface.isInstance(this) ? iface.cast(this) : this.delegate().unwrap(iface); } @Override - public boolean isWrapperFor(final Class iface) throws SQLException { - return this.delegate.isWrapperFor(iface); + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this) || this.delegate().isWrapperFor(iface); } } diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingDatabaseMetaData.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingDatabaseMetaData.java new file mode 100644 index 00000000000..8d6b4aeb329 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingDatabaseMetaData.java @@ -0,0 +1,968 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; +import java.util.Objects; + +/** + * A JDBC + * 4.3-compliant {@link DatabaseMetaData} that delegates to another JDBC 4.3-compliant {@link DatabaseMetaData}. + */ +public class DelegatingDatabaseMetaData implements DatabaseMetaData { + + private final Connection connection; + + private final DatabaseMetaData delegate; + + /** + * Creates a new {@link DelegatingDatabaseMetaData}. + * + * @param connection the {@link Connection} that will be returned by the {@link #getConnection()} method; must not + * be {@code null} + * + * @param delegate the {@link DatabaseMetaData} to which all operations will be delegated; must not be {@code null} + * + * @exception NullPointerException if either {@code connection} or {@code delegate} is {@code null} + */ + protected DelegatingDatabaseMetaData(Connection connection, DatabaseMetaData delegate) { + super(); + this.connection = Objects.requireNonNull(connection, "connection"); + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public boolean allProceduresAreCallable() throws SQLException { + return this.delegate.allProceduresAreCallable(); + } + + @Override + public boolean allTablesAreSelectable() throws SQLException { + return this.delegate.allTablesAreSelectable(); + } + + @Override + public String getURL() throws SQLException { + return this.delegate.getURL(); + } + + @Override + public String getUserName() throws SQLException { + return this.delegate.getUserName(); + } + + @Override + public boolean isReadOnly() throws SQLException { + return this.delegate.isReadOnly(); + } + + @Override + public boolean nullsAreSortedHigh() throws SQLException { + return this.delegate.nullsAreSortedHigh(); + } + + @Override + public boolean nullsAreSortedLow() throws SQLException { + return this.delegate.nullsAreSortedLow(); + } + + @Override + public boolean nullsAreSortedAtStart() throws SQLException { + return this.delegate.nullsAreSortedAtStart(); + } + + @Override + public boolean nullsAreSortedAtEnd() throws SQLException { + return this.delegate.nullsAreSortedAtEnd(); + } + + @Override + public String getDatabaseProductName() throws SQLException { + return this.delegate.getDatabaseProductName(); + } + + @Override + public String getDatabaseProductVersion() throws SQLException { + return this.delegate.getDatabaseProductVersion(); + } + + @Override + public String getDriverName() throws SQLException { + return this.delegate.getDriverName(); + } + + @Override + public String getDriverVersion() throws SQLException { + return this.delegate.getDriverVersion(); + } + + @Override + public int getDriverMajorVersion() { + return this.delegate.getDriverMajorVersion(); + } + + @Override + public int getDriverMinorVersion() { + return this.delegate.getDriverMinorVersion(); + } + + @Override + public boolean usesLocalFiles() throws SQLException { + return this.delegate.usesLocalFiles(); + } + + @Override + public boolean usesLocalFilePerTable() throws SQLException { + return this.delegate.usesLocalFilePerTable(); + } + + @Override + public boolean supportsMixedCaseIdentifiers() throws SQLException { + return this.delegate.supportsMixedCaseIdentifiers(); + } + + @Override + public boolean storesUpperCaseIdentifiers() throws SQLException { + return this.delegate.storesUpperCaseIdentifiers(); + } + + @Override + public boolean storesLowerCaseIdentifiers() throws SQLException { + return this.delegate.storesLowerCaseIdentifiers(); + } + + @Override + public boolean storesMixedCaseIdentifiers() throws SQLException { + return this.delegate.storesMixedCaseIdentifiers(); + } + + @Override + public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + return this.delegate.supportsMixedCaseQuotedIdentifiers(); + } + + @Override + public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + return this.delegate.storesUpperCaseQuotedIdentifiers(); + } + + @Override + public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + return this.delegate.storesLowerCaseQuotedIdentifiers(); + } + + @Override + public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + return this.delegate.storesMixedCaseQuotedIdentifiers(); + } + + @Override + public String getIdentifierQuoteString() throws SQLException { + return this.delegate.getIdentifierQuoteString(); + } + + @Override + public String getSQLKeywords() throws SQLException { + return this.delegate.getSQLKeywords(); + } + + @Override + public String getNumericFunctions() throws SQLException { + return this.delegate.getNumericFunctions(); + } + + @Override + public String getStringFunctions() throws SQLException { + return this.delegate.getStringFunctions(); + } + + @Override + public String getSystemFunctions() throws SQLException { + return this.delegate.getSystemFunctions(); + } + + @Override + public String getTimeDateFunctions() throws SQLException { + return this.delegate.getTimeDateFunctions(); + } + + @Override + public String getSearchStringEscape() throws SQLException { + return this.delegate.getSearchStringEscape(); + } + + @Override + public String getExtraNameCharacters() throws SQLException { + return this.delegate.getExtraNameCharacters(); + } + + @Override + public boolean supportsAlterTableWithAddColumn() throws SQLException { + return this.delegate.supportsAlterTableWithAddColumn(); + } + + @Override + public boolean supportsAlterTableWithDropColumn() throws SQLException { + return this.delegate.supportsAlterTableWithDropColumn(); + } + + @Override + public boolean supportsColumnAliasing() throws SQLException { + return this.delegate.supportsColumnAliasing(); + } + + @Override + public boolean nullPlusNonNullIsNull() throws SQLException { + return this.delegate.nullPlusNonNullIsNull(); + } + + @Override + public boolean supportsConvert() throws SQLException { + return this.delegate.supportsConvert(); + } + + @Override + public boolean supportsConvert(int fromType, int toType) throws SQLException { + return this.delegate.supportsConvert(fromType, toType); + } + + @Override + public boolean supportsTableCorrelationNames() throws SQLException { + return this.delegate.supportsTableCorrelationNames(); + } + + @Override + public boolean supportsDifferentTableCorrelationNames() throws SQLException { + return this.delegate.supportsDifferentTableCorrelationNames(); + } + + @Override + public boolean supportsExpressionsInOrderBy() throws SQLException { + return this.delegate.supportsExpressionsInOrderBy(); + } + + @Override + public boolean supportsOrderByUnrelated() throws SQLException { + return this.delegate.supportsOrderByUnrelated(); + } + + @Override + public boolean supportsGroupBy() throws SQLException { + return this.delegate.supportsGroupBy(); + } + + @Override + public boolean supportsGroupByUnrelated() throws SQLException { + return this.delegate.supportsGroupByUnrelated(); + } + + @Override + public boolean supportsGroupByBeyondSelect() throws SQLException { + return this.delegate.supportsGroupByBeyondSelect(); + } + + @Override + public boolean supportsLikeEscapeClause() throws SQLException { + return this.delegate.supportsLikeEscapeClause(); + } + + @Override + public boolean supportsMultipleResultSets() throws SQLException { + return this.delegate.supportsMultipleResultSets(); + } + + @Override + public boolean supportsMultipleTransactions() throws SQLException { + return this.delegate.supportsMultipleTransactions(); + } + + @Override + public boolean supportsNonNullableColumns() throws SQLException { + return this.delegate.supportsNonNullableColumns(); + } + + @Override + public boolean supportsMinimumSQLGrammar() throws SQLException { + return this.delegate.supportsMinimumSQLGrammar(); + } + + @Override + public boolean supportsCoreSQLGrammar() throws SQLException { + return this.delegate.supportsCoreSQLGrammar(); + } + + @Override + public boolean supportsExtendedSQLGrammar() throws SQLException { + return this.delegate.supportsExtendedSQLGrammar(); + } + + @Override + public boolean supportsANSI92EntryLevelSQL() throws SQLException { + return this.delegate.supportsANSI92EntryLevelSQL(); + } + + @Override + public boolean supportsANSI92IntermediateSQL() throws SQLException { + return this.delegate.supportsANSI92IntermediateSQL(); + } + + @Override + public boolean supportsANSI92FullSQL() throws SQLException { + return this.delegate.supportsANSI92FullSQL(); + } + + @Override + public boolean supportsIntegrityEnhancementFacility() throws SQLException { + return this.delegate.supportsIntegrityEnhancementFacility(); + } + + @Override + public boolean supportsOuterJoins() throws SQLException { + return this.delegate.supportsOuterJoins(); + } + + @Override + public boolean supportsFullOuterJoins() throws SQLException { + return this.delegate.supportsFullOuterJoins(); + } + + @Override + public boolean supportsLimitedOuterJoins() throws SQLException { + return this.delegate.supportsLimitedOuterJoins(); + } + + @Override + public String getSchemaTerm() throws SQLException { + return this.delegate.getSchemaTerm(); + } + + @Override + public String getProcedureTerm() throws SQLException { + return this.delegate.getProcedureTerm(); + } + + @Override + public String getCatalogTerm() throws SQLException { + return this.delegate.getCatalogTerm(); + } + + @Override + public boolean isCatalogAtStart() throws SQLException { + return this.delegate.isCatalogAtStart(); + } + + @Override + public String getCatalogSeparator() throws SQLException { + return this.delegate.getCatalogSeparator(); + } + + @Override + public boolean supportsSchemasInDataManipulation() throws SQLException { + return this.delegate.supportsSchemasInDataManipulation(); + } + + @Override + public boolean supportsSchemasInProcedureCalls() throws SQLException { + return this.delegate.supportsSchemasInProcedureCalls(); + } + + @Override + public boolean supportsSchemasInTableDefinitions() throws SQLException { + return this.delegate.supportsSchemasInTableDefinitions(); + } + + @Override + public boolean supportsSchemasInIndexDefinitions() throws SQLException { + return this.delegate.supportsSchemasInIndexDefinitions(); + } + + @Override + public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { + return this.delegate.supportsSchemasInPrivilegeDefinitions(); + } + + @Override + public boolean supportsCatalogsInDataManipulation() throws SQLException { + return this.delegate.supportsCatalogsInDataManipulation(); + } + + @Override + public boolean supportsCatalogsInProcedureCalls() throws SQLException { + return this.delegate.supportsCatalogsInProcedureCalls(); + } + + @Override + public boolean supportsCatalogsInTableDefinitions() throws SQLException { + return this.delegate.supportsCatalogsInTableDefinitions(); + } + + @Override + public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + return this.delegate.supportsCatalogsInIndexDefinitions(); + } + + @Override + public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + return this.delegate.supportsCatalogsInPrivilegeDefinitions(); + } + + @Override + public boolean supportsPositionedDelete() throws SQLException { + return this.delegate.supportsPositionedDelete(); + } + + @Override + public boolean supportsPositionedUpdate() throws SQLException { + return this.delegate.supportsPositionedUpdate(); + } + + @Override + public boolean supportsSelectForUpdate() throws SQLException { + return this.delegate.supportsSelectForUpdate(); + } + + @Override + public boolean supportsStoredProcedures() throws SQLException { + return this.delegate.supportsStoredProcedures(); + } + + @Override + public boolean supportsSubqueriesInComparisons() throws SQLException { + return this.delegate.supportsSubqueriesInComparisons(); + } + + @Override + public boolean supportsSubqueriesInExists() throws SQLException { + return this.delegate.supportsSubqueriesInExists(); + } + + @Override + public boolean supportsSubqueriesInIns() throws SQLException { + return this.delegate.supportsSubqueriesInIns(); + } + + @Override + public boolean supportsSubqueriesInQuantifieds() throws SQLException { + return this.delegate.supportsSubqueriesInQuantifieds(); + } + + @Override + public boolean supportsCorrelatedSubqueries() throws SQLException { + return this.delegate.supportsCorrelatedSubqueries(); + } + + @Override + public boolean supportsUnion() throws SQLException { + return this.delegate.supportsUnion(); + } + + @Override + public boolean supportsUnionAll() throws SQLException { + return this.delegate.supportsUnionAll(); + } + + @Override + public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + return this.delegate.supportsOpenCursorsAcrossCommit(); + } + + @Override + public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + return this.delegate.supportsOpenCursorsAcrossRollback(); + } + + @Override + public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + return this.delegate.supportsOpenStatementsAcrossCommit(); + } + + @Override + public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + return this.delegate.supportsOpenStatementsAcrossRollback(); + } + + @Override + public int getMaxBinaryLiteralLength() throws SQLException { + return this.delegate.getMaxBinaryLiteralLength(); + } + + @Override + public int getMaxCharLiteralLength() throws SQLException { + return this.delegate.getMaxCharLiteralLength(); + } + + @Override + public int getMaxColumnNameLength() throws SQLException { + return this.delegate.getMaxColumnNameLength(); + } + + @Override + public int getMaxColumnsInGroupBy() throws SQLException { + return this.delegate.getMaxColumnsInGroupBy(); + } + + @Override + public int getMaxColumnsInIndex() throws SQLException { + return this.delegate.getMaxColumnsInIndex(); + } + + @Override + public int getMaxColumnsInOrderBy() throws SQLException { + return this.delegate.getMaxColumnsInOrderBy(); + } + + @Override + public int getMaxColumnsInSelect() throws SQLException { + return this.delegate.getMaxColumnsInSelect(); + } + + @Override + public int getMaxColumnsInTable() throws SQLException { + return this.delegate.getMaxColumnsInTable(); + } + + @Override + public int getMaxConnections() throws SQLException { + return this.delegate.getMaxConnections(); + } + + @Override + public int getMaxCursorNameLength() throws SQLException { + return this.delegate.getMaxCursorNameLength(); + } + + @Override + public int getMaxIndexLength() throws SQLException { + return this.delegate.getMaxIndexLength(); + } + + @Override + public int getMaxSchemaNameLength() throws SQLException { + return this.delegate.getMaxSchemaNameLength(); + } + + @Override + public int getMaxProcedureNameLength() throws SQLException { + return this.delegate.getMaxProcedureNameLength(); + } + + @Override + public int getMaxCatalogNameLength() throws SQLException { + return this.delegate.getMaxCatalogNameLength(); + } + + @Override + public int getMaxRowSize() throws SQLException { + return this.delegate.getMaxRowSize(); + } + + @Override + public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { + return this.delegate.doesMaxRowSizeIncludeBlobs(); + } + + @Override + public int getMaxStatementLength() throws SQLException { + return this.delegate.getMaxStatementLength(); + } + + @Override + public int getMaxStatements() throws SQLException { + return this.delegate.getMaxStatements(); + } + + @Override + public int getMaxTableNameLength() throws SQLException { + return this.delegate.getMaxTableNameLength(); + } + + @Override + public int getMaxTablesInSelect() throws SQLException { + return this.delegate.getMaxTablesInSelect(); + } + + @Override + public int getMaxUserNameLength() throws SQLException { + return this.delegate.getMaxUserNameLength(); + } + + @Override + public int getDefaultTransactionIsolation() throws SQLException { + return this.delegate.getDefaultTransactionIsolation(); + } + + @Override + public boolean supportsTransactions() throws SQLException { + return this.delegate.supportsTransactions(); + } + + @Override + public boolean supportsTransactionIsolationLevel(int level) throws SQLException { + return this.delegate.supportsTransactionIsolationLevel(level); + } + + @Override + public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + return this.delegate.supportsDataDefinitionAndDataManipulationTransactions(); + } + + @Override + public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + return this.delegate.supportsDataManipulationTransactionsOnly(); + } + + @Override + public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + return this.delegate.dataDefinitionCausesTransactionCommit(); + } + + @Override + public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + return this.delegate.dataDefinitionIgnoredInTransactions(); + } + + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { + return this.delegate.getProcedures(catalog, schemaPattern, procedureNamePattern); + } + + @Override + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedurePattern, String columnNamePattern) + throws SQLException { + return this.delegate.getProcedureColumns(catalog, schemaPattern, procedurePattern, columnNamePattern); + } + + @Override + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) + throws SQLException { + return this.delegate.getTables(catalog, schemaPattern, tableNamePattern, types); + } + + @Override + public ResultSet getSchemas() throws SQLException { + return this.delegate.getSchemas(); + } + + @Override + public ResultSet getCatalogs() throws SQLException { + return this.delegate.getCatalogs(); + } + + @Override + public ResultSet getTableTypes() throws SQLException { + return this.delegate.getTableTypes(); + } + + @Override + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + return this.delegate.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + } + + @Override + public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) + throws SQLException { + return this.delegate.getColumnPrivileges(catalog, schema, table, columnNamePattern); + } + + @Override + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return this.delegate.getTablePrivileges(catalog, schemaPattern, tableNamePattern); + } + + @Override + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) + throws SQLException { + return this.delegate.getBestRowIdentifier(catalog, schema, table, scope, nullable); + } + + @Override + public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { + return this.delegate.getVersionColumns(catalog, schema, table); + } + + @Override + public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + return this.delegate.getPrimaryKeys(catalog, schema, table); + } + + @Override + public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { + return this.delegate.getImportedKeys(catalog, schema, table); + } + + @Override + public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { + return this.delegate.getExportedKeys(catalog, schema, table); + } + + @Override + public ResultSet getCrossReference(String parentCatalog, + String parentSchema, + String parentTable, + String foreignCatalog, + String foreignSchema, + String foreignTable) + throws SQLException { + return + this.delegate.getCrossReference(parentCatalog, + parentSchema, + parentTable, + foreignCatalog, + foreignSchema, + foreignTable); + } + + @Override + public ResultSet getTypeInfo() throws SQLException { + return this.delegate.getTypeInfo(); + } + + @Override + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) + throws SQLException { + return this.delegate.getIndexInfo(catalog, schema, table, unique, approximate); + } + + @Override + public boolean supportsResultSetType(int type) throws SQLException { + return this.delegate.supportsResultSetType(type); + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { + return this.delegate.supportsResultSetConcurrency(type, concurrency); + } + + @Override + public boolean ownUpdatesAreVisible(int type) throws SQLException { + return this.delegate.ownUpdatesAreVisible(type); + } + + @Override + public boolean ownDeletesAreVisible(int type) throws SQLException { + return this.delegate.ownDeletesAreVisible(type); + } + + @Override + public boolean ownInsertsAreVisible(int type) throws SQLException { + return this.delegate.ownInsertsAreVisible(type); + } + + @Override + public boolean othersUpdatesAreVisible(int type) throws SQLException { + return this.delegate.othersUpdatesAreVisible(type); + } + + @Override + public boolean othersDeletesAreVisible(int type) throws SQLException { + return this.delegate.othersDeletesAreVisible(type); + } + + @Override + public boolean othersInsertsAreVisible(int type) throws SQLException { + return this.delegate.othersInsertsAreVisible(type); + } + + @Override + public boolean updatesAreDetected(int type) throws SQLException { + return this.delegate.updatesAreDetected(type); + } + + @Override + public boolean deletesAreDetected(int type) throws SQLException { + return this.delegate.deletesAreDetected(type); + } + + @Override + public boolean insertsAreDetected(int type) throws SQLException { + return this.delegate.insertsAreDetected(type); + } + + @Override + public boolean supportsBatchUpdates() throws SQLException { + return this.delegate.supportsBatchUpdates(); + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + return this.delegate.getUDTs(catalog, schemaPattern, typeNamePattern, types); + } + + @Override + public Connection getConnection() throws SQLException { + // NOTE + return this.connection; + } + + @Override + public boolean supportsSavepoints() throws SQLException { + return this.delegate.supportsSavepoints(); + } + + @Override + public boolean supportsNamedParameters() throws SQLException { + return this.delegate.supportsNamedParameters(); + } + + @Override + public boolean supportsMultipleOpenResults() throws SQLException { + return this.delegate.supportsMultipleOpenResults(); + } + + @Override + public boolean supportsGetGeneratedKeys() throws SQLException { + return this.delegate.supportsGetGeneratedKeys(); + } + + @Override + public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { + return this.delegate.getSuperTypes(catalog, schemaPattern, typeNamePattern); + } + + @Override + public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + return this.delegate.getSuperTables(catalog, schemaPattern, tableNamePattern); + } + + @Override + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) + throws SQLException { + return this.delegate.getAttributes(catalog, schemaPattern, typeNamePattern, attributeNamePattern); + } + + @Override + public boolean supportsResultSetHoldability(int holdability) throws SQLException { + return this.delegate.supportsResultSetHoldability(holdability); + } + + @Override + public int getResultSetHoldability() throws SQLException { + return this.delegate.getResultSetHoldability(); + } + + @Override + public int getDatabaseMajorVersion() throws SQLException { + return this.delegate.getDatabaseMajorVersion(); + } + + @Override + public int getDatabaseMinorVersion() throws SQLException { + return this.delegate.getDatabaseMinorVersion(); + } + + @Override + public int getJDBCMajorVersion() throws SQLException { + return this.delegate.getJDBCMajorVersion(); + } + + @Override + public int getJDBCMinorVersion() throws SQLException { + return this.delegate.getJDBCMinorVersion(); + } + + @Override + public int getSQLStateType() throws SQLException { + return this.delegate.getSQLStateType(); + } + + @Override + public boolean locatorsUpdateCopy() throws SQLException { + return this.delegate.locatorsUpdateCopy(); + } + + @Override + public boolean supportsStatementPooling() throws SQLException { + return this.delegate.supportsStatementPooling(); + } + + @Override + public RowIdLifetime getRowIdLifetime() throws SQLException { + return this.delegate.getRowIdLifetime(); + } + + @Override + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return this.delegate.getSchemas(catalog, schemaPattern); + } + + @Override + public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + return this.delegate.supportsStoredFunctionsUsingCallSyntax(); + } + + @Override + public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + return this.delegate.autoCommitFailureClosesAllResultSets(); + } + + @Override + public ResultSet getClientInfoProperties() throws SQLException { + return this.delegate.getClientInfoProperties(); + } + + @Override + public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { + return this.delegate.getFunctions(catalog, schemaPattern, functionNamePattern); + } + + @Override + public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionPattern, String columnNamePattern) + throws SQLException { + return this.delegate.getFunctionColumns(catalog, schemaPattern, functionPattern, columnNamePattern); + } + + @Override + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) + throws SQLException { + return this.delegate.getPseudoColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + } + + @Override + public boolean generatedKeyAlwaysReturned() throws SQLException { + return this.delegate.generatedKeyAlwaysReturned(); + } + + @Override + public long getMaxLogicalLobSize() throws SQLException { + return this.delegate.getMaxLogicalLobSize(); + } + + @Override + public boolean supportsRefCursors() throws SQLException { + return this.delegate.supportsRefCursors(); + } + + @Override + public boolean supportsSharding() throws SQLException { + return this.delegate.supportsSharding(); + } + + @Override + public T unwrap(Class iface) throws SQLException { + return iface.isInstance(this) ? iface.cast(this) : this.delegate.unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return iface.isInstance(this) || this.delegate.isWrapperFor(iface); + } + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingPreparedStatement.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingPreparedStatement.java new file mode 100644 index 00000000000..4a34512fc49 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingPreparedStatement.java @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; + +/** + * A JDBC + * 4.3-compliant {@link PreparedStatement} that delegates to another JDBC 4.3-compliant {@link PreparedStatement}. + * + * @param the type of the {@link PreparedStatement} subclass + */ +public class DelegatingPreparedStatement extends DelegatingStatement implements PreparedStatement { + + /** + * Creates a new {@link DelegatingPreparedStatement}. + * + * @param connection the {@link Connection} that created this {@link DelegatingPreparedStatement}; must not be + * {@code null} + * + * @param delegate the {@link PreparedStatement} instance to which all operations will be delegated; must not be + * {@code null} + * + * @param closeable the initial value for this {@link DelegatingPreparedStatement}'s {@linkplain #isCloseable() + * closeable} status + * + * @param strictClosedChecking if {@code true}, then this {@link DelegatingPreparedStatement}'s {@link + * #isClosed()} method will be invoked before every operation that cannot take place on a closed statement, and, if + * it returns {@code true}, the operation in question will fail with a {@link SQLException} + * + * @exception NullPointerException if either argument is {@code + * null} + * + * @see DelegatingStatement#isCloseable() + * + * @see DelegatingStatement#setCloseable(boolean) + * + * @see DelegatingStatement#close() + * + * @see DelegatingStatement#isClosed() + */ + public DelegatingPreparedStatement(Connection connection, + S delegate, + boolean closeable, + boolean strictClosedChecking) { + super(connection, delegate, closeable, strictClosedChecking); + } + + @Override + public ResultSet executeQuery() throws SQLException { + checkOpen(); + return + new DelegatingResultSet(this, // NOTE + this.delegate().executeQuery(), + true, + true); + } + + @Override + public int executeUpdate() throws SQLException { + checkOpen(); + return this.delegate().executeUpdate(); + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + checkOpen(); + this.delegate().setNull(parameterIndex, sqlType); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + checkOpen(); + this.delegate().setBoolean(parameterIndex, x); + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + checkOpen(); + this.delegate().setByte(parameterIndex, x); + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + checkOpen(); + this.delegate().setShort(parameterIndex, x); + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + checkOpen(); + this.delegate().setInt(parameterIndex, x); + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + checkOpen(); + this.delegate().setLong(parameterIndex, x); + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + checkOpen(); + this.delegate().setFloat(parameterIndex, x); + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + checkOpen(); + this.delegate().setDouble(parameterIndex, x); + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + checkOpen(); + this.delegate().setBigDecimal(parameterIndex, x); + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + checkOpen(); + this.delegate().setString(parameterIndex, x); + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + checkOpen(); + this.delegate().setBytes(parameterIndex, x); + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + checkOpen(); + this.delegate().setDate(parameterIndex, x); + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + checkOpen(); + this.delegate().setTime(parameterIndex, x); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + checkOpen(); + this.delegate().setTimestamp(parameterIndex, x); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().setAsciiStream(parameterIndex, x, length); + } + + @Deprecated + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().setUnicodeStream(parameterIndex, x, length); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().setBinaryStream(parameterIndex, x, length); + } + + @Override + public void clearParameters() throws SQLException { + checkOpen(); + this.delegate().clearParameters(); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterIndex, x, targetSqlType); + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterIndex, x); + } + + @Override + public boolean execute() throws SQLException { + checkOpen(); + return this.delegate().execute(); + } + + @Override + public void addBatch() throws SQLException { + checkOpen(); + this.delegate().addBatch(); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + checkOpen(); + this.delegate().setCharacterStream(parameterIndex, reader, length); + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + checkOpen(); + this.delegate().setRef(parameterIndex, x); + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + checkOpen(); + this.delegate().setBlob(parameterIndex, x); + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + checkOpen(); + this.delegate().setClob(parameterIndex, x); + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + checkOpen(); + this.delegate().setArray(parameterIndex, x); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + checkOpen(); + return this.delegate().getMetaData(); + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + checkOpen(); + this.delegate().setDate(parameterIndex, x, cal); + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + checkOpen(); + this.delegate().setTime(parameterIndex, x, cal); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + checkOpen(); + this.delegate().setTimestamp(parameterIndex, x, cal); + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + checkOpen(); + this.delegate().setNull(parameterIndex, sqlType, typeName); + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + checkOpen(); + this.delegate().setURL(parameterIndex, x); + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + checkOpen(); + return this.delegate().getParameterMetaData(); + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + checkOpen(); + this.delegate().setRowId(parameterIndex, x); + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + checkOpen(); + this.delegate().setNString(parameterIndex, value); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + checkOpen(); + this.delegate().setNCharacterStream(parameterIndex, value, length); + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + checkOpen(); + this.delegate().setNClob(parameterIndex, value); + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().setClob(parameterIndex, reader, length); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + checkOpen(); + this.delegate().setBlob(parameterIndex, inputStream, length); + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().setNClob(parameterIndex, reader, length); + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + checkOpen(); + this.delegate().setSQLXML(parameterIndex, xmlObject); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().setAsciiStream(parameterIndex, x, length); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().setBinaryStream(parameterIndex, x, length); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().setCharacterStream(parameterIndex, reader, length); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + checkOpen(); + this.delegate().setAsciiStream(parameterIndex, x); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + checkOpen(); + this.delegate().setBinaryStream(parameterIndex, x); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + checkOpen(); + this.delegate().setCharacterStream(parameterIndex, reader); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + checkOpen(); + this.delegate().setNCharacterStream(parameterIndex, value); + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + checkOpen(); + this.delegate().setClob(parameterIndex, reader); + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + checkOpen(); + this.delegate().setBlob(parameterIndex, inputStream); + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + checkOpen(); + this.delegate().setNClob(parameterIndex, reader); + } + + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } + + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException { + checkOpen(); + this.delegate().setObject(parameterIndex, x, targetSqlType); + } + + @Override + public long executeLargeUpdate() throws SQLException { + checkOpen(); + return this.delegate().executeLargeUpdate(); + } + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingResultSet.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingResultSet.java new file mode 100644 index 00000000000..a4ebbd3b3e9 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingResultSet.java @@ -0,0 +1,1349 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLType; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; +import java.util.Objects; + +/** + * A JDBC + * 4.3-compliant {@link ResultSet} that delegates to another JDBC 4.3-compliant {@link ResultSet}. + */ +public class DelegatingResultSet implements ResultSet { + + private final Statement statement; + + private final ResultSet delegate; + + private final SQLRunnable closedChecker; + + private volatile boolean closeable; + + /** + * Creates a new {@link DelegatingResultSet}. + * + * @param statement the {@link Statement} that will be returned by the {@link #getStatement()} method; may be {@code + * null} + * + * @param delegate the {@link ResultSet} to which all operations will be delegated; must not be {@code null} + * + * @param closeable the initial value for this {@link DelegatingResultSet}'s {@linkplain #isCloseable() closeable} + * status + * + * @param strictClosedChecking if {@code true}, then this {@link DelegatingResultSet}'s {@link #isClosed()} + * method will be invoked before every operation that cannot take place on a closed statement, and, if it returns + * {@code true}, the operation in question will fail with a {@link SQLException} + * + * @exception NullPointerException if {@code delegate} is {@code null} + */ + public DelegatingResultSet(Statement statement, ResultSet delegate, boolean closeable, boolean strictClosedChecking) { + super(); + this.statement = statement; + this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.closeable = closeable; + this.closedChecker = strictClosedChecking ? this::failWhenClosed : DelegatingResultSet::doNothing; + } + + /** + * Returns the {@link ResultSet} to which all operations will be delegated. + * + *

This method never returns {@code null}.

+ * + * @return the {@link ResultSet} to which all operations will be delegated; never {@code null} + */ + protected final ResultSet delegate() { + return this.delegate; + } + + /** + * Ensures this {@link DelegatingResultSet} is {@linkplain #isClosed() not closed}, if {@linkplain + * #DelegatingResultSet(Connection, ResultSet, boolean, boolean) strict closed checking was enabled at construction + * time}. + * + *

If a subclass overrides the {@link #isClosed()} method, the override must not call this method or undefined + * behavior, such as an infinite loop, may result.

+ * + *

This method is intended for advanced use cases only and almost all users of this class will have no reason to + * call it.

+ * + * @exception SQLException if this {@link DelegatingResultSet} was {@linkplain #DelegatingResultSet(Connection, + * ResultSet, boolean, boolean) created with strict closed checking enabled} and an invocation of the {@link + * #isClosed()} method returns {@code true}, or if some other database access error occurs + */ + protected final void checkOpen() throws SQLException { + this.closedChecker.run(); + } + + // (Invoked by method reference only.) + private void failWhenClosed() throws SQLException { + if (this.isClosed()) { + throw new SQLNonTransientConnectionException("ResultSet is closed", "08000"); + } + } + + /** + * Returns {@code true} if a call to {@link #close()} will actually close this {@link DelegatingResultSet}. + * + *

This method returns {@code true} when {@link #setCloseable(boolean)} has been called with a value of {@code + * true} and the {@link #isClosed()} method returns {@code false}.

+ * + * @return {@code true} if a call to {@link #close()} will actually close this {@link DelegatingResultSet}; {@code + * false} in all other cases + * + * @exception SQLException if {@link #isClosed()} throws a {@link SQLException} + * + * @see #setCloseable(boolean) + * + * @see #close() + * + * @see #isClosed() + */ + public boolean isCloseable() throws SQLException { + // this.checkOpen(); // Deliberately omitted. + return this.closeable && !this.isClosed(); + } + + /** + * Sets the closeable status of this {@link DelegatingResultSet}. + * + *

Note that calling this method with a value of {@code true} does not necessarily mean that the {@link + * #isCloseable()} method will subsequently return {@code true}, since the {@link #isClosed()} method may return + * {@code true}.

+ * + * @param closeable whether or not a call to {@link #close()} will actually close this {@link DelegatingResultSet} + * + * @see #isCloseable() + * + * @see #close() + * + * @see Statement#close() + * + * @see #isClosed() + */ + public void setCloseable(boolean closeable) { + // this.checkOpen(); // Deliberately omitted. + this.closeable = closeable; + } + + @Override + public boolean next() throws SQLException { + checkOpen(); + return this.delegate().next(); + } + + @Override + public void close() throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + this.delegate().close(); + } + + @Override + public boolean wasNull() throws SQLException { + checkOpen(); + return this.delegate().wasNull(); + } + + @Override + public String getString(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getString(columnIndex); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getBoolean(columnIndex); + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getByte(columnIndex); + } + + @Override + public short getShort(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getShort(columnIndex); + } + + @Override + public int getInt(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getInt(columnIndex); + } + + @Override + public long getLong(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getLong(columnIndex); + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getFloat(columnIndex); + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getDouble(columnIndex); + } + + @Deprecated + @Override + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(columnIndex, scale); + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getBytes(columnIndex); + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getDate(columnIndex); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getTime(columnIndex); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(columnIndex); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getAsciiStream(columnIndex); + } + + @Deprecated + @Override + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getUnicodeStream(columnIndex); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getBinaryStream(columnIndex); + } + + @Override + public String getString(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getString(columnLabel); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getBoolean(columnLabel); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getByte(columnLabel); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getShort(columnLabel); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getInt(columnLabel); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getLong(columnLabel); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getFloat(columnLabel); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getDouble(columnLabel); + } + + @Deprecated + @Override + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(columnLabel, scale); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getBytes(columnLabel); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getDate(columnLabel); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getTime(columnLabel); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(columnLabel); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getAsciiStream(columnLabel); + } + + @Deprecated + @Override + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getUnicodeStream(columnLabel); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getBinaryStream(columnLabel); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkOpen(); + return this.delegate().getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + checkOpen(); + this.delegate().clearWarnings(); + } + + @Override + public String getCursorName() throws SQLException { + checkOpen(); + return this.delegate().getCursorName(); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + checkOpen(); + return this.delegate().getMetaData(); + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getObject(columnIndex); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getObject(columnLabel); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().findColumn(columnLabel); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getCharacterStream(columnIndex); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getCharacterStream(columnLabel); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(columnIndex); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getBigDecimal(columnLabel); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + checkOpen(); + return this.delegate().isBeforeFirst(); + } + + @Override + public boolean isAfterLast() throws SQLException { + checkOpen(); + return this.delegate().isAfterLast(); + } + + @Override + public boolean isFirst() throws SQLException { + checkOpen(); + return this.delegate().isFirst(); + } + + @Override + public boolean isLast() throws SQLException { + checkOpen(); + return this.delegate().isLast(); + } + + @Override + public void beforeFirst() throws SQLException { + checkOpen(); + this.delegate().beforeFirst(); + } + + @Override + public void afterLast() throws SQLException { + checkOpen(); + this.delegate().afterLast(); + } + + @Override + public boolean first() throws SQLException { + checkOpen(); + return this.delegate().first(); + } + + @Override + public boolean last() throws SQLException { + checkOpen(); + return this.delegate().last(); + } + + @Override + public int getRow() throws SQLException { + checkOpen(); + return this.delegate().getRow(); + } + + @Override + public boolean absolute(int row) throws SQLException { + checkOpen(); + return this.delegate().absolute(row); + } + + @Override + public boolean relative(int rows) throws SQLException { + checkOpen(); + return this.delegate().relative(rows); + } + + @Override + public boolean previous() throws SQLException { + checkOpen(); + return this.delegate().previous(); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + checkOpen(); + this.delegate().setFetchDirection(direction); + } + + @Override + public int getFetchDirection() throws SQLException { + checkOpen(); + return this.delegate().getFetchDirection(); + } + + @Override + public void setFetchSize(int rows) throws SQLException { + checkOpen(); + this.delegate().setFetchSize(rows); + } + + @Override + public int getFetchSize() throws SQLException { + checkOpen(); + return this.delegate().getFetchSize(); + } + + @Override + public int getType() throws SQLException { + checkOpen(); + return this.delegate().getType(); + } + + @Override + public int getConcurrency() throws SQLException { + checkOpen(); + return this.delegate().getConcurrency(); + } + + @Override + public boolean rowUpdated() throws SQLException { + checkOpen(); + return this.delegate().rowUpdated(); + } + + @Override + public boolean rowInserted() throws SQLException { + checkOpen(); + return this.delegate().rowInserted(); + } + + @Override + public boolean rowDeleted() throws SQLException { + checkOpen(); + return this.delegate().rowDeleted(); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + checkOpen(); + this.delegate().updateNull(columnIndex); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + checkOpen(); + this.delegate().updateBoolean(columnIndex, x); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + checkOpen(); + this.delegate().updateByte(columnIndex, x); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + checkOpen(); + this.delegate().updateShort(columnIndex, x); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + checkOpen(); + this.delegate().updateInt(columnIndex, x); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + checkOpen(); + this.delegate().updateLong(columnIndex, x); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + checkOpen(); + this.delegate().updateFloat(columnIndex, x); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + checkOpen(); + this.delegate().updateDouble(columnIndex, x); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + checkOpen(); + this.delegate().updateBigDecimal(columnIndex, x); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + checkOpen(); + this.delegate().updateString(columnIndex, x); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + checkOpen(); + this.delegate().updateBytes(columnIndex, x); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + checkOpen(); + this.delegate().updateDate(columnIndex, x); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + checkOpen(); + this.delegate().updateTime(columnIndex, x); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + checkOpen(); + this.delegate().updateTimestamp(columnIndex, x); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().updateAsciiStream(columnIndex, x, length); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().updateBinaryStream(columnIndex, x, length); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + checkOpen(); + this.delegate().updateCharacterStream(columnIndex, x, length); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnIndex, x, scaleOrLength); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnIndex, x); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + checkOpen(); + this.delegate().updateNull(columnLabel); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + checkOpen(); + this.delegate().updateBoolean(columnLabel, x); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + checkOpen(); + this.delegate().updateByte(columnLabel, x); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + checkOpen(); + this.delegate().updateShort(columnLabel, x); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + checkOpen(); + this.delegate().updateInt(columnLabel, x); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + checkOpen(); + this.delegate().updateLong(columnLabel, x); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + checkOpen(); + this.delegate().updateFloat(columnLabel, x); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + checkOpen(); + this.delegate().updateDouble(columnLabel, x); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + checkOpen(); + this.delegate().updateBigDecimal(columnLabel, x); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + checkOpen(); + this.delegate().updateString(columnLabel, x); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + checkOpen(); + this.delegate().updateBytes(columnLabel, x); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + checkOpen(); + this.delegate().updateDate(columnLabel, x); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + checkOpen(); + this.delegate().updateTime(columnLabel, x); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + checkOpen(); + this.delegate().updateTimestamp(columnLabel, x); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().updateAsciiStream(columnLabel, x, length); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + checkOpen(); + this.delegate().updateBinaryStream(columnLabel, x, length); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + checkOpen(); + this.delegate().updateCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnLabel, x, scaleOrLength); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnLabel, x); + } + + @Override + public void insertRow() throws SQLException { + checkOpen(); + this.delegate().insertRow(); + } + + @Override + public void updateRow() throws SQLException { + checkOpen(); + this.delegate().updateRow(); + } + + @Override + public void deleteRow() throws SQLException { + checkOpen(); + this.delegate().deleteRow(); + } + + @Override + public void refreshRow() throws SQLException { + checkOpen(); + this.delegate().refreshRow(); + } + + @Override + public void cancelRowUpdates() throws SQLException { + checkOpen(); + this.delegate().cancelRowUpdates(); + } + + @Override + public void moveToInsertRow() throws SQLException { + checkOpen(); + this.delegate().moveToInsertRow(); + } + + @Override + public void moveToCurrentRow() throws SQLException { + checkOpen(); + this.delegate().moveToCurrentRow(); + } + + @Override + public Statement getStatement() throws SQLException { + checkOpen(); + // NOTE + return this.statement; + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + checkOpen(); + return this.delegate().getObject(columnIndex, map); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getRef(columnIndex); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getBlob(columnIndex); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getClob(columnIndex); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getArray(columnIndex); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + checkOpen(); + return this.delegate().getObject(columnLabel, map); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getRef(columnLabel); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getBlob(columnLabel); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getClob(columnLabel); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getArray(columnLabel); + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getDate(columnIndex, cal); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getDate(columnLabel, cal); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTime(columnIndex, cal); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTime(columnLabel, cal); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(columnIndex, cal); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + checkOpen(); + return this.delegate().getTimestamp(columnLabel, cal); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getURL(columnIndex); + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getURL(columnLabel); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + checkOpen(); + this.delegate().updateRef(columnIndex, x); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + checkOpen(); + this.delegate().updateRef(columnLabel, x); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + checkOpen(); + this.delegate().updateBlob(columnIndex, x); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + checkOpen(); + this.delegate().updateBlob(columnLabel, x); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + checkOpen(); + this.delegate().updateClob(columnIndex, x); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + checkOpen(); + this.delegate().updateClob(columnLabel, x); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + checkOpen(); + this.delegate().updateArray(columnIndex, x); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + checkOpen(); + this.delegate().updateArray(columnLabel, x); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getRowId(columnIndex); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getRowId(columnLabel); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + checkOpen(); + this.delegate().updateRowId(columnIndex, x); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + checkOpen(); + this.delegate().updateRowId(columnLabel, x); + } + + @Override + public int getHoldability() throws SQLException { + checkOpen(); + return this.delegate().getHoldability(); + } + + @Override + public boolean isClosed() throws SQLException { + // this.checkOpen(); // Deliberately omitted per spec. + return this.delegate().isClosed(); + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + checkOpen(); + this.delegate().updateNString(columnIndex, nString); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + checkOpen(); + this.delegate().updateNString(columnLabel, nString); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + checkOpen(); + this.delegate().updateNClob(columnIndex, nClob); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + checkOpen(); + this.delegate().updateNClob(columnLabel, nClob); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getNClob(columnIndex); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getNClob(columnLabel); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getSQLXML(columnIndex); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getSQLXML(columnLabel); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + checkOpen(); + this.delegate().updateSQLXML(columnIndex, xmlObject); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + checkOpen(); + this.delegate().updateSQLXML(columnLabel, xmlObject); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getNString(columnIndex); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getNString(columnLabel); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + checkOpen(); + return this.delegate().getNCharacterStream(columnIndex); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + checkOpen(); + return this.delegate().getNCharacterStream(columnLabel); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + checkOpen(); + this.delegate().updateNCharacterStream(columnIndex, x, length); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().updateNCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().updateAsciiStream(columnIndex, x, length); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().updateBinaryStream(columnIndex, x, length); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + checkOpen(); + this.delegate().updateCharacterStream(columnIndex, x, length); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().updateAsciiStream(columnLabel, x, length); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + checkOpen(); + this.delegate().updateBinaryStream(columnLabel, x, length); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().updateCharacterStream(columnLabel, reader, length); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + checkOpen(); + this.delegate().updateBlob(columnIndex, inputStream, length); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + checkOpen(); + this.delegate().updateBlob(columnLabel, inputStream, length); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().updateClob(columnIndex, reader, length); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().updateClob(columnLabel, reader, length); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().updateNClob(columnIndex, reader, length); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + checkOpen(); + this.delegate().updateNClob(columnLabel, reader, length); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + checkOpen(); + this.delegate().updateNCharacterStream(columnIndex, x); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + checkOpen(); + this.delegate().updateNCharacterStream(columnLabel, reader); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + checkOpen(); + this.delegate().updateAsciiStream(columnIndex, x); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + checkOpen(); + this.delegate().updateBinaryStream(columnIndex, x); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + checkOpen(); + this.delegate().updateCharacterStream(columnIndex, x); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + checkOpen(); + this.delegate().updateAsciiStream(columnLabel, x); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + checkOpen(); + this.delegate().updateBinaryStream(columnLabel, x); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + checkOpen(); + this.delegate().updateCharacterStream(columnLabel, reader); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + checkOpen(); + this.delegate().updateBlob(columnIndex, inputStream); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + checkOpen(); + this.delegate().updateBlob(columnLabel, inputStream); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + checkOpen(); + this.delegate().updateClob(columnIndex, reader); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + checkOpen(); + this.delegate().updateClob(columnLabel, reader); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + checkOpen(); + this.delegate().updateNClob(columnIndex, reader); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + checkOpen(); + this.delegate().updateNClob(columnLabel, reader); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + checkOpen(); + return this.delegate().getObject(columnIndex, type); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + checkOpen(); + return this.delegate().getObject(columnLabel, type); + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnIndex, x, targetSqlType, scaleOrLength); + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnLabel, x, targetSqlType, scaleOrLength); + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnIndex, x, targetSqlType); + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType) throws SQLException { + checkOpen(); + this.delegate().updateObject(columnLabel, x, targetSqlType); + } + + @Override + public T unwrap(Class iface) throws SQLException { + // checkOpen(); // Deliberately omitted per spec. + return iface.isInstance(this) ? iface.cast(this) : this.delegate().unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + // checkOpen(); // Deliberately omitted per spec. + return iface.isInstance(this) || this.delegate().isWrapperFor(iface); + } + + + /* + * Static methods. + */ + + + // (Invoked by method reference only.) + private static void doNothing() { + + } + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingStatement.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingStatement.java new file mode 100644 index 00000000000..b6fa230beaf --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/DelegatingStatement.java @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.util.Objects; + +/** + * A JDBC + * 4.3-compliant {@link Statement} that delegates to another JDBC 4.3-compliant {@link Statement}. + * + * @param the type of the {@link Statement} subclass + */ +public class DelegatingStatement implements Statement { + + private final Connection connection; + + private final S delegate; + + private final SQLRunnable closedChecker; + + private volatile boolean closeable; + + /** + * Creates a new {@link DelegatingStatement}. + * + * @param connection the {@link Connection} that created this {@link DelegatingStatement}; must not be {@code null} + * + * @param delegate the {@link Statement} instance to which all operations will be delegated; must not be {@code + * null} + * + * @param closeable the initial value for this {@link DelegatingStatement}'s {@linkplain #isCloseable() closeable} + * status + * + * @param strictClosedChecking if {@code true}, then this {@link DelegatingStatement}'s {@link #isClosed()} + * method will be invoked before every operation that cannot take place on a closed statement, and, if it returns + * {@code true}, the operation in question will fail with a {@link SQLException} + * + * @exception NullPointerException if either {@code connection} or {@code delegate} is {@code null} + * + * @see #getConnection() + */ + public DelegatingStatement(Connection connection, + S delegate, + boolean closeable, + boolean strictClosedChecking) { + super(); + this.connection = Objects.requireNonNull(connection, "connection"); + this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.closeable = closeable; + this.closedChecker = strictClosedChecking ? this::failWhenClosed : DelegatingStatement::doNothing; + } + + /** + * Returns the {@link Statement} to which all operations will be delegated. + * + *

This method never returns {@code null}.

+ * + * @return the {@link Statement} to which all operations will be delegated; never {@code null} + */ + protected final S delegate() { + return this.delegate; + } + + /** + * Returns {@code true} if a call to {@link #close()} will actually close this {@link DelegatingStatement}. + * + *

This method returns {@code true} when {@link #setCloseable(boolean)} has been called with a value of {@code + * true} and the {@link #isClosed()} method returns {@code false}.

+ * + * @return {@code true} if a call to {@link #close()} will actually close this {@link DelegatingStatement}; {@code + * false} in all other cases + * + * @exception SQLException if {@link #isClosed()} throws a {@link SQLException} + * + * @see #setCloseable(boolean) + * + * @see #close() + * + * @see #isClosed() + */ + public boolean isCloseable() throws SQLException { + return this.closeable && !this.isClosed(); + } + + /** + * Sets the closeable status of this {@link DelegatingStatement}. + * + *

Note that calling this method with a value of {@code true} does not necessarily mean that the {@link + * #isCloseable()} method will subsequently return {@code true}, since the {@link #isClosed()} method may return + * {@code true}.

+ * + * @param closeable whether or not a call to {@link #close()} will actually close this {@link DelegatingStatement} + * + * @see #isCloseable() + * + * @see #close() + * + * @see Statement#close() + * + * @see #isClosed() + */ + public void setCloseable(boolean closeable) { + // this.checkOpen(); // Deliberately omitted. + this.closeable = closeable; + } + + @Override + public boolean isClosed() throws SQLException { + return this.delegate().isClosed(); + } + + /** + * Overrides the {@link Statement#close()} method so that when it is invoked this {@link DelegatingStatement} is + * {@linkplain Statement#close() closed} only if it {@linkplain #isCloseable() is closeable}. + * + *

Overrides should normally call {@code super.close()} as part of their implementation.

+ * + * @exception SQLException if an error occurs + * + * @see #isCloseable() + */ + @Override + public void close() throws SQLException { + // NOTE + if (this.isCloseable()) { + this.delegate().close(); + } + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + checkOpen(); + return + new DelegatingResultSet(this, // NOTE + this.delegate().executeQuery(sql), + true, + true); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + checkOpen(); + return this.delegate().executeUpdate(sql); + } + + @Override + public int getMaxFieldSize() throws SQLException { + checkOpen(); + return this.delegate().getMaxFieldSize(); + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + checkOpen(); + this.delegate().setMaxFieldSize(max); + } + + @Override + public int getMaxRows() throws SQLException { + checkOpen(); + return this.delegate().getMaxRows(); + } + + @Override + public void setMaxRows(int max) throws SQLException { + checkOpen(); + this.delegate().setMaxRows(max); + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + checkOpen(); + this.delegate().setEscapeProcessing(enable); + } + + @Override + public int getQueryTimeout() throws SQLException { + checkOpen(); + return this.delegate().getQueryTimeout(); + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + checkOpen(); + this.delegate().setQueryTimeout(seconds); + } + + @Override + public void cancel() throws SQLException { + checkOpen(); + this.delegate().cancel(); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkOpen(); + return this.delegate().getWarnings(); + } + + @Override + public void clearWarnings() throws SQLException { + checkOpen(); + this.delegate().clearWarnings(); + } + + @Override + public void setCursorName(String name) throws SQLException { + checkOpen(); + this.delegate().setCursorName(name); + } + + @Override + public boolean execute(String sql) throws SQLException { + checkOpen(); + return this.delegate().execute(sql); + } + + @Override + public ResultSet getResultSet() throws SQLException { + checkOpen(); + return + new DelegatingResultSet(this, // NOTE + this.delegate().getResultSet(), + true, + true); + } + + @Override + public int getUpdateCount() throws SQLException { + checkOpen(); + return this.delegate().getUpdateCount(); + } + + @Override + public boolean getMoreResults() throws SQLException { + checkOpen(); + return this.delegate().getMoreResults(); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + checkOpen(); + this.delegate().setFetchDirection(direction); + } + + @Override + public int getFetchDirection() throws SQLException { + checkOpen(); + return this.delegate().getFetchDirection(); + } + + @Override + public void setFetchSize(int rows) throws SQLException { + checkOpen(); + this.delegate().setFetchSize(rows); + } + + @Override + public int getFetchSize() throws SQLException { + checkOpen(); + return this.delegate().getFetchSize(); + } + + @Override + public int getResultSetConcurrency() throws SQLException { + checkOpen(); + return this.delegate().getResultSetConcurrency(); + } + + @Override + public int getResultSetType() throws SQLException { + checkOpen(); + return this.delegate().getResultSetType(); + } + + @Override + public void addBatch(String sql) throws SQLException { + checkOpen(); + this.delegate().addBatch(sql); + } + + @Override + public void clearBatch() throws SQLException { + checkOpen(); + this.delegate().clearBatch(); + } + + @Override + public int[] executeBatch() throws SQLException { + checkOpen(); + return this.delegate().executeBatch(); + } + + /** + * Returns the {@link Connection} {@linkplain #DelegatingStatement(Connection, Statement, boolean, boolean) supplied + * at construction time}. + * + * @return the {@link Connection} {@linkplain #DelegatingStatement(Connection, Statement, boolean, boolean) supplied + * at construction time}; never {@code null} + * + * @exception SQLException not thrown by the default implementation of this method + * + * @see #DelegatingStatement(Connection, Statement, boolean, boolean) + */ + @Override + public Connection getConnection() throws SQLException { + checkOpen(); + // NOTE + return this.connection; + } + + @Override + public boolean getMoreResults(int current) throws SQLException { + checkOpen(); + return this.delegate().getMoreResults(current); + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + checkOpen(); + return + new DelegatingResultSet(this, // NOTE + this.delegate().getGeneratedKeys(), + true, + true); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkOpen(); + return this.delegate().executeUpdate(sql, autoGeneratedKeys); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + checkOpen(); + return this.delegate().executeUpdate(sql, columnIndexes); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + checkOpen(); + return this.delegate().executeUpdate(sql, columnNames); + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + checkOpen(); + return this.delegate().execute(sql, autoGeneratedKeys); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + checkOpen(); + return this.delegate().execute(sql, columnIndexes); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + checkOpen(); + return this.delegate().execute(sql, columnNames); + } + + @Override + public int getResultSetHoldability() throws SQLException { + checkOpen(); + return this.delegate().getResultSetHoldability(); + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + checkOpen(); + this.delegate().setPoolable(poolable); + } + + @Override + public boolean isPoolable() throws SQLException { + checkOpen(); + return this.delegate().isPoolable(); + } + + @Override + public void closeOnCompletion() throws SQLException { + checkOpen(); + this.delegate().closeOnCompletion(); + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + checkOpen(); + return this.delegate().isCloseOnCompletion(); + } + + @Override + public long getLargeUpdateCount() throws SQLException { + checkOpen(); + return this.delegate().getLargeUpdateCount(); + } + + @Override + public void setLargeMaxRows(long max) throws SQLException { + checkOpen(); + this.delegate().setLargeMaxRows(max); + } + + @Override + public long getLargeMaxRows() throws SQLException { + checkOpen(); + return this.delegate().getLargeMaxRows(); + } + + @Override + public long[] executeLargeBatch() throws SQLException { + checkOpen(); + return this.delegate().executeLargeBatch(); + } + + @Override + public long executeLargeUpdate(String sql) throws SQLException { + checkOpen(); + return this.delegate().executeLargeUpdate(sql); + } + + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + checkOpen(); + return this.delegate().executeLargeUpdate(sql, autoGeneratedKeys); + } + + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + checkOpen(); + return this.delegate().executeLargeUpdate(sql, columnIndexes); + } + + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + checkOpen(); + return this.delegate().executeLargeUpdate(sql, columnNames); + } + + @Override + public String enquoteLiteral(String val) throws SQLException { + checkOpen(); + return this.delegate().enquoteLiteral(val); + } + + @Override + public String enquoteIdentifier(String identifier, boolean alwaysQuote) throws SQLException { + checkOpen(); + return this.delegate().enquoteIdentifier(identifier, alwaysQuote); + } + + @Override + public boolean isSimpleIdentifier(String identifier) throws SQLException { + checkOpen(); + return this.delegate().isSimpleIdentifier(identifier); + } + + @Override + public String enquoteNCharLiteral(String val) throws SQLException { + checkOpen(); + return this.delegate().enquoteNCharLiteral(val); + } + + @Override + public T unwrap(Class iface) throws SQLException { + // checkOpen(); // Deliberately omitted + return iface.isInstance(this) ? iface.cast(this) : this.delegate().unwrap(iface); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + // checkOpen(); // Deliberately omitted + return iface.isInstance(this) || this.delegate().isWrapperFor(iface); + } + + /** + * Ensures this {@link DelegatingStatement} is {@linkplain #isClosed() not closed}, if {@linkplain + * #DelegatingStatement(Connection, Statement, boolean, boolean) strict closed checking was enabled at construction + * time}. + * + *

If a subclass overrides the {@link #isClosed()} method, the override must not call this method or undefined + * behavior, such as an infinite loop, may result.

+ * + *

This method is intended for advanced use cases only and almost all users of this class will have no reason to + * call it.

+ * + * @exception SQLException if this {@link DelegatingStatement} was {@linkplain #DelegatingStatement(Connection, + * Statement, boolean, boolean) created with strict closed checking enabled} and an invocation of the {@link + * #isClosed()} method returns {@code true}, or if some other database access error occurs + */ + protected final void checkOpen() throws SQLException { + this.closedChecker.run(); + } + + // (Invoked by method reference only.) + private void failWhenClosed() throws SQLException { + if (this.isClosed()) { + throw new SQLNonTransientConnectionException("Statement is closed", "08000"); + } + } + + + /* + * Static methods. + */ + + + // (Invoked by method reference only.) + private static void doNothing() { + + } + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLBooleanSupplier.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLBooleanSupplier.java new file mode 100644 index 00000000000..c9882567e37 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLBooleanSupplier.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.SQLException; + +/** + * A useful functional interface whose implementations can perform work that may throw a {@link SQLException}. + * + * @see #getAsBoolean() + */ +@FunctionalInterface +public interface SQLBooleanSupplier { + + /** + * Performs work and returns the result. + * + * @return a {@code boolean} + * + * @exception SQLException if a database access error occurs + */ + boolean getAsBoolean() throws SQLException; + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLRunnable.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLRunnable.java new file mode 100644 index 00000000000..07a44d74a59 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLRunnable.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.SQLException; + +/** + * A useful functional interface whose implementations can perform work that may throw a {@link SQLException}. + * + * @see #run() + */ +@FunctionalInterface +public interface SQLRunnable { + + /** + * Performs work. + * + * @exception SQLException if a database access error occurs + */ + void run() throws SQLException; + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLSupplier.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLSupplier.java new file mode 100644 index 00000000000..39b1f06db4d --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/SQLSupplier.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.SQLException; + +/** + * A useful functional interface whose implementations can perform work that may throw a {@link SQLException}. + * + * @param the type of the object supplied + * + * @see #get() + */ +@FunctionalInterface +public interface SQLSupplier { + + /** + * Performs work and returns the result. + * + * @return the result of the work + * + * @exception SQLException if a database access error occurs + */ + T get() throws SQLException; + +} diff --git a/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/UncheckedSQLException.java b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/UncheckedSQLException.java new file mode 100644 index 00000000000..6c792fabe68 --- /dev/null +++ b/integrations/jdbc/jdbc/src/main/java/io/helidon/integrations/jdbc/UncheckedSQLException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.SQLException; + +/** + * A {@link RuntimeException} that wraps a {@link SQLException}. + */ +public final class UncheckedSQLException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + * Creates a new {@link UncheckedSQLException}. + * + * @param cause the {@link SQLException} that this {@link UncheckedSQLException} will represent; may be {@code null} + */ + public UncheckedSQLException(SQLException cause) { + super(cause); + } + + /** + * Returns the {@link SQLException} this {@link UncheckedSQLException} represents. + * + *

This method may return {@code null}.

+ * + * @return the {@link SQLException} this {@link UncheckedSQLException} represents, or {@code null} + */ + @Override + public SQLException getCause() { + return (SQLException) super.getCause(); + } + +} diff --git a/integrations/jdbc/jdbc/src/main/java/module-info.java b/integrations/jdbc/jdbc/src/main/java/module-info.java index 922b2013542..35152e0a9b4 100644 --- a/integrations/jdbc/jdbc/src/main/java/module-info.java +++ b/integrations/jdbc/jdbc/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,9 @@ */ module io.helidon.integrations.jdbc { + // Tests use H2 which requires resolution of java.naming at compile time only. + requires static java.naming; + requires transitive java.sql; exports io.helidon.integrations.jdbc; diff --git a/integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestConditionallyCloseableConnection.java b/integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestConditionallyCloseableConnection.java new file mode 100644 index 00000000000..ba58aa1920e --- /dev/null +++ b/integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestConditionallyCloseableConnection.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; + +final class TestConditionallyCloseableConnection { + + private DataSource ds; + + private TestConditionallyCloseableConnection() { + super(); + } + + @BeforeEach + final void initializeDataSource() throws SQLException { + final JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:testConditionallyCloseableConnection"); + ds.setUser("sa"); + ds.setPassword("sa"); + this.ds = ds; + } + + @AfterEach + final void closeDataSource() throws SQLException { + try (final Connection c = this.ds.getConnection(); + final Statement s = c.createStatement();) { + s.execute("SHUTDOWN"); + } + } + + @SuppressWarnings("try") + @Test + final void testLegacyIsCloseable() throws SQLException { + try (@SuppressWarnings("deprecation") + final ConditionallyCloseableConnection c = new ConditionallyCloseableConnection(this.ds.getConnection()); + final Statement s = c.createStatement(); + final ResultSet rs = s.executeQuery("SHOW TABLES")) { + assertThat(rs.next(), is(false)); // no tables + + // ConditionallyCloseableConnections are closeable by default. + assertThat(c.isCloseable(), is(true)); + + c.setCloseable(false); + assertThat(c.isCloseable(), is(false)); + + // Closing a ConditionallyCloseableConnection when + // isCloseable() returns false does not actually close the delegate. + c.close(); + assertThat(c.delegate().isClosed(), is(false)); + + // IN BACKWARDS COMPATIBILITY MODE ONLY, it also does not + // make the ConditionallyCloseableConnection itself look + // like it is closed. + assertThat(c.isClosed(), is(false)); + + // Make sure we recorded that a close() attempt happened. + assertThat(c.isClosePending(), is(true)); + + // We can reset the closeable status. + c.setCloseable(true); + assertThat(c.isCloseable(), is(true)); + assertThat(c.isClosed(), is(false)); + + // This also wipes out the fact that a close was + // previously attempted. + assertThat(c.isClosePending(), is(false)); + + // Closing a ConditionallyCloseableConnection when + // isCloseable() returns true actually irrevocably closes + // the connection. + c.close(); // closes for real + assertThat(c.isClosed(), is(true)); + assertThat(c.isClosePending(), is(false)); + + // Still closed. + c.setCloseable(false); // won't matter + assertThat(c.isClosed(), is(true)); + assertThat(c.isClosePending(), is(false)); + + // Note that the JDBC specification says that closing a + // connection will release "this Connection object's + // database and JDBC resources immediately" + // (https://docs.oracle.com/en/java/javase/19/docs/api/java.sql/java/sql/Connection.html#close()). + // However it is unclear whether a Statement constitutes + // either a "database" or a "JDBC resource" in this + // context. You would think it would, but H2 does not + // close open Statements or ResultSets when their creating + // Connection is closed, and neither does PostgreSQL. + assertThat(s.isClosed(), is(false)); + assertThat(rs.isClosed(), is(false)); + } + } + + @SuppressWarnings("try") + @Test + final void testIsCloseable() throws SQLException { + try (final ConditionallyCloseableConnection c = new ConditionallyCloseableConnection(this.ds.getConnection(), true, true); + final Statement s = c.createStatement(); + final ResultSet rs = s.executeQuery("SHOW TABLES")) { + assertThat(rs.next(), is(false)); // no tables + + // ConditionallyCloseableConnections are closeable by default. + assertThat(c.isCloseable(), is(true)); + + c.setCloseable(false); + assertThat(c.isCloseable(), is(false)); + + // Closing a ConditionallyCloseableConnection when + // isCloseable() returns false does not actually close the delegate. + c.close(); + assertThat(c.delegate().isClosed(), is(false)); + + // It does make the ConditionallyCloseableConnection + // itself look like it is closed, however. + assertThat(c.isClosed(), is(true)); + + // Make sure we recorded that a close() attempt happened. + assertThat(c.isClosePending(), is(true)); + + // We can reset the closeable status. + c.setCloseable(true); + assertThat(c.isCloseable(), is(true)); + assertThat(c.isClosed(), is(false)); + + // This also wipes out the fact that a close was + // previously attempted. + assertThat(c.isClosePending(), is(false)); + + // Closing a ConditionallyCloseableConnection when + // isCloseable() returns true actually irrevocably closes + // the connection. + c.close(); // closes for real + assertThat(c.isClosed(), is(true)); + assertThat(c.isClosePending(), is(false)); + + // Still closed. + c.setCloseable(false); // won't matter + assertThat(c.isClosed(), is(true)); + assertThat(c.isClosePending(), is(false)); + + // Note that the JDBC specification says that closing a + // connection will release "this Connection object's + // database and JDBC resources immediately" + // (https://docs.oracle.com/en/java/javase/19/docs/api/java.sql/java/sql/Connection.html#close()). + // However it is unclear whether a Statement constitutes + // either a "database" or a "JDBC resource" in this + // context. You would think it would, but H2 does not + // close open Statements or ResultSets when their creating + // Connection is closed, and neither does PostgreSQL. + assertThat(s.isClosed(), is(false)); + assertThat(rs.isClosed(), is(false)); + } + } + +} diff --git a/integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestH2IsolationLevelCases.java b/integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestH2IsolationLevelCases.java new file mode 100644 index 00000000000..8e27c5d0f22 --- /dev/null +++ b/integrations/jdbc/jdbc/src/test/java/io/helidon/integrations/jdbc/TestH2IsolationLevelCases.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; + +final class TestH2IsolationLevelCases { + + private DataSource ds; + + private TestH2IsolationLevelCases() { + super(); + } + + @BeforeEach + final void initializeDataSource() throws SQLException { + final JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:testH2IsolationLevelCases;INIT=SET DB_CLOSE_DELAY=-1"); + ds.setUser("sa"); + ds.setPassword("sa"); + this.ds = ds; + } + + @AfterEach + final void closeDataSource() throws SQLException { + try (final Connection c = this.ds.getConnection(); + final Statement s = c.createStatement();) { + s.execute("SHUTDOWN"); + } + } + + @Test + final void testGeneratedKeysAreVisibleWithinATransactionWithReadCommittedIsolationLevel() throws SQLException { + try (final Connection c = this.ds.getConnection()) { + assertThat(c.getTransactionIsolation(), is(Connection.TRANSACTION_READ_COMMITTED)); + c.setAutoCommit(false); + assertThat(c.getAutoCommit(), is(false)); + + Statement s = c.createStatement(); + s.executeUpdate(""" + CREATE TABLE TRAINER (ID INTEGER GENERATED BY DEFAULT AS IDENTITY, + AGE INTEGER NOT NULL, + NAME VARCHAR(255), + PRIMARY KEY (ID)); + """); + s.close(); + + s = c.createStatement(); + s.executeUpdate(""" + CREATE TABLE POKEMON (ID INTEGER NOT NULL, + CP INTEGER NOT NULL, + NAME VARCHAR(255), + TRAINER_ID INTEGER, + PRIMARY KEY (ID)); + """); + s.close(); + + s = c.createStatement(); + s.executeUpdate(""" + ALTER TABLE IF EXISTS POKEMON + ADD CONSTRAINT POKEMON_TO_TRAINER + FOREIGN KEY (TRAINER_ID) REFERENCES TRAINER; + """); + s.close(); + + PreparedStatement ps = + c.prepareStatement(""" + INSERT INTO TRAINER (ID, AGE, NAME) + VALUES (DEFAULT, ?, ?); + """, Statement.RETURN_GENERATED_KEYS); + ps.setInt(1, 10); + ps.setString(2, "Ash Ketchum"); + ps.executeUpdate(); + + ResultSet keys = ps.getGeneratedKeys(); + keys.next(); + int id = keys.getInt(1); + assertThat(id, is(1)); + keys.close(); + + ps.close(); + + ps = + c.prepareStatement(""" + INSERT INTO POKEMON (CP, NAME, TRAINER_ID, ID) + VALUES (?, ?, ?, ?); + """); + ps.setInt(1, 252); + ps.setString(2, "Pikachu"); + ps.setInt(3, id); + ps.setInt(4, 1); + ps.executeUpdate(); + ps.close(); + + c.commit(); + } + } + +} diff --git a/integrations/jta/jdbc/etc/spotbugs/exclude.xml b/integrations/jta/jdbc/etc/spotbugs/exclude.xml new file mode 100644 index 00000000000..970ca624b0f --- /dev/null +++ b/integrations/jta/jdbc/etc/spotbugs/exclude.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + diff --git a/integrations/jta/jdbc/pom.xml b/integrations/jta/jdbc/pom.xml index 569255b37d2..dc2ed451f1f 100644 --- a/integrations/jta/jdbc/pom.xml +++ b/integrations/jta/jdbc/pom.xml @@ -31,8 +31,18 @@ Helidon Integrations JTA/JDBC JTA- and JDBC-related integration classes + + etc/spotbugs/exclude.xml + + + + + io.helidon.integrations.jdbc + helidon-integrations-jdbc + + jakarta.transaction @@ -40,12 +50,64 @@ provided - + - io.helidon.integrations.jdbc - helidon-integrations-jdbc + org.junit.jupiter + junit-jupiter-api + test + + + + org.hamcrest + hamcrest-all + test + + + + com.h2database + h2 + test + + + + com.zaxxer + HikariCP + test + + + + org.jboss.logging + jboss-logging + test + + + + org.jboss.narayana.jta + narayana-jta-jakarta + test + + + + org.slf4j + slf4j-jdk14 + test + + + + org.apache.maven.plugins + maven-surefire-plugin + + + ${project.build.testOutputDirectory}/logging.properties + jdk + + + + + + diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/ExceptionConverter.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/ExceptionConverter.java new file mode 100644 index 00000000000..b0e339a37bc --- /dev/null +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/ExceptionConverter.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import javax.transaction.xa.XAException; + +import static javax.transaction.xa.XAException.XAER_ASYNC; +import static javax.transaction.xa.XAException.XAER_DUPID; +import static javax.transaction.xa.XAException.XAER_INVAL; +import static javax.transaction.xa.XAException.XAER_NOTA; +import static javax.transaction.xa.XAException.XAER_OUTSIDE; +import static javax.transaction.xa.XAException.XAER_PROTO; +import static javax.transaction.xa.XAException.XAER_RMERR; +import static javax.transaction.xa.XAException.XAER_RMFAIL; +import static javax.transaction.xa.XAException.XA_HEURCOM; +import static javax.transaction.xa.XAException.XA_HEURHAZ; +import static javax.transaction.xa.XAException.XA_HEURMIX; +import static javax.transaction.xa.XAException.XA_HEURRB; +import static javax.transaction.xa.XAException.XA_NOMIGRATE; +import static javax.transaction.xa.XAException.XA_RBCOMMFAIL; +import static javax.transaction.xa.XAException.XA_RBDEADLOCK; +import static javax.transaction.xa.XAException.XA_RBINTEGRITY; +import static javax.transaction.xa.XAException.XA_RBOTHER; +import static javax.transaction.xa.XAException.XA_RBPROTO; +import static javax.transaction.xa.XAException.XA_RBROLLBACK; +import static javax.transaction.xa.XAException.XA_RBTIMEOUT; +import static javax.transaction.xa.XAException.XA_RBTRANSIENT; +import static javax.transaction.xa.XAException.XA_RDONLY; +import static javax.transaction.xa.XAException.XA_RETRY; + +/** + * A {@linkplain FunctionalInterface functional interface} whose implementations can convert a kind of {@link Exception} + * encountered in the context of an {@linkplain XARoutine XA routine} to an appropriate {@link XAException}, according + * to the rules in the XA specification as + * expressed in the {@linkplain javax.transaction.xa.XAResource documentation for the XAResource + * interface}. + * + * @see #convert(XARoutine, Exception) + * + * @see XARoutine + * + * @see XAException#errorCode + * + * @see javax.transaction.xa.XAResource + * + * @see The XA Specification + */ +@FunctionalInterface +public interface ExceptionConverter { + + + /** + * Converts the supplied {@link Exception} encountered in the context of the supplied {@link XARoutine} to an {@link + * XAException} with {@linkplain XAException#errorCode an appropriate error code}, idiomatically following the rules + * of the XA specification. + * + * @param xaRoutine the {@link XARoutine}; must not be {@code null} + * + * @param exception the {@link Exception} to convert; most commonly a variety of {@link java.sql.SQLException} or + * {@link RuntimeException}; if supplied with an {@link XAException} this {@link ExceptionConverter} must + * simply return it; if supplied with {@code null} a new {@link XAException} with a general-purpose error + * codemust be returned + * + * @return a suitable non-{@code null} {@link XAException} + * + * @exception NullPointerException if {@code routine} is {@code null} + * + * @see XAException + * + * @see javax.transaction.xa.XAResource + */ + XAException convert(XARoutine xaRoutine, Exception exception); + + + /* + * Static methods. + */ + + + /** + * Returns a non-{@code null} {@link String} representation of the supplied code. + * + *

The format of the returned {@link String} is left deliberately undefined and may change between versions of + * this interface without prior notice.

+ * + * @param code a code; usually the value of a {@code static} field in the {@link XAException} class + * + * @return a non-{@code null} {@link String} representation of the supplied code + */ + static String codeToString(int code) { + switch (code) { + case XA_HEURCOM: + return "XA_HEURCOM"; + case XA_HEURHAZ: + return "XA_HEURHAZ"; + case XA_HEURMIX: + return "XA_HEURMIX"; + case XA_HEURRB: + return "XA_HEURRB"; + case XA_NOMIGRATE: + return "XA_NOMIGRATE"; + case XA_RBCOMMFAIL: + return "XA_RBCOMMFAIL"; + case XA_RBDEADLOCK: + return "XA_RBDEADLOCK"; + case XA_RBINTEGRITY: + return "XA_RBINTEGRITY"; + case XA_RBOTHER: + return "XA_RBOTHER"; + case XA_RBPROTO: + return "XA_RBPROTO"; + case XA_RBROLLBACK: + return "XA_RBROLLBACK"; + case XA_RBTIMEOUT: + return "XA_RBTIMEOUT"; + case XA_RBTRANSIENT: + return "XA_RBTRANSIENT"; + case XA_RDONLY: + return "XA_RDONLY"; + case XA_RETRY: + return "XA_RETRY"; + case XAER_ASYNC: + return "XAER_ASYNC"; + case XAER_DUPID: + return "XAER_DUPID"; + case XAER_INVAL: + return "XAER_INVAL"; + case XAER_NOTA: + return "XAER_NOTA"; + case XAER_OUTSIDE: + return "XAER_OUTSIDE"; + case XAER_PROTO: + return "XAER_PROTO"; + case XAER_RMERR: + return "XAER_RMERR"; + case XAER_RMFAIL: + return "XAER_RMFAIL"; + default: + return String.valueOf(code); + } + } + + + /* + * Inner and nested classes. + */ + + + /** + * An enum describing XA routines modeled by an {@link javax.transaction.xa.XAResource} implementation. + */ + enum XARoutine { + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#start(Xid, int)} method. + */ + START, + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#end(Xid, int)} method. + */ + END, + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#prepare(Xid)} method. + */ + PREPARE, + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#commit(Xid, boolean)} method. + */ + COMMIT, + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#rollback(Xid)} method. + */ + ROLLBACK, + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#recover(int)} method. + */ + RECOVER, + + /** + * An enum constant modeling the {@link javax.transaction.xa.XAResource#forget(Xid)} method. + */ + FORGET; + + } + +} diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaAdaptingDataSource.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaAdaptingDataSource.java new file mode 100644 index 00000000000..5fc7c6d70af --- /dev/null +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaAdaptingDataSource.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Objects; + +import javax.sql.DataSource; +import javax.sql.XAConnection; +import javax.sql.XADataSource; + +import io.helidon.integrations.jdbc.AbstractDataSource; + +import jakarta.transaction.TransactionSynchronizationRegistry; + +/** + * An {@link AbstractDataSource} that wraps another {@link DataSource} that might not behave correctly in the presence + * of JTA transaction management, such as one supplied by any of several freely and commercially available connection + * pools, and that makes such a non-JTA-aware {@link DataSource} behave as sensibly as possible in the presence of a + * JTA-managed transaction. + */ +public final class JtaAdaptingDataSource extends AbstractDataSource { + + + /* + * Instance fields. + */ + + + private final AuthenticatedConnectionSupplier acs; + + private final UnauthenticatedConnectionSupplier uacs; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link JtaAdaptingDataSource} that wraps the supplied {@link DataSource} and helps its {@linkplain + * DataSource#getConnection() connections} participate in XA transactions. + * + *

Behavior is left deliberately undefined if the supplied {@link DataSource}'s {@link + * DataSource#getConnection()} or {@link DataSource#getConnection(String, String)} methods are implemented to return + * or augment the return value of an invocation of the {@link javax.sql.PooledConnection#getConnection() + * XAConnection#getConnection()} method. Less formally, and in general, this class is deliberately not designed to + * work with JDBC constructs that are already XA-aware.

+ * + * @param ts a {@link TransactionSupplier}; must not be {@code null} + * + * @param tsr a {@link TransactionSynchronizationRegistry}; must not be {@code null} + * + * @param interposedSynchronizations whether any {@link jakarta.transaction.Synchronization Synchronization}s + * registered should be registered as interposed synchronizations; see {@link + * TransactionSynchronizationRegistry#registerInterposedSynchronization(jakarta.transaction.Synchronization)} and + * {@link jakarta.transaction.Transaction#registerSynchronization(jakarta.transaction.Synchronization)} + * + * @param ec an {@link ExceptionConverter}; may be {@code null} in which case a default implementation will be used + * instead + * + * @param ds a {@link DataSource} that may not be XA-compliant; must not be {@code null}; normally supplied by a + * connection pool implementation + * + * @param immediateEnlistment whether attempts to enlist new {@link Connection}s in a global transaction should be + * made immediately upon {@link Connection} allocation + * + * @exception NullPointerException if {@code ts}, {@code tsr} or {@code ds} is {@code null} + */ + public JtaAdaptingDataSource(TransactionSupplier ts, + TransactionSynchronizationRegistry tsr, + boolean interposedSynchronizations, + ExceptionConverter ec, + DataSource ds, + boolean immediateEnlistment) { + super(); + Objects.requireNonNull(ts, "ts"); + Objects.requireNonNull(tsr, "tsr"); + if (ds instanceof XADataSource xads) { + // Some connection pools supply XADataSource objects that pool XAConnections while also unconventionally + // exposing them to end users, and also implement the javax.sql.DataSource interface by throwing exceptions + // when its methods are invoked. Although XAConnections are not intended for end users, when logical + // representations of pooled XAConnections *are* supplied to end users by such a connection pool, those + // "borrowed" representations must be "returned" via their close() methods, counter to the documentation of + // the (inherited) PooledConnection#close() method, which reads, in part: "An application never calls this + // method directly; it is called by the connection pool module, or manager." As of this writing this branch + // of this constructor implements this non-standard behavior. + this.acs = + (u, p) -> xa(ts, tsr, interposedSynchronizations, ec, xads.getXAConnection(u, p), immediateEnlistment, true); + this.uacs = () -> xa(ts, tsr, interposedSynchronizations, ec, xads.getXAConnection(), immediateEnlistment, true); + } else { + Objects.requireNonNull(ds, "ds"); + this.acs = + (u, p) -> new JtaConnection(ts, tsr, interposedSynchronizations, ec, ds.getConnection(u, p), immediateEnlistment); + this.uacs = () -> new JtaConnection(ts, tsr, interposedSynchronizations, ec, ds.getConnection(), immediateEnlistment); + } + } + + /** + * Creates a new {@link JtaAdaptingDataSource} that adapts the supplied {@link XADataSource} and helps {@link + * Connection}s it indirectly supplies (by way of its {@linkplain XADataSource#getXAConnection() associated + * XAConnection}) participate in XA transactions. + * + * @param ts a {@link TransactionSupplier}; must not be {@code null} + * + * @param tsr a {@link TransactionSynchronizationRegistry}; must not be {@code null} + * + * @param interposedSynchronizations whether any {@link jakarta.transaction.Synchronization Synchronization}s + * registered should be registered as interposed synchronizations; see {@link + * TransactionSynchronizationRegistry#registerInterposedSynchronization(jakarta.transaction.Synchronization)} and + * {@link jakarta.transaction.Transaction#registerSynchronization(jakarta.transaction.Synchronization)} + * + * @param ec an {@link ExceptionConverter}; may be {@code null} in which case a default implementation will be used + * instead + * + * @param xads an {@link XADataSource} supplied by a connection pool implementation; must not be {@code null} + * + * @param immediateEnlistment whether attempts to enlist new {@link Connection}s in a global transaction should be + * made immediately upon {@link Connection} allocation + * + * @param closeXac whether or not {@link XAConnection}s {@linkplain XADataSource#getXAConnection() supplied} by the + * supplied {@link XADataSource} should be {@linkplain javax.sql.PooledConnection#close() closed} when {@linkplain + * XAConnection#getConnection() their Connection}s are {@linkplain Connection#close() closed} (in a + * non-standard fashion) + * + * @exception NullPointerException if {@code ts}, {@code tsr} or {@code xads} is {@code null} + * + * @deprecated This constructor exists only to handle certain XA-aware connection pools that allow an end-user + * caller to "borrow" {@link XAConnection}s and to "return" them using their {@link + * javax.sql.PooledConnection#close() close()} methods, a non-standard practice which is discouraged by the + * documentation of {@link javax.sql.PooledConnection} (from which {@link XAConnection} inherits). For + * such connection pools, {@link XAConnection}s that are "borrowed" must be returned in this manner to avoid leaks. + * This constructor implements this behavior. Before using it, you should make sure that the connection pool in + * question implementing or supplying the {@link XADataSource} has the behavior described above; normally an {@link + * XAConnection} should not be used directly or closed by end-user code. + */ + @Deprecated(since = "3.1.0") + public JtaAdaptingDataSource(TransactionSupplier ts, + TransactionSynchronizationRegistry tsr, + boolean interposedSynchronizations, + ExceptionConverter ec, + XADataSource xads, + boolean immediateEnlistment, + boolean closeXac) { + super(); + Objects.requireNonNull(xads, "xads"); + Objects.requireNonNull(ts, "ts"); + Objects.requireNonNull(tsr, "tsr"); + // Some connection pools supply XADataSource objects that pool XAConnections. In all likelihood, they should not + // do this, given that XAConnections are supposed to be for the innards of connection pools, not for end users. + // Nevertheless, when such XAConnections are pooled and logical representations of them are supplied by such a + // connection pool, those representations must be closed, counter to the documentation of the (inherited) + // PooledConnection#close() method, which reads, in part: "An application never calls this method directly; it + // is called by the connection pool module, or manager." As of this writing this constructor permits this + // non-standard behavior when closeXac is true. + this.acs = + (u, p) -> xa(ts, tsr, interposedSynchronizations, ec, xads.getXAConnection(u, p), immediateEnlistment, closeXac); + this.uacs = () -> xa(ts, tsr, interposedSynchronizations, ec, xads.getXAConnection(), immediateEnlistment, closeXac); + } + + @Override // DataSource + public Connection getConnection(String username, String password) throws SQLException { + return this.acs.getConnection(username, password); + } + + @Override // DataSource + public Connection getConnection() throws SQLException { + return this.uacs.getConnection(); + } + + + /* + * Static methods. + */ + + + @Deprecated + private static JtaConnection xa(TransactionSupplier ts, + TransactionSynchronizationRegistry tsr, + boolean interposedSynchronizations, + ExceptionConverter ec, + XAConnection xac, + boolean immediateEnlistment, + boolean closeXac) + throws SQLException { + if (closeXac) { + // Some connection pools allow you to "borrow" XAConnections. XAConnections were never intended to be + // exposed to end users in this fashion. To return a "borrowed" XAConnection, you invoke its close() method, + // which violates the contract of PooledConnection#close(), which XAConnection inherits, whose documentation + // reads: "An application never calls this method directly; it is called by the connection pool module, or + // manager." This branch of this method implements this non-standard behavior, ensuring that both the + // Connection and its sourcing XAConnection are closed appropriately. + return new JtaConnection(ts, + tsr, + interposedSynchronizations, + ec, + xac.getConnection(), + xac::getXAResource, + immediateEnlistment) { + @Override + protected void onClose() throws SQLException { + xac.close(); + } + }; + } + return + new JtaConnection(ts, + tsr, + interposedSynchronizations, + ec, + xac.getConnection(), + xac::getXAResource, + immediateEnlistment); + } + + + /* + * Inner and nested classes. + */ + + + @FunctionalInterface + private interface UnauthenticatedConnectionSupplier { + + Connection getConnection() throws SQLException; + + } + + @FunctionalInterface + private interface AuthenticatedConnectionSupplier { + + Connection getConnection(String username, String password) throws SQLException; + + } + +} diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaConnection.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaConnection.java new file mode 100644 index 00000000000..875ff67eef7 --- /dev/null +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaConnection.java @@ -0,0 +1,1201 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLNonTransientConnectionException; +import java.sql.SQLNonTransientException; +import java.sql.SQLTransientException; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.ShardingKey; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import io.helidon.integrations.jdbc.ConditionallyCloseableConnection; +import io.helidon.integrations.jdbc.SQLSupplier; +import io.helidon.integrations.jdbc.UncheckedSQLException; + +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.Synchronization; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import static javax.transaction.xa.XAResource.TMSUCCESS; + +/** + * A JDBC 4.3-compliant {@link ConditionallyCloseableConnection} that can participate in a {@link Transaction}. + */ +class JtaConnection extends ConditionallyCloseableConnection { + + + /* + * Static fields. + */ + + + private static final Logger LOGGER = Logger.getLogger(JtaConnection.class.getName()); + + // The standard SQL state used for unspecified connection exceptions. Used in this class primarily to indicate + // premature connection closure. + private static final String CONNECTION_EXCEPTION_NO_SUBCLASS = "08000"; + + // The standard SQL state used for transaction-related issues. + private static final String INVALID_TRANSACTION_STATE_NO_SUBCLASS = "25000"; + + // IBM's proprietary but very descriptive, useful and specific SQL state for when a savepoint operation has been + // attempted during a global transaction ("A SAVEPOINT, RELEASE SAVEPOINT, or ROLLBACK TO SAVEPOINT is not allowed + // in a trigger, function, or global transaction"). + private static final String PROHIBITED_SAVEPOINT_OPERATION = "3B503"; + + // SQL state 40000 ("transaction rollback, no subclass") is unclear whether it means the SQL/local transaction or + // the XA branch transaction or both. It's a convenient SQLState to use nonetheless for handling converting + // RollbackExceptions to SQLExceptions. + private static final String TRANSACTION_ROLLBACK = "40000"; + + private static final VarHandle ENLISTMENT; + + static { + try { + ENLISTMENT = MethodHandles.lookup().findVarHandle(JtaConnection.class, "enlistment", Enlistment.class); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw (ExceptionInInitializerError) new ExceptionInInitializerError(e.getMessage()).initCause(e); + } + } + + + /* + * Instance fields. + */ + + + /** + * A supplier of {@link Transaction} objects. Often initialized to {@link + * jakarta.transaction.TransactionManager#getTransaction() transactionManager::getTransaction}. + * + *

This field is never {@code null}.

+ * + * @see TransactionSupplier + * + * @see jakarta.transaction.TransactionManager#getTransaction() + */ + private final TransactionSupplier ts; + + /** + * A {@link TransactionSynchronizationRegistry}. + * + *

This field is never {@code null}.

+ * + * @see TransactionSynchronizationRegistry + */ + private final TransactionSynchronizationRegistry tsr; + + /** + * Whether any {@link Synchronization}s registered by this {@link JtaConnection} + * should be registered as interposed synchronizations. + * + * @see TransactionSynchronizationRegistry#registerInterposedSynchronization(Synchronization) + * + * @see Transaction#registerSynchronization(Synchronization) + */ + private final boolean interposedSynchronizations; + + private final SQLSupplier xaResourceSupplier; + + private final Consumer xidConsumer; + + private volatile Enlistment enlistment; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link JtaConnection}. + * + * @param transactionSupplier a {@link TransactionSupplier}; must not be {@code null}; often {@link + * jakarta.transaction.TransactionManager#getTransaction() transactionManager::getTransaction} + * + * @param transactionSynchronizationRegistry a {@link TransactionSynchronizationRegistry}; must not be {@code null} + * + * @param interposedSynchronizations whether any {@link Synchronization}s registered by this {@link JtaConnection} + * should be registered as interposed synchronizations; see {@link + * TransactionSynchronizationRegistry#registerInterposedSynchronization(Synchronization)} and {@link + * Transaction#registerSynchronization(Synchronization)} + * + * @param exceptionConverter an {@link ExceptionConverter}; may be {@code null} + * + * @param delegate a {@link Connection} that was not sourced from an invocation of {@link + * javax.sql.XAConnection#getConnection()}; must not be {@code null} + * + * @param immediateEnlistment whether an attempt to enlist the new {@link JtaConnection} in a global transaction, if + * there is one, will be made immediately + * + * @param exceptionConverter an {@link ExceptionConverter}; may be {@code null} + * + * @exception SQLException if transaction enlistment fails or the supplied {@code delegate} {@linkplain + * Connection#isClosed() is closed} + * + * @see #JtaConnection(TransactionSupplier, TransactionSynchronizationRegistry, boolean, ExceptionConverter, + * Connection, Supplier, Consumer, boolean) + */ + JtaConnection(TransactionSupplier transactionSupplier, + TransactionSynchronizationRegistry transactionSynchronizationRegistry, + boolean interposedSynchronizations, + ExceptionConverter exceptionConverter, + Connection delegate, + boolean immediateEnlistment) + throws SQLException { + this(transactionSupplier, + transactionSynchronizationRegistry, + interposedSynchronizations, + exceptionConverter, + delegate, + null, + null, + immediateEnlistment); + } + + /** + * Creates a new {@link JtaConnection}. + * + * @param transactionSupplier a {@link TransactionSupplier}; must not be {@code null}; often {@link + * jakarta.transaction.TransactionManager#getTransaction() transactionManager::getTransaction} + * + * @param transactionSynchronizationRegistry a {@link TransactionSynchronizationRegistry}; must not be {@code null} + * + * @param interposedSynchronizations whether any {@link Synchronization}s registered by this {@link JtaConnection} + * should be registered as interposed synchronizations; see {@link + * TransactionSynchronizationRegistry#registerInterposedSynchronization(Synchronization)} and {@link + * Transaction#registerSynchronization(Synchronization)} + * + * @param exceptionConverter an {@link ExceptionConverter}; may be {@code null}; ignored if {@code + * xaResourceSupplier} is non-{@code null} + * + * @param delegate a {@link Connection} that was not sourced from an invocation of {@link + * javax.sql.XAConnection#getConnection()}; must not be {@code null} + * + * @param xaResourceSupplier a {@link SQLSupplier} of an {@link XAResource} to represent this {@link JtaConnection}; + * may be and often is {@code null} in which case a new {@link LocalXAResource} will be used instead + * + * @param immediateEnlistment whether an attempt to enlist the new {@link JtaConnection} in a global transaction, if + * there is one, will be made immediately + * + * @exception NullPointerException if {@code transactionSupplier} or {@code transactionSynchronizationRegistry} is + * {@code null} + * + * @exception SQLException if transaction enlistment fails or the supplied {@code delegate} {@linkplain + * Connection#isClosed() is closed} + * + * @see #JtaConnection(TransactionSupplier, TransactionSynchronizationRegistry, boolean, ExceptionConverter, + * Connection, Supplier, Consumer, boolean) + */ + JtaConnection(TransactionSupplier transactionSupplier, + TransactionSynchronizationRegistry transactionSynchronizationRegistry, + boolean interposedSynchronizations, + ExceptionConverter exceptionConverter, + Connection delegate, + SQLSupplier xaResourceSupplier, + boolean immediateEnlistment) + throws SQLException { + this(transactionSupplier, + transactionSynchronizationRegistry, + interposedSynchronizations, + exceptionConverter, + delegate, + xaResourceSupplier, + null, + immediateEnlistment); + } + + /** + * Creates a new {@link JtaConnection}. + * + * @param transactionSupplier a {@link TransactionSupplier}; must not be {@code null}; often {@link + * jakarta.transaction.TransactionManager#getTransaction() transactionManager::getTransaction} + * + * @param transactionSynchronizationRegistry a {@link TransactionSynchronizationRegistry}; must not be {@code null} + * + * @param interposedSynchronizations whether any {@link Synchronization}s registered by this {@link JtaConnection} + * should be registered as interposed synchronizations; see {@link + * TransactionSynchronizationRegistry#registerInterposedSynchronization(Synchronization)} and {@link + * Transaction#registerSynchronization(Synchronization)} + * + * @param exceptionConverter an {@link ExceptionConverter}; may be {@code null}; ignored if {@code + * xaResourceSupplier} is non-{@code null} + * + * @param delegate a {@link Connection} that was not sourced from an invocation of {@link + * javax.sql.XAConnection#getConnection()}; must not be {@code null} + * + * @param xaResourceSupplier a {@link SQLSupplier} of an {@link XAResource} to represent this {@link JtaConnection}; + * may be and often is {@code null} in which case a new {@link LocalXAResource} will be used instead + * + * @param xidConsumer a {@link Consumer} of {@link Xid}s that will be invoked when {@code xaResource} is {@code + * null} and a new {@link LocalXAResource} has been created and enlisted; may be {@code null}; useful mainly for + * testing + * + * @param immediateEnlistment whether an attempt to enlist the new {@link JtaConnection} in a global transaction, if + * there is one, will be made immediately + * + * @exception NullPointerException if {@code transactionSupplier} or {@code transactionSynchronizationRegistry} is + * {@code null} + * + * @exception SQLException if transaction enlistment fails or the supplied {@code delegate} {@linkplain + * Connection#isClosed() is closed} + */ + JtaConnection(TransactionSupplier transactionSupplier, + TransactionSynchronizationRegistry transactionSynchronizationRegistry, + boolean interposedSynchronizations, + ExceptionConverter exceptionConverter, + Connection delegate, + SQLSupplier xaResourceSupplier, + Consumer xidConsumer, + boolean immediateEnlistment) + throws SQLException { + super(delegate, + true, // closeable + true); // strict isClosed checking; always a good thing + this.ts = Objects.requireNonNull(transactionSupplier, "transactionSupplier"); + this.tsr = Objects.requireNonNull(transactionSynchronizationRegistry, "transactionSynchronizationRegistry"); + if (delegate.isClosed()) { + throw new SQLNonTransientConnectionException("delegate is closed", CONNECTION_EXCEPTION_NO_SUBCLASS); + } + this.interposedSynchronizations = interposedSynchronizations; + this.xaResourceSupplier = + xaResourceSupplier == null + ? () -> new LocalXAResource(this::connectionFunction, exceptionConverter) + : xaResourceSupplier; + this.xidConsumer = xidConsumer == null ? JtaConnection::sink : xidConsumer; + if (immediateEnlistment) { + this.enlist(); + } + } + + + /* + * Instance methods. + */ + + + @Override // ConditionallyCloseableConnection + public final void setCloseable(boolean closeable) { + super.setCloseable(closeable); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "setCloseable", closeable); + LOGGER.exiting(this.getClass().getName(), "setCloseable"); + } + } + + @Override // ConditionallyCloseableConnection + public final Statement createStatement() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createStatement(); + } + + @Override // ConditionallyCloseableConnection + public final PreparedStatement prepareStatement(String sql) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareStatement(sql); + } + + @Override // ConditionallyCloseableConnection + public final CallableStatement prepareCall(String sql) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareCall(sql); + } + + @Override // ConditionallyCloseableConnection + public final String nativeSQL(String sql) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.nativeSQL(sql); + } + + @Override // ConditionallyCloseableConnection + public final void setAutoCommit(boolean autoCommit) throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (autoCommit && this.enlisted()) { + // "SQLException...if...setAutoCommit(true) is called while participating in a distributed transaction" + throw new SQLNonTransientException("Connection enlisted in transaction", INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + super.setAutoCommit(autoCommit); + } + + @Override // ConditionallyCloseableConnection + public final boolean getAutoCommit() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getAutoCommit(); + } + + @Override // ConditionallyCloseableConnection + public final void commit() throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (this.enlisted()) { + // "SQLException...if...this method is called while participating in a distributed transaction" + throw new SQLNonTransientException("Connection enlisted in transaction", INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + super.commit(); + } + + @Override // ConditionallyCloseableConnection + public final void rollback() throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (this.enlisted()) { + // "SQLException...if...this method is called while participating in a distributed transaction" + throw new SQLNonTransientException("Connection enlisted in transaction", INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + super.rollback(); + } + + @Override // ConditionallyCloseableConnection + public final DatabaseMetaData getMetaData() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getMetaData(); + } + + @Override // ConditionallyCloseableConnection + public final void setReadOnly(boolean readOnly) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setReadOnly(readOnly); + } + + @Override // ConditionallyCloseableConnection + public final boolean isReadOnly() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.isReadOnly(); + } + + @Override // ConditionallyCloseableConnection + public final void setCatalog(String catalog) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setCatalog(catalog); + } + + @Override // ConditionallyCloseableConnection + public final String getCatalog() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getCatalog(); + } + + @Override // ConditionallyCloseableConnection + public final void setTransactionIsolation(int level) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setTransactionIsolation(level); + } + + @Override // ConditionallyCloseableConnection + public final int getTransactionIsolation() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getTransactionIsolation(); + } + + @Override // ConditionallyCloseableConnection + public final Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createStatement(resultSetType, resultSetConcurrency); + } + + @Override // ConditionallyCloseableConnection + public final PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareStatement(sql, resultSetType, resultSetConcurrency); + } + + @Override // ConditionallyCloseableConnection + public final CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareCall(sql, resultSetType, resultSetConcurrency); + } + + @Override // ConditionallyCloseableConnection + public final Map> getTypeMap() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getTypeMap(); + } + + @Override // ConditionallyCloseableConnection + public final void setTypeMap(Map> map) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setTypeMap(map); + } + + @Override // ConditionallyCloseableConnection + public final void setHoldability(int holdability) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setHoldability(holdability); + } + + @Override // ConditionallyCloseableConnection + public final int getHoldability() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getHoldability(); + } + + @Override // ConditionallyCloseableConnection + public final Savepoint setSavepoint() throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (this.enlisted()) { + // "SQLException...if...this method is called while participating in a distributed transaction" + throw new SQLNonTransientException("Connection enlisted in transaction", PROHIBITED_SAVEPOINT_OPERATION); + } + return super.setSavepoint(); + } + + @Override // ConditionallyCloseableConnection + public final Savepoint setSavepoint(String name) throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (this.enlisted()) { + // "SQLException...if...this method is called while participating in a distributed transaction" + throw new SQLNonTransientException("Connection enlisted in transaction", PROHIBITED_SAVEPOINT_OPERATION); + } + return super.setSavepoint(name); + } + + @Override // ConditionallyCloseableConnection + public final void rollback(Savepoint savepoint) throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (this.enlisted()) { + // "SQLException...if...this method is called while participating in a distributed transaction" + throw new SQLNonTransientException("Connection enlisted in transaction", PROHIBITED_SAVEPOINT_OPERATION); + } + super.rollback(savepoint); + } + + @Override // ConditionallyCloseableConnection + public final void releaseSavepoint(Savepoint savepoint) throws SQLException { + this.failWhenClosed(); + this.enlist(); + if (this.enlisted()) { + // "SQLException...if...the given Savepoint object is not a valid savepoint in the current transaction" + // + // Interestingly JDBC doesn't mandate an exception being thrown here if the connection is enlisted in a + // global transaction, but it looks like a SQL state such as 3B503 is often thrown in this case. + throw new SQLNonTransientException("Connection enlisted in transaction", PROHIBITED_SAVEPOINT_OPERATION); + } + super.releaseSavepoint(savepoint); + } + + @Override // ConditionallyCloseableConnection + public final Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override // ConditionallyCloseableConnection + public final PreparedStatement prepareStatement(String sql, int rsType, int rsConcurrency, int rsHoldability) + throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareStatement(sql, rsType, rsConcurrency, rsHoldability); + } + + @Override // ConditionallyCloseableConnection + public final CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + + @Override // ConditionallyCloseableConnection + public final PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareStatement(sql, autoGeneratedKeys); + } + + @Override // ConditionallyCloseableConnection + public final PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareStatement(sql, columnIndexes); + } + + @Override // ConditionallyCloseableConnection + public final PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.prepareStatement(sql, columnNames); + } + + @Override // ConditionallyCloseableConnection + public final Clob createClob() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createClob(); + } + + @Override // ConditionallyCloseableConnection + public final Blob createBlob() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createBlob(); + } + + @Override // ConditionallyCloseableConnection + public final NClob createNClob() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createNClob(); + } + + @Override // ConditionallyCloseableConnection + public final SQLXML createSQLXML() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createSQLXML(); + } + + @Override // ConditionallyCloseableConnection + public final boolean isValid(int timeout) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.isValid(timeout); + } + + @Override // ConditionallyCloseableConnection + public final void setClientInfo(String name, String value) throws SQLClientInfoException { + try { + this.failWhenClosed(); + this.enlist(); + super.setClientInfo(name, value); + } catch (SQLClientInfoException e) { + throw e; + } catch (SQLException e) { + throw new SQLClientInfoException(e.getMessage(), e.getSQLState(), e.getErrorCode(), Map.of(), e); + } + } + + @Override // ConditionallyCloseableConnection + public final void setClientInfo(Properties properties) throws SQLClientInfoException { + try { + this.failWhenClosed(); + this.enlist(); + super.setClientInfo(properties); + } catch (SQLClientInfoException e) { + throw e; + } catch (SQLException e) { + throw new SQLClientInfoException(e.getMessage(), e.getSQLState(), e.getErrorCode(), Map.of(), e); + } + } + + @Override // ConditionallyCloseableConnection + public final String getClientInfo(String name) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getClientInfo(name); + } + + @Override // ConditionallyCloseableConnection + public final Properties getClientInfo() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getClientInfo(); + } + + @Override // ConditionallyCloseableConnection + public final Array createArrayOf(String typeName, Object[] elements) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createArrayOf(typeName, elements); + } + + @Override // ConditionallyCloseableConnection + public final Struct createStruct(String typeName, Object[] attributes) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.createStruct(typeName, attributes); + } + + @Override // ConditionallyCloseableConnection + public final void setSchema(String schema) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setSchema(schema); + } + + @Override // ConditionallyCloseableConnection + public final String getSchema() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getSchema(); + } + + @Override // ConditionallyCloseableConnection + public final void abort(Executor executor) throws SQLException { + // this.enlist(); // Deliberately omitted, but not by spec. + + // NOTE + // + // abort(Executor) is a method that seems to be designed for an administrator, and so even if there is a + // transaction in progress we probably should allow closing. + // + // TO DO: should we heuristically roll back? Purge the Xid? + this.setCloseable(true); + super.abort(executor); + } + + @Override // ConditionallyCloseableConnection + public final void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setNetworkTimeout(executor, milliseconds); + } + + @Override // ConditionallyCloseableConnection + public final int getNetworkTimeout() throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.getNetworkTimeout(); + } + + @Override // ConditionallyCloseableConnection + public final void beginRequest() throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.beginRequest(); + } + + @Override // ConditionallyCloseableConnection + public final void endRequest() throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.endRequest(); + } + + @Override // ConditionallyCloseableConnection + public final boolean setShardingKeyIfValid(ShardingKey shardingKey, ShardingKey superShardingKey, int timeout) + throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.setShardingKeyIfValid(shardingKey, superShardingKey, timeout); + } + + @Override // ConditionallyCloseableConnection + public final boolean setShardingKeyIfValid(ShardingKey shardingKey, int timeout) throws SQLException { + this.failWhenClosed(); + this.enlist(); + return super.setShardingKeyIfValid(shardingKey, timeout); + } + + @Override // ConditionallyCloseableConnection + public final void setShardingKey(ShardingKey shardingKey, ShardingKey superShardingKey) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setShardingKey(shardingKey, superShardingKey); + } + + @Override // ConditionallyCloseableConnection + public final void setShardingKey(ShardingKey shardingKey) throws SQLException { + this.failWhenClosed(); + this.enlist(); + super.setShardingKey(shardingKey); + } + + @Override // ConditionallyCloseableConnection + public final void close() throws SQLException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "close"); + } + // The JTA Specification, section 4.2, has a non-normative diagram illustrating that close() is expected, + // but not required, to call Transaction#delistResource(XAResource). This is, mind you, before the + // prepare/commit completion cycle has started. + Enlistment enlistment = this.enlistment; // volatile read + if (enlistment != null) { + try { + // TMSUCCESS because it's an ordinary close() call, not a delisting due to an exception + boolean delisted = enlistment.transaction().delistResource(enlistment.xaResource(), TMSUCCESS); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, + this.getClass().getName(), "close", + "{0} {1} from {2}", + new Object[] {delisted ? "Delisted" : "Failed to delist", + enlistment.xaResource(), + enlistment.transaction()}); + } + } catch (IllegalStateException e) { + // Transaction went from active or marked for rollback to some other state; whatever; we're no longer + // enlisted so we didn't delist. + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "close", e.getMessage(), e); + } + } catch (SystemException e) { + throw new SQLTransientException(e.getMessage(), INVALID_TRANSACTION_STATE_NO_SUBCLASS, e); + } + } + super.close(); + if (LOGGER.isLoggable(Level.FINE) && !this.isClosePending()) { + // If a close is not pending then that means it actually happened. + LOGGER.logp(Level.FINE, this.getClass().getName(), "close", + "Closed {0} on thread {1}", new Object[] {this, Thread.currentThread()}); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "close"); + } + } + + @Override // Object + public final int hashCode() { + return System.identityHashCode(this); + } + + @Override // Object + public final boolean equals(Object other) { + return this == other; + } + + /** + * Returns {@code true} if a JTA transaction exists and {@linkplain + * TransactionSynchronizationRegistry#getTransactionStatus() has a status} equal to either {@link + * Status#STATUS_ACTIVE} or {@link Status#STATUS_MARKED_ROLLBACK}. + * + * @return {@code true} if a JTA transaction exists and {@linkplain + * TransactionSynchronizationRegistry#getTransactionStatus() has a status} equal to either {@link + * Status#STATUS_ACTIVE} or {@link Status#STATUS_MARKED_ROLLBACK}; {@code false} in all other cases + * + * @exception SQLException if the status could not be acquired + * + * @see TransactionSynchronizationRegistry#getTransactionStatus() + * + * @see Status + */ + private boolean activeOrMarkedRollbackTransaction() throws SQLException { + switch (this.transactionStatus()) { + // See https://www.eclipse.org/lists/jta-dev/msg00264.html and + // https://github.com/jakartaee/transactions/issues/211. + case Status.STATUS_ACTIVE: + case Status.STATUS_MARKED_ROLLBACK: + return true; + default: + return false; + } + } + + private int transactionStatus() throws SQLException { + try { + return this.tsr.getTransactionStatus(); + } catch (RuntimeException e) { + // See + // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionSynchronizationRegistryImple.java#L153-L164; + // despite it not being documented, TCK-passing implementations of + // TransactionSynchronizationRegistry#getTransactionStatus() can apparently throw RuntimeException. Since + // getTransactionStatus() is specified to return "the result of executing TransactionManager.getStatus() in + // the context of the transaction bound to the current thread at the time this method is called", it follows + // that possible SystemExceptions thrown by TransactionManager#getStatus() implementations will have to be + // dealt with in *some* way, even though the javadoc for + // TransactionSynchronizationRegistry#getTransactionStatus() does not account for such a thing. + throw new SQLTransientException(e.getMessage(), INVALID_TRANSACTION_STATE_NO_SUBCLASS, e); + } + } + + /** + * Returns {@code true} if and only if this {@link JtaConnection} is associated with a JTA transaction whose + * {@linkplain Transaction#getStatus() status} is one of {@link Status#STATUS_ACTIVE} or {@link + * Status#STATUS_MARKED_ROLLBACK} as a result of a prior {@link #enlist()} invocation on the current thread. + * + * @return {@code true} if and only if this {@link JtaConnection} is associated with a {@linkplain + * #activeOrMarkedRollbackTransaction() JTA transaction whose status is known and not yet prepared}; {@code false} + * in all other cases + * + * @exception SQLException if the enlisted status could not be acquired + */ + boolean enlisted() throws SQLException { + Enlistment enlistment = this.enlistment; // volatile read + if (enlistment == null) { + return false; + } else if (enlistment.threadId() != Thread.currentThread().getId()) { + // We're enlisted in a Transaction on thread 1, and a caller from thread 2 is trying to do something with + // us. This could have unintended side effects. Throw. + throw new SQLTransientException("Already enlisted (" + enlistment + "); current thread id: " + + Thread.currentThread().getId(), INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + + // We are, or were, enlisted. Let's see in what way. + + // We're enlisted in a Transaction that was created on the current thread. So far so good. Is it active? + Transaction t = enlistment.transaction(); + int transactionStatus = statusFrom(t); + switch (transactionStatus) { + case Status.STATUS_ACTIVE: + // We have been enlisted in an active transaction that was created on this thread. Is the current + // transaction, whatever it is, active? + int currentThreadTransactionStatus = this.transactionStatus(); + switch (currentThreadTransactionStatus) { + case Status.STATUS_ACTIVE: + // We have been enlisted in an active transaction that was created on this thread AND the current + // thread's transaction status is ALSO active. Is the current thread's transaction equal to the one + // we're enlisted in? Or is it a different one (in which case our transaction has been suspended)? + if (t.equals(this.transaction())) { + // The Transaction associated with the current thread is the active one we're enlisted with, so + // we're already enlisted and everything is fine. (Equality is governed by the spec: + // https://jakarta.ee/specifications/transactions/2.0/jakarta-transactions-spec-2.0.html#transaction-equality-and-hash-code) + return true; + } + throw new SQLTransientException("Attempting to perform work while associated with a suspended transaction", + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + case Status.STATUS_COMMITTED: + case Status.STATUS_ROLLEDBACK: + case Status.STATUS_COMMITTING: + case Status.STATUS_MARKED_ROLLBACK: + case Status.STATUS_PREPARED: + case Status.STATUS_PREPARING: + case Status.STATUS_ROLLING_BACK: + case Status.STATUS_NO_TRANSACTION: + // The current thread's transaction status is not active, so it can't be the same transaction with + // which we are enlisted, so by definition we're enlisted in a suspended transaction and no further + // work should be done until that transaction is resumed. + throw new SQLTransientException("Attempting to perform work while associated with a suspended transaction", + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + case Status.STATUS_UNKNOWN: + default: + // Unexpected or illegal. Throw. + throw new SQLTransientException("Unexpected transaction status: " + currentThreadTransactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + case Status.STATUS_COMMITTED: + case Status.STATUS_ROLLEDBACK: + // We *were* enlisted, and sort of still are (there's a non-null Enlistment), but now the transaction is + // irrevocably completed, and the non-null Enlistment will be removed momentarily by another thread + // executing our transactionCompleted(int) method, so really for all intents and purposes we're no + // longer enlisted. + return false; + case Status.STATUS_COMMITTING: + case Status.STATUS_MARKED_ROLLBACK: + case Status.STATUS_PREPARED: + case Status.STATUS_PREPARING: + case Status.STATUS_ROLLING_BACK: + // Interim or effectively interim. Throw to prevent accidental side effects. + throw new SQLTransientException("Non-terminal transaction status: " + transactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + case Status.STATUS_NO_TRANSACTION: + // Somehow an Enlistment was created with a Transaction in the Status.STATUS_NO_TRANSACTION status. This + // should absolutely never happen. + throw new AssertionError(); + case Status.STATUS_UNKNOWN: + default: + // Unexpected or illegal. Throw. + throw new SQLTransientException("Unexpected transaction status: " + transactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + } + + private Transaction transaction() throws SQLException { + try { + return this.ts.getTransaction(); + } catch (RuntimeException | SystemException e) { + throw new SQLTransientException(e.getMessage(), INVALID_TRANSACTION_STATE_NO_SUBCLASS, e); + } + } + + /** + * Attempts to enlist this {@link JtaConnection} in the current JTA transaction, if there is one, and its status is + * {@link Status#STATUS_ACTIVE}, and this {@link JtaConnection} is not already {@linkplain #enlisted() enlisted}. + * + * @exception SQLException if a transaction-related error occurs, or if the return value of an invocation of the + * {@link #getAutoCommit()} method returns {@code false} + * + * @see #enlisted() + */ + @SuppressWarnings("checkstyle:MethodLength") + private void enlist() throws SQLException { + + if (this.enlisted()) { + return; + } + + int currentThreadTransactionStatus = this.transactionStatus(); + switch (currentThreadTransactionStatus) { + case Status.STATUS_ACTIVE: + // There is a global transaction currently active on the current thread. That's good. Keep going. + break; + case Status.STATUS_COMMITTED: + case Status.STATUS_NO_TRANSACTION: + case Status.STATUS_ROLLEDBACK: + // There is no global transaction or there is very shortly about to be no global transaction on the current + // thread; this is an effectively terminal state. Enlistment is impossible or silly. Very common. Return + // without enlisting. + return; + case Status.STATUS_COMMITTING: + case Status.STATUS_MARKED_ROLLBACK: + case Status.STATUS_PREPARED: + case Status.STATUS_PREPARING: + case Status.STATUS_ROLLING_BACK: + // Interim or effectively interim state. Uncommon. Throw to prevent accidental side effects. + throw new SQLTransientException("Non-terminal current thread transaction status: " + currentThreadTransactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + case Status.STATUS_UNKNOWN: + default: + // Unexpected or illegal state. Throw. + throw new SQLTransientException("Unexpected current thread transaction status: " + currentThreadTransactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + + if (!super.getAutoCommit()) { + // There is, as far as we can tell, an active global transaction on the current thread, and + // super.getAutoCommit() (super. on purpose, not this.) returned false, and we aren't (yet) enlisted with + // the active global transaction, so autoCommit must have been disabled on purpose by the caller, not by the + // transaction enlistment machinery. In such a case, we don't want to permit enlistment, because a local + // transaction may be in progress and we don't want to have its effects mixed in. + throw new SQLTransientException("autoCommit was false during active transaction enlistment", + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + + Transaction t = this.transaction(); + int transactionStatus = statusFrom(t); + switch (transactionStatus) { + case Status.STATUS_ACTIVE: + // No one has started or finished the transaction completion process yet. Most common. Keep going. + break; + case Status.STATUS_COMMITTED: + case Status.STATUS_ROLLEDBACK: + // Terminal. Return without enlisting. + return; + case Status.STATUS_COMMITTING: + case Status.STATUS_MARKED_ROLLBACK: + case Status.STATUS_PREPARED: + case Status.STATUS_PREPARING: + case Status.STATUS_ROLLING_BACK: + // Interim or effectively interim. Throw to prevent accidental side effects. + throw new SQLTransientException("Non-terminal transaction status: " + transactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + case Status.STATUS_NO_TRANSACTION: + // Impossible state machine transition since t is non-null and at least at one earlier point on this thread + // the status was Status.STATUS_ACTIVE. Even if somehow the global transaction is disassociated from the + // current thread, the Transaction object's status will never, by spec, go to + // Status.STATUS_NO_TRANSACTION. See + // https://groups.google.com/g/narayana-users/c/eYVUmhE9QZg/m/xbBh2CsBBQAJ. + throw new AssertionError(); // per spec + case Status.STATUS_UNKNOWN: + default: + // Unexpected or illegal. Throw. + throw new SQLTransientException("Unexpected transaction status: " + transactionStatus, + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + + // One last check to see if we've been actually closed or requested to close asynchronously. + this.failWhenClosed(); + + // Point of no return. We ensured that the Transaction at one point had a status of Status.STATUS_ACTIVE and + // ensured we aren't already enlisted and our autoCommit status is true. The Transaction's status can still + // change at any point (as a result of asynchronous rollback, for example) through certain permitted state + // transitions, so we have to watch for exceptions. + + XAResource xar = this.xaResourceSupplier.get(); + if (xar == null) { + throw new SQLTransientException("xaResourceSupplier.get() == null"); + } + + Enlistment enlistment = new Enlistment(Thread.currentThread().getId(), t, xar); + if (!ENLISTMENT.compareAndSet(this, null, enlistment)) { // atomic volatile write + // Setting this.enlistment could conceivably fail if another thread already enlisted this JtaConnection. + // That would be bad. + // + // (The this.enlistment read in the exception message is a volatile read, and thanks to the compareAndSet + // call above we know it is non-null.) + throw new SQLTransientException("Already enlisted (" + this.enlistment + + "); current transaction: " + t + + "; current thread id: " + Thread.currentThread().getId(), + INVALID_TRANSACTION_STATE_NO_SUBCLASS); + } + + try { + // The XAResource is placed into the TransactionSynchronizationRegistry so that it can be delisted if + // appropriate during invocation of the close() method (q.v). We do it before the actual + // Transaction#enlistResource(XAResource) call on purpose. + if (this.interposedSynchronizations) { + this.tsr.registerInterposedSynchronization((Sync) this::transactionCompleted); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "connectionFunction", + "Registered interposed synchronization (transactionCompleted(int)) for {0}", t); + } + } else { + t.registerSynchronization((Sync) this::transactionCompleted); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "connectionFunction", + "Registered synchronization (transactionCompleted(int)) for {0}", t); + } + } + // Don't let our caller close us "for real". close() invocations will be recorded as pending. See + // #close(). (Note that this is "undone" after transaction completion in #transactionCompleted(int), which + // was just registered as a synchronization immediately above.) + this.setCloseable(false); + // (Guaranteed to call xar.start(Xid, int) on this thread.) + t.enlistResource(xar); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, + this.getClass().getName(), "enlist", + "Enlisted {0} in transaction {1}", new Object[] {xar, t}); + } + } catch (RollbackException e) { + this.enlistment = null; // volatile write + // The enlistResource(XAResource) or registerSynchronization(Synchronization) operation failed because the + // transaction was rolled back. + throw new SQLNonTransientException(e.getMessage(), TRANSACTION_ROLLBACK, e); + } catch (RuntimeException | SystemException e) { + this.enlistment = null; // volatile write + if (e.getCause() instanceof RollbackException) { + // The enlistResource(XAResource) operation failed because the transaction was rolled back. + throw new SQLNonTransientException(e.getMessage(), TRANSACTION_ROLLBACK, e); + } + // The t.enlistResource(XAResource) operation failed, or the + // tsr.registerInterposedSynchronization(Synchronization) operation failed, or the + // t.registerSynchronization(Synchronization) operation failed. In any case, no XAResource was actually + // enlisted. + throw new SQLTransientException(e.getMessage(), INVALID_TRANSACTION_STATE_NO_SUBCLASS, e); + } catch (Error e) { + this.enlistment = null; // volatile write + throw e; + } + } + + // (Used only by reference by LocalXAResource#start(Xid, int) as a result of calling + // Transaction#enlistResource(XAResource) in enlist() above.) + private Connection connectionFunction(Xid xid) { + this.xidConsumer.accept(xid); + return this.delegate(); + } + + // (Used only by reference in enlist() above. Remember, this callback may be called by the TransactionManager on + // any thread at any time for any reason.) + private void transactionCompleted(int commitedOrRolledBack) { + this.enlistment = null; // volatile write + try { + boolean closeWasPending = this.isClosePending(); + this.setCloseable(true); + assert this.isCloseable(); + assert !this.isClosePending(); + if (closeWasPending) { + assert !this.isClosed(); + assert !this.delegate().isClosed(); + this.close(); + assert this.isClosed(); + assert this.delegate().isClosed(); + assert !this.isCloseable(); + assert !this.isClosePending(); + } + } catch (SQLException e) { + // (Synchronization implementations can throw only unchecked exceptions.) + throw new UncheckedSQLException(e); + } + } + + + /* + * Static methods. + */ + + + private static int statusFrom(Transaction t) throws SQLException { + Objects.requireNonNull(t, "t"); + try { + return t.getStatus(); + } catch (RuntimeException | SystemException e) { + throw new SQLTransientException(e.getMessage(), INVALID_TRANSACTION_STATE_NO_SUBCLASS, e); + } + } + + private static void sink(Object ignored) { + + } + + + /* + * Inner and nested classes. + */ + + + /** + * A functional {@link Synchronization}. + * + * @see Synchronization + */ + @FunctionalInterface + interface Sync extends Synchronization { + + /** + * Called prior to the start of the two-phase transaction commit process. + * + *

The default implementation of this method does nothing.

+ * + * @see Synchronization#beforeCompletion() + */ + default void beforeCompletion() { + + } + + } + + private static final record Enlistment(long threadId, Transaction transaction, XAResource xaResource) { + + private Enlistment { + Objects.requireNonNull(transaction, "transaction"); + Objects.requireNonNull(xaResource, "xaResource"); + } + + } + +} diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaDataSource.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaDataSource.java index 9878b8461a0..fa5f8e1f243 100644 --- a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaDataSource.java +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/JtaDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,24 +34,27 @@ import jakarta.transaction.Synchronization; /** - * An {@link AbstractDataSource} and a {@link Synchronization} that - * wraps another {@link DataSource} that is known to not behave - * correctly in the presence of JTA transaction management, such as - * one supplied by any of several freely and commercially available - * connection pools, and that makes such a non-JTA-aware {@link - * DataSource} behave as sensibly as possible in the presence of a - * JTA-managed transaction. + * An {@link AbstractDataSource} and a {@link Synchronization} that wraps another {@link DataSource} that is known to + * not behave correctly in the presence of JTA transaction management, such as one supplied by any of several freely and + * commercially available connection pools, and that makes such a non-JTA-aware {@link DataSource} behave as sensibly as + * possible in the presence of a JTA-managed transaction. * *

Thread Safety

* - *

Instances of this class are safe for concurrent use by multiple - * threads. No such guarantee obviously can be made about the {@link - * DataSource} wrapped by any given instance of this class.

+ *

Instances of this class are safe for concurrent use by multiple threads. No such guarantee obviously can be made + * about the {@link DataSource} wrapped by any given instance of this class.

* - *

Note that the JDBC specification places no requirement on any - * implementor to make any implementations of any JDBC constructs - * thread-safe.

+ *

Note that the JDBC specification places no requirement on any implementor to make any implementations of any JDBC + * constructs thread-safe. Nevertheless, a certain amount of unspecified thread safety must exist in all JDBC + * implementations or their constructs could never be enrolled in JTA-compliant transactions.

+ * + * @deprecated This class is slated for removal. It makes incorrect assumptions about threading in a JTA environment. + * Specifically, this class' implementation incorrectly assumes that the {@link #afterCompletion(int)} method will be + * invoked on the same thread as the governing transaction, which is not necessarily the case, especially in the case of + * asynchronous rollbacks. As a result, {@link Connection}s acquired by instances of this class may not be closed + * properly. */ +@Deprecated(forRemoval = true, since = "3.0.3") public final class JtaDataSource extends AbstractDataSource implements Synchronization { @@ -63,7 +66,7 @@ public final class JtaDataSource extends AbstractDataSource implements Synchroni private static final Object UNAUTHENTICATED_CONNECTION_IDENTIFIER = new Object(); private static final ThreadLocal>> CONNECTIONS_TL = - ThreadLocal.withInitial(() -> new HashMap<>()); + ThreadLocal.withInitial(HashMap::new); /* @@ -84,39 +87,33 @@ public final class JtaDataSource extends AbstractDataSource implements Synchroni /** * Creates a new {@link JtaDataSource}. * - * @param dataSource the {@link DataSource} instance to which - * operations will be delegated; must not be {@code null} + * @param dataSource the {@link DataSource} instance to which operations will be delegated; must not be {@code null} * - * @param transactionIsActiveSupplier a {@link BooleanSupplier} - * that returns {@code true} only if the current transaction, if - * any, is active; must not be {@code null} + * @param transactionIsActiveSupplier a {@link BooleanSupplier} that returns {@code true} only if the current + * transaction, if any, is active; must not be {@code null} * - * @exception NullPointerException if either parameter is {@code - * null} + * @exception NullPointerException if either parameter is {@code null} * * @see #JtaDataSource(Supplier, BooleanSupplier) */ - public JtaDataSource(final DataSource dataSource, - final BooleanSupplier transactionIsActiveSupplier) { + public JtaDataSource(DataSource dataSource, + BooleanSupplier transactionIsActiveSupplier) { this(() -> dataSource, transactionIsActiveSupplier); } /** * Creates a new {@link JtaDataSource}. * - * @param delegateSupplier a {@link Supplier} of {@link - * DataSource} instances to which operations will be delegated; + * @param delegateSupplier a {@link Supplier} of {@link DataSource} instances to which operations will be delegated; * must not be {@code null} * - * @param transactionIsActiveSupplier an {@link BooleanSupplier} that - * returns {@code true} only if the current transaction, if any, is - * active; must not be {@code null} + * @param transactionIsActiveSupplier an {@link BooleanSupplier} that returns {@code true} only if the current + * transaction, if any, is active; must not be {@code null} * - * @exception NullPointerException if either parameter is {@code - * null} + * @exception NullPointerException if either parameter is {@code null} */ - public JtaDataSource(final Supplier delegateSupplier, - final BooleanSupplier transactionIsActiveSupplier) { + public JtaDataSource(Supplier delegateSupplier, + BooleanSupplier transactionIsActiveSupplier) { super(); this.delegateSupplier = Objects.requireNonNull(delegateSupplier, "delegateSupplier"); this.transactionIsActiveSupplier = Objects.requireNonNull(transactionIsActiveSupplier, "transactionIsActiveSupplier"); @@ -129,30 +126,27 @@ public JtaDataSource(final Supplier delegateSupplier, /** - * If there is an active transaction, registers this {@link - * JtaDataSource} with the supplied registrar, which is most - * commonly—but is not required to be—a reference to - * the {@link + * If there is an active transaction, registers this {@link JtaDataSource} with the supplied registrar, which is + * most commonly—but is not required to be—a reference to the {@link * jakarta.transaction.TransactionSynchronizationRegistry#registerInterposedSynchronization(Synchronization)} * method. * *

If there is no currently active transaction, no action is taken.

* - * @param registrar a {@link Consumer} that may {@linkplain - * Consumer#accept(Object) accept} this {@link JtaDataSource} if - * there is a currently active transaction; must not be {@code - * null} + * @param registrar a {@link Consumer} that may {@linkplain Consumer#accept(Object) accept} this {@link + * JtaDataSource} if there is a currently active transaction; must not be {@code null} * - * @return {@code true} if registration occurred; {@code false} - * otherwise + * @return {@code true} if registration occurred; {@code false} otherwise * * @exception NullPointerException if {@code registrar} is {@code null} * - * @exception RuntimeException if the supplied {@code registrar}'s - * {@link Consumer#accept(Object) accept} method throws a {@link - * RuntimeException} + * @exception RuntimeException if the supplied {@code registrar}'s {@link Consumer#accept(Object) accept} method + * throws a {@link RuntimeException} + * + * @deprecated This method is slated for removal with no replacement. */ - public boolean registerWith(final Consumer registrar) { + @Deprecated(forRemoval = true) + public boolean registerWith(Consumer registrar) { if (this.transactionIsActiveSupplier.getAsBoolean()) { registrar.accept(this); return true; @@ -161,8 +155,7 @@ public boolean registerWith(final Consumer registrar) { } /** - * Implements the {@link Synchronization#beforeCompletion()} - * method to do nothing. + * Implements the {@link Synchronization#beforeCompletion()} method to do nothing. */ @Override // Synchronization public void beforeCompletion() { @@ -170,28 +163,23 @@ public void beforeCompletion() { } /** - * Ensures that any thread-associated connections are properly - * committed, restored to their initial states, closed where - * appropriate and removed from the system when a definitionally - * thread-scoped JTA transaction commits or rolls back. + * Ensures that any thread-associated connections are properly committed, restored to their initial states, closed + * where appropriate and removed from the system when a definitionally thread-scoped JTA transaction commits or + * rolls back. * - * @param status the status of the transaction after completion; - * must be either {@link Status#STATUS_COMMITTED} or {@link - * Status#STATUS_ROLLEDBACK} + * @param status the status of the transaction after completion; must be either {@link Status#STATUS_COMMITTED} or + * {@link Status#STATUS_ROLLEDBACK} * - * @exception IllegalArgumentException if {@code status} is - * neither {@link Status#STATUS_COMMITTED} nor {@link + * @exception IllegalArgumentException if {@code status} is neither {@link Status#STATUS_COMMITTED} nor {@link * Status#STATUS_ROLLEDBACK} */ @Override // Synchronization - public void afterCompletion(final int status) { - // Validate the status coming in, but make sure that no matter - // what we remove any transaction-specific connections from - // the ThreadLocal storing such connections. Doing this right - // is the reason for deferring any throwing of an - // IllegalArgumentException in invalid cases. - final IllegalArgumentException badStatusException; - final CheckedConsumer consumer; + public void afterCompletion(int status) { + // Validate the status coming in, but make sure that no matter what we remove any transaction-specific + // connections from the ThreadLocal storing such connections. Doing this right is the reason for deferring any + // throwing of an IllegalArgumentException in invalid cases. + IllegalArgumentException badStatusException; + CheckedConsumer consumer; switch (status) { case Status.STATUS_COMMITTED: badStatusException = null; @@ -207,17 +195,16 @@ public void afterCompletion(final int status) { break; } - // Get all of the TransactionSpecificConnections we have - // released into the world via our getConnection() and - // getConnection(String, String) methods, and inform them that - // the transaction is over. Then remove them from the map - // since the transaction is over. These particular - // Connections out in the world will not participate in future - // JTA transactions, even if such transactions are started on - // this thread. - final Map extantConnectionsMap = CONNECTIONS_TL.get().get(this); + // Get all of the TransactionSpecificConnections we have released into the world via our getConnection() and + // getConnection(String, String) methods, and inform them that the transaction is over. Then remove them from + // the map since the transaction is over. These particular Connections out in the world will not participate in + // future JTA transactions, even if such transactions are started on this thread. + // + // TO DO: this assumes that afterCompletion(int) will be called on the same thread as the transaction. This is + // not the case. An asynchronous rollback is possible, and in this case, this thread local map will be empty. + Map extantConnectionsMap = CONNECTIONS_TL.get().get(this); if (extantConnectionsMap != null) { - final Collection extantConnections = extantConnectionsMap.values(); + Collection extantConnections = extantConnectionsMap.values(); try { if (badStatusException == null) { complete(extantConnections, consumer); @@ -231,58 +218,45 @@ public void afterCompletion(final int status) { } /** - * Given an {@link Iterable} of {@link - * TransactionSpecificConnection} instances and a {@link - * CheckedConsumer} of {@link Connection} instances, ensures that - * the {@link CheckedConsumer#accept(Object)} method is - * invoked on each reachable {@link - * TransactionSpecificConnection}, properly handling all - * exceptional conditions. - * - *

The {@link TransactionSpecificConnection} instances will - * have their auto-commit statuses reset and their closeable - * statuses set to {@code true}, even in the presence of - * exceptional conditions.

- * - *

The {@link TransactionSpecificConnection}s will also be - * closed if a caller has requested their closing prior to this - * method executing.

- * - *

If a user has not requested their closing prior to this - * method executing, the {@link TransactionSpecificConnection}s - * will not be closed, but will become closeable by the end user - * (allowing them to be released back to any backing connection - * pool that might exist). They will no longer take part in any - * new JTA transactions from this point forward (a new {@link - * Connection} will have to be acquired while a JTA transaction is - * active for that behavior).

- * - * @param connections an {@link Iterable} of {@link - * TransactionSpecificConnection} instances; must not be {@code + * Given an {@link Iterable} of {@link TransactionSpecificConnection} instances and a {@link CheckedConsumer} of + * {@link Connection} instances, ensures that the {@link CheckedConsumer#accept(Object)} method is invoked on each + * reachable {@link TransactionSpecificConnection}, properly handling all exceptional conditions. + * + *

The {@link TransactionSpecificConnection} instances will have their auto-commit statuses reset and their + * closeable statuses set to {@code true}, even in the presence of exceptional conditions.

+ * + *

The {@link TransactionSpecificConnection}s will also be closed if a caller has requested their closing prior + * to this method executing.

+ * + *

If a user has not requested their closing prior to this method executing, the {@link + * TransactionSpecificConnection}s will not be closed, but will become closeable by the end user (allowing them to + * be released back to any backing connection pool that might exist). They will no longer take part in any new JTA + * transactions from this point forward (a new {@link Connection} will have to be acquired while a JTA transaction + * is active for that behavior).

+ * + * @param connections an {@link Iterable} of {@link TransactionSpecificConnection} instances; must not be {@code * null} * - * @param consumer a {@link CheckedConsumer} that will be invoked - * on each connection, even in the presence of exceptional - * conditions; must not be {@code null} + * @param consumer a {@link CheckedConsumer} that will be invoked on each connection, even in the presence of + * exceptional conditions; must not be {@code null} * - * @exception NullPointerException if {@code connections} or - * {@code consumer} is {@code null} + * @exception NullPointerException if {@code connections} or {@code consumer} is {@code null} * * @exception IllegalStateException if an error occurs */ - private static void complete(final Iterable connections, - final CheckedConsumer consumer) { + private static void complete(Iterable connections, + CheckedConsumer consumer) { RuntimeException runtimeException = null; - for (final TransactionSpecificConnection connection : connections) { + for (TransactionSpecificConnection connection : connections) { try { consumer.accept(connection); - } catch (final RuntimeException exception) { + } catch (RuntimeException exception) { if (runtimeException == null) { runtimeException = exception; } else { runtimeException.addSuppressed(exception); } - } catch (final Exception exception) { + } catch (Exception exception) { if (runtimeException == null) { runtimeException = new IllegalStateException(exception.getMessage(), exception); } else { @@ -291,13 +265,13 @@ private static void complete(final IterableThe {@link Connection} returned by this method:

* *
    * - *
  • is never {@code null} (unless the underlying {@link - * DataSource} is not JDBC-compliant)
  • + *
  • is never {@code null} (unless the underlying {@link DataSource} is not JDBC-compliant)
  • * - *
  • is exactly the {@link Connection} returned by the - * underlying {@link DataSource} when there is no JTA transaction - * in effect at the time that this method is invoked
  • + *
  • is exactly the {@link Connection} returned by the underlying {@link DataSource} when there is no JTA + * transaction in effect at the time that this method is invoked
  • * *
* - *

Otherwise, when a JTA transaction is in effect, the {@link - * Connection} returned by this method:

+ *

Otherwise, when a JTA transaction is in effect, the {@link Connection} returned by this method:

* *
    * - *
  • is the same {@link Connection} returned by prior - * invocations of this method on the same thread during the - * lifespan of a JTA transaction. That is, the {@link Connection} - * is "pinned" to the current thread for the lifespan of the - * transaction.
  • - * - *
  • is not actually closeable when a JTA transaction is in - * effect. The {@link Connection#close()} method will behave from - * the standpoint of the caller as if it functions normally, but - * its invocation will not actually be propagated to the - * underlying {@link DataSource}'s connection. The fact that it - * was in fact invoked will be stored, and at such time that the - * JTA transaction completes this {@link Connection} will be - * closed at that point.
  • + *
  • is the same {@link Connection} returned by prior invocations of this method on the same thread during the + * lifespan of a JTA transaction. That is, the {@link Connection} is "pinned" to the current thread for the + * lifespan of the transaction.
  • + * + *
  • is not actually closeable when a JTA transaction is in effect. The {@link Connection#close()} method will + * behave from the standpoint of the caller as if it functions normally, but its invocation will not actually be + * propagated to the underlying {@link DataSource}'s connection. The fact that it was in fact invoked will be + * stored, and at such time that the JTA transaction completes this {@link Connection} will be closed at that + * point.
  • * *
  • has its autocommit status set to {@code false}
  • * - *
  • will have {@link Connection#commit()} called on it when the - * JTA transaction commits
  • + *
  • will have {@link Connection#commit()} called on it when the JTA transaction commits
  • * - *
  • will have {@link Connection#rollback()} called on it when - * the JTA transaction rolls back
  • + *
  • will have {@link Connection#rollback()} called on it when the JTA transaction rolls back
  • * - *
  • will have its autocommit status restored to its original - * value after the transaction completes
  • + *
  • will have its autocommit status restored to its original value after the transaction completes
  • * *
* @@ -378,11 +340,9 @@ private static void complete(final IterableThe {@link Connection} returned by this method:

* *
    * - *
  • is never {@code null} (unless the underlying {@link - * DataSource} is not JDBC-compliant)
  • + *
  • is never {@code null} (unless the underlying {@link DataSource} is not JDBC-compliant)
  • * - *
  • is exactly the {@link Connection} returned by the - * underlying {@link DataSource} when there is no JTA transaction - * in effect at the time that this method is invoked
  • + *
  • is exactly the {@link Connection} returned by the underlying {@link DataSource} when there is no JTA + * transaction in effect at the time that this method is invoked
  • * *
* - *

Otherwise, when a JTA transaction is in effect, the {@link - * Connection} returned by this method:

+ *

Otherwise, when a JTA transaction is in effect, the {@link Connection} returned by this method:

* *
    * - *
  • is the same {@link Connection} returned by prior - * invocations of this method with the same credentials (or no - * credentials) on the same thread during the lifespan of a JTA - * transaction. That is, the {@link Connection} is "pinned" to - * the current thread for the lifespan of the transaction
  • - * - *
  • is not actually closeable when a JTA transaction is in - * effect. The {@link Connection#close()} method will behave from - * the standpoint of the caller as if it functions normally, but - * its invocation will not actually be propagated to the - * underlying {@link DataSource}'s connection. The fact that it - * was in fact invoked will be stored, and at such time that the - * JTA transaction completes this {@link Connection} will be - * closed at that point.
  • + *
  • is the same {@link Connection} returned by prior invocations of this method with the same credentials (or no + * credentials) on the same thread during the lifespan of a JTA transaction. That is, the {@link Connection} is + * "pinned" to the current thread for the lifespan of the transaction
  • + * + *
  • is not actually closeable when a JTA transaction is in effect. The {@link Connection#close()} method will + * behave from the standpoint of the caller as if it functions normally, but its invocation will not actually be + * propagated to the underlying {@link DataSource}'s connection. The fact that it was in fact invoked will be + * stored, and at such time that the JTA transaction completes this {@link Connection} will be closed at that + * point.
  • * *
  • has its autocommit status set to {@code false}
  • * - *
  • will have {@link Connection#commit()} called on it when the - * JTA transaction commits
  • + *
  • will have {@link Connection#commit()} called on it when the JTA transaction commits
  • * - *
  • will have {@link Connection#rollback()} called on it when - * the JTA transaction rolls back
  • + *
  • will have {@link Connection#rollback()} called on it when the JTA transaction rolls back
  • * - *
  • will have its autocommit status restored to its original - * value after the transaction completes
  • + *
  • will have its autocommit status restored to its original value after the transaction completes
  • * *
* - * @param username the username to use to acquire an underlying - * {@link Connection}; may be {@code null} + * @param username the username to use to acquire an underlying {@link Connection}; may be {@code null} * - * @param password the password to use to acquire an underlying - * {@link Connection}; may be {@code null} + * @param password the password to use to acquire an underlying {@link Connection}; may be {@code null} * * @return a non-{@code null} {@link Connection} * * @exception SQLException if an error occurs * - * @exception RuntimeException if the {@link BooleanSupplier} - * supplied at construction time that reports a transaction's - * status throws a {@link RuntimeException}, or if the {@link - * Supplier} supplied at construction time that retrieves a - * delegate {@link DataSource} throws a {@link RuntimeException} + * @exception RuntimeException if the {@link BooleanSupplier} supplied at construction time that reports a + * transaction's status throws a {@link RuntimeException}, or if the {@link Supplier} supplied at construction time + * that retrieves a delegate {@link DataSource} throws a {@link RuntimeException} * * @see DataSource#getConnection() * * @see DataSource#getConnection(String, String) */ @Override // AbstractDataSource - public Connection getConnection(final String username, final String password) throws SQLException { + public Connection getConnection(String username, String password) throws SQLException { return this.getConnection(username, password, false); } /** - * Returns a special kind of {@link Connection} that is sourced - * from an underlying {@link DataSource}. + * Returns a special kind of {@link Connection} that is sourced from an underlying {@link DataSource}. * *

The {@link Connection} returned by this method:

* *
    * - *
  • is never {@code null} (unless the underlying {@link - * DataSource} is not JDBC-compliant)
  • + *
  • is never {@code null} (unless the underlying {@link DataSource} is not JDBC-compliant)
  • * - *
  • is exactly the {@link Connection} returned by the - * underlying {@link DataSource} when there is no JTA transaction - * in effect at the time that this method is invoked
  • + *
  • is exactly the {@link Connection} returned by the underlying {@link DataSource} when there is no JTA + * transaction in effect at the time that this method is invoked
  • * *
* - *

Otherwise, when a JTA transaction is in effect, the {@link - * Connection} returned by this method:

+ *

Otherwise, when a JTA transaction is in effect, the {@link Connection} returned by this method:

* *
    * - *
  • is the same {@link Connection} returned by prior - * invocations of this method with the same credentials (or no - * credentials) on the same thread during the lifespan of a JTA - * transaction. That is, the {@link Connection} is "pinned" to - * the current thread for the lifespan of the transaction.
  • - * - *
  • is not actually closeable when a JTA transaction is in - * effect. The {@link Connection#close()} method will behave from - * the standpoint of the caller as if it functions normally, but - * its invocation will not actually be propagated to the - * underlying {@link DataSource}'s connection. The fact that it - * was in fact invoked will be stored, and at such time that the - * JTA transaction completes this {@link Connection} will be - * closed at that point.
  • + *
  • is the same {@link Connection} returned by prior invocations of this method with the same credentials (or no + * credentials) on the same thread during the lifespan of a JTA transaction. That is, the {@link Connection} is + * "pinned" to the current thread for the lifespan of the transaction.
  • + * + *
  • is not actually closeable when a JTA transaction is in effect. The {@link Connection#close()} method will + * behave from the standpoint of the caller as if it functions normally, but its invocation will not actually be + * propagated to the underlying {@link DataSource}'s connection. The fact that it was in fact invoked will be + * stored, and at such time that the JTA transaction completes this {@link Connection} will be closed at that + * point.
  • * *
  • has its autocommit status set to {@code false}
  • * - *
  • will have {@link Connection#commit()} called on it when the - * JTA transaction commits
  • + *
  • will have {@link Connection#commit()} called on it when the JTA transaction commits
  • * - *
  • will have {@link Connection#rollback()} called on it when - * the JTA transaction rolls back
  • + *
  • will have {@link Connection#rollback()} called on it when the JTA transaction rolls back
  • * - *
  • will have its autocommit status restored to its original - * value after the transaction completes
  • + *
  • will have its autocommit status restored to its original value after the transaction completes
  • * *
* - * @param username the username to use to acquire an underlying - * {@link Connection}; may be {@code null} + * @param username the username to use to acquire an underlying {@link Connection}; may be {@code null} * - * @param password the password to use to acquire an underlying - * {@link Connection}; may be {@code null} + * @param password the password to use to acquire an underlying {@link Connection}; may be {@code null} * - * @param useZeroArgumentForm whether the underlying {@link - * DataSource}'s {@link DataSource#getConnection()} method should - * be called + * @param useZeroArgumentForm whether the underlying {@link DataSource}'s {@link DataSource#getConnection()} method + * should be called * * @return a non-{@code null} {@link Connection} * * @exception SQLException if an error occurs * - * @exception RuntimeException if the {@link BooleanSupplier} - * supplied at construction time that reports a transaction's - * status throws a {@link RuntimeException}, or if the {@link - * Supplier} supplied at construction time that retrieves a - * delegate {@link DataSource} throws a {@link RuntimeException} + * @exception RuntimeException if the {@link BooleanSupplier} supplied at construction time that reports a + * transaction's status throws a {@link RuntimeException}, or if the {@link Supplier} supplied at construction time + * that retrieves a delegate {@link DataSource} throws a {@link RuntimeException} * * @see DataSource#getConnection() * * @see DataSource#getConnection(String, String) */ - private Connection getConnection(final String username, - final String password, - final boolean useZeroArgumentForm) + private Connection getConnection(String username, + String password, + boolean useZeroArgumentForm) throws SQLException { - final Connection returnValue; + Connection returnValue; if (this.transactionIsActiveSupplier.getAsBoolean()) { - final Map extantConnections = + Map extantConnections = CONNECTIONS_TL.get().computeIfAbsent(this, k -> new HashMap<>()); - final Object id; + Object id; if (useZeroArgumentForm) { id = UNAUTHENTICATED_CONNECTION_IDENTIFIER; } else { @@ -576,19 +503,6 @@ private Connection getConnection(final String username, return returnValue; } - /** - * A method conforming to the {@link Consumer} contract, used in - * this class only via a method reference, that deliberately does - * nothing. - * - * @param ignored ignored - * - * @see Consumer#accept(Object) - */ - private static void sink(final Object ignored) { - - } - /* * Inner and nested classes. @@ -596,8 +510,7 @@ private static void sink(final Object ignored) { /** - * A functional interface that accepts a payload and may throw an - * {@link Exception} as a result. + * A functional interface that accepts a payload and may throw an {@link Exception} as a result. * * @param the type of payload * @@ -618,18 +531,25 @@ private interface CheckedConsumer { } /** - * A {@link ConditionallyCloseableConnection} that tracks when the - * {@link #close()} method has been called and that handles - * auto-commit gracefully. + * A {@link ConditionallyCloseableConnection} that tracks when the {@link #close()} method has been called and that + * handles auto-commit gracefully. + * + * @deprecated This class is slated for removal. See the {@link ConditionallyCloseableConnection} class for an + * alternative. */ - private static final class TransactionSpecificConnection extends ConditionallyCloseableConnection { + @Deprecated(forRemoval = true, since = "3.0.3") + static final class TransactionSpecificConnection extends ConditionallyCloseableConnection { + + private static final boolean STRICT_CLOSED_CHECKING = Boolean.getBoolean("helidon.jta.strict.closed.checking"); private final boolean oldAutoCommit; private boolean closeCalled; - private TransactionSpecificConnection(final Connection delegate) throws SQLException { - super(delegate, false /* not closeable */); + TransactionSpecificConnection(Connection delegate) throws SQLException { + super(delegate, + false, // not closeable + STRICT_CLOSED_CHECKING); this.oldAutoCommit = this.getAutoCommit(); this.setAutoCommit(false); } @@ -644,24 +564,25 @@ public void close() throws SQLException { super.close(); } - private boolean isCloseCalled() throws SQLException { + boolean isCloseCalled() throws SQLException { return this.closeCalled || this.isClosed(); } - private void setCloseCalled(final boolean closeCalled) { + private void setCloseCalled(boolean closeCalled) { this.closeCalled = closeCalled; } } + @Deprecated(forRemoval = true, since = "3.0.3") private static final class AuthenticatedConnectionIdentifier { private final String username; private final String password; - private AuthenticatedConnectionIdentifier(final String username, - final String password) { + private AuthenticatedConnectionIdentifier(String username, + String password) { super(); this.username = username; this.password = password; @@ -673,11 +594,11 @@ public int hashCode() { } @Override // Object - public boolean equals(final Object other) { + public boolean equals(Object other) { if (other == this) { return true; } else if (other != null && other.getClass().equals(AuthenticatedConnectionIdentifier.class)) { - final AuthenticatedConnectionIdentifier her = (AuthenticatedConnectionIdentifier) other; + AuthenticatedConnectionIdentifier her = (AuthenticatedConnectionIdentifier) other; return Objects.equals(this.username, her.username) && Objects.equals(this.password, her.password); } else { diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/LocalXAResource.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/LocalXAResource.java new file mode 100644 index 00000000000..6c4c4570e70 --- /dev/null +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/LocalXAResource.java @@ -0,0 +1,1031 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.EnumSet; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import io.helidon.integrations.jdbc.SQLRunnable; +import io.helidon.integrations.jdbc.UncheckedSQLException; +import io.helidon.integrations.jta.jdbc.ExceptionConverter.XARoutine; + +import static javax.transaction.xa.XAException.XAER_DUPID; +import static javax.transaction.xa.XAException.XAER_INVAL; +import static javax.transaction.xa.XAException.XAER_NOTA; +import static javax.transaction.xa.XAException.XAER_PROTO; +import static javax.transaction.xa.XAException.XAER_RMERR; +import static javax.transaction.xa.XAException.XAER_RMFAIL; +import static javax.transaction.xa.XAException.XA_RBROLLBACK; +import static javax.transaction.xa.XAResource.TMENDRSCAN; +import static javax.transaction.xa.XAResource.TMFAIL; +import static javax.transaction.xa.XAResource.TMJOIN; +import static javax.transaction.xa.XAResource.TMNOFLAGS; +import static javax.transaction.xa.XAResource.TMONEPHASE; +import static javax.transaction.xa.XAResource.TMRESUME; +import static javax.transaction.xa.XAResource.TMSTARTRSCAN; +import static javax.transaction.xa.XAResource.TMSUCCESS; +import static javax.transaction.xa.XAResource.TMSUSPEND; +import static javax.transaction.xa.XAResource.XA_OK; +import static javax.transaction.xa.XAResource.XA_RDONLY; + +/** + * An {@link XAResource} that adapts an ordinary arbitrary {@link Connection} as much as possible to the {@link + * XAResource} contract. + * + *

Note: instances of this class are lossless in the presence of one-phase commit operations and + * potentially lossy in the presence of two-phase commit operations.

+ * + *

Instances of this class are safe for concurrent use by multiple threads.

+ */ +final class LocalXAResource implements XAResource { + + + /* + * Static fields. + */ + + + private static final Logger LOGGER = Logger.getLogger(LocalXAResource.class.getName()); + + private static final Xid[] EMPTY_XID_ARRAY = new Xid[0]; + + // package-protected for testing only. + static final ConcurrentMap ASSOCIATIONS = new ConcurrentHashMap<>(); + + + /* + * Instance fields. + */ + + + private final Function connectionFunction; + + private final ExceptionConverter exceptionConverter; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link LocalXAResource}. + * + * @param connectionFunction a {@link Function} that accepts a {@link Xid} (supplied by the {@link #start(Xid, int)} + * method) and returns a {@link Connection} to associate with the global transaction; must not be {@code null}; must + * never return {@code null}; must be safe for concurrent use by multiple threads; will never be invoked with a + * {@code null} {@link Xid} + * + * @param exceptionConverter a {@link ExceptionConverter} that accepts a {@link XARoutine} and a {@link + * SQLException} and converts the {@link SQLException} to an appropriate {@link XAException} following the + * rules defined by the XA Specification as + * interpreted by the specification of the {@code javax.transaction.xa} package and its classes; may be {@code null} + * in which case a default implementation will be used instead + * + * @see #start(Xid, int) + */ + LocalXAResource(Function connectionFunction, ExceptionConverter exceptionConverter) { + super(); + this.connectionFunction = Objects.requireNonNull(connectionFunction, "connectionFunction"); + this.exceptionConverter = exceptionConverter == null ? LocalXAResource::convert0 : exceptionConverter; + } + + + /* + * Instance methods. + */ + + + @Override // XAResource + public void start(Xid xid, int flags) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "start", new Object[] {xid, flagsToString(flags)}); + } + requireNonNullXid(xid); + + BiFunction remappingFunction; + switch (flags) { + case TMJOIN: + remappingFunction = LocalXAResource::join; + break; + case TMNOFLAGS: + remappingFunction = this::start; + break; + case TMRESUME: + remappingFunction = LocalXAResource::resume; + break; + default: + // Bad flags. + throw (XAException) new XAException(XAER_INVAL) + .initCause(new IllegalArgumentException("xid: " + xid + "; flags: " + flagsToString(flags))); + } + + this.computeAssociation(XARoutine.START, xid, remappingFunction); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "start"); + } + } + + // (Remapping BiFunction, used in start() above and supplied to computeAssociation() below.) + private Association start(Xid x, Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "start", new Object[] {x, a}); + } + assert x != null; // x has already been vetted and is known to be non-null + if (a != null) { + throw new UncheckedXAException((XAException) new XAException(XAER_DUPID) + .initCause(new IllegalArgumentException("xid: " + x + "; association: " + a))); + } + Connection c; + try { + c = this.connectionFunction.apply(x); + } catch (RuntimeException e) { + // Weirdly, XAER_RMERR seems to be the "please retry" error code in this one case, not XAER_RMFAIL: + // https://github.com/jbosstm/narayana/blob/8ccaf0f85c7a76c227941d26cc3aa3fa9f05b160/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionImple.java#L682-L689. + throw new UncheckedXAException((XAException) new XAException(XAER_RMERR).initCause(e)); + } + if (c == null) { + // Weirdly, XAER_RMERR seems to be the "please retry" error code in this one case, not XAER_RMFAIL: + // https://github.com/jbosstm/narayana/blob/8ccaf0f85c7a76c227941d26cc3aa3fa9f05b160/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionImple.java#L682-L689. + throw new UncheckedXAException((XAException) new XAException(XAER_RMERR) + .initCause(new NullPointerException("connectionFunction.apply(" + x + ")"))); + } + a = new Association(Association.BranchState.ACTIVE, x, c); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "start", + "Created new Association ({0}) for connection ({1}) in state ACTIVE", new Object[] {a, c}); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "start", a); + } + return a; + } + + @Override // XAResource + public void end(Xid xid, int flags) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "end", new Object[] {xid, flagsToString(flags)}); + } + requireNonNullXid(xid); + + BiFunction remappingFunction; + switch (flags) { + case TMFAIL: + case TMSUCCESS: + remappingFunction = LocalXAResource::activeToIdle; + break; + case TMSUSPEND: + remappingFunction = LocalXAResource::suspend; + break; + default: + // Bad flags. + throw (XAException) new XAException(XAER_INVAL) + .initCause(new IllegalArgumentException("xid: " + xid + "; flags: " + flagsToString(flags))); + } + + // Any XAException thrown can have any error code. The transaction will be marked as rollback only. See + // https://github.com/jbosstm/narayana/blob/8ccaf0f85c7a76c227941d26cc3aa3fa9f05b160/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/transaction/arjunacore/TransactionImple.java#L978-L992. + this.computeAssociation(XARoutine.END, xid, remappingFunction); + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "end"); + } + } + + @Override // XAResource + public int prepare(Xid xid) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "prepare", xid); + } + requireNonNullXid(xid); + + // Any XAException thrown can have basically any error code. See + // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L227-L261. + Object association = + this.computeAssociation(XARoutine.PREPARE, + xid, + EnumSet.of(Association.BranchState.IDLE), + LocalXAResource::prepare, + false); // don't remove association on error + + int returnValue = association == null ? XA_RDONLY : XA_OK; + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "prepare", returnValue); + } + return returnValue; + } + + @Override // XAResource + public void commit(Xid xid, boolean onePhase) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "commit", new Object[] {xid, onePhase}); + } + requireNonNullXid(xid); + + // Error handling needs to be extraordinarily specific. XAER_RMERR indicates catastrophic failure (like if a + // local rollback(), issued in response to a local commit() failure, occurs). XAER_RMFAIL indicates a transient + // error, i.e. we tried to Do The Thing but for now it Didn't Work. + // + // Concrete examples: You can see that XAER_RMERR and (the completely non-transient) XAER_PROTO (for example) + // are both treated as Equally Bad Things: + // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L512-L514 + // + // You can also see that XAER_RMFAIL does something different and is no different from XA_RETRY: + // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L525-L534 + // + // Finally, if an error happens during commit, we issue a SQL/JDBC/local transaction rollback() command. If + // that works, then we return XA_RB*. If that fails, chances are we return XAER_RMERR, but + this.computeAssociation(XARoutine.COMMIT, + xid, + EnumSet.of(Association.BranchState.IDLE, + Association.BranchState.PREPARED), + a -> commitAndReset(a, onePhase), + true); // do remove association on error + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "commit"); + } + } + + @Override // XAResource + public void rollback(Xid xid) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "rollback", xid); + } + requireNonNullXid(xid); + + // An error during rollback is bad; in a two-phase situation where we've already prepared, then returning + // XAER_RMERR will put us in HEURISTIC_HAZARD + // (https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jta/classes/com/arjuna/ats/internal/jta/resources/arjunacore/XAResourceRecord.java#L379-L420). + // Doing XAER_RMFAIL will put us in FINISH_ERROR. + this.computeAssociation(XARoutine.ROLLBACK, + xid, + EnumSet.of(Association.BranchState.IDLE, + Association.BranchState.PREPARED, + Association.BranchState.ROLLBACK_ONLY), + LocalXAResource::rollbackAndReset, + true); // do remove association on error + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "rollback"); + } + } + + @Override // XAResource + public void forget(Xid xid) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "forget", xid); + } + requireNonNullXid(xid); + + this.computeAssociation(XARoutine.FORGET, + xid, + EnumSet.of(Association.BranchState.HEURISTICALLY_COMPLETED), + LocalXAResource::forgetAndReset, + false); // don't remove association on error + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "forget"); + } + } + + @Override // XAResource + public Xid[] recover(int flags) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "recover", flagsToString(flags)); + } + + switch (flags) { + case TMENDRSCAN: + case TMNOFLAGS: + case TMSTARTRSCAN: + break; + default: + // Bad flags. + throw (XAException) new XAException(XAER_INVAL) + .initCause(new IllegalArgumentException("flags: " + flagsToString(flags))); + } + + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(this.getClass().getName(), "recover", EMPTY_XID_ARRAY); + } + return EMPTY_XID_ARRAY; + } + + @Override // XAResource + public boolean isSameRM(XAResource xaResource) throws XAException { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "isSameRM", xaResource); + LOGGER.exiting(this.getClass().getName(), "isSameRM", this == xaResource); + } + return this == xaResource; + } + + @Override // XAResource + public int getTransactionTimeout() { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "getTransactionTimeout"); + LOGGER.exiting(this.getClass().getName(), "getTransactionTimeout", 0); + } + return 0; + } + + @Override // XAResource + public boolean setTransactionTimeout(int transactionTimeoutInSeconds) { + // (Interesting note: this is the first method that is called by the TransactionManager, i.e. before #start(Xid, + // int).) + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(this.getClass().getName(), "setTransactionTimeout", transactionTimeoutInSeconds); + LOGGER.exiting(this.getClass().getName(), "setTransactionTimeout", false); + } + return false; + } + + private Association computeAssociation(XARoutine xaRoutine, + Xid xid, + BiFunction f) + throws XAException { + try { + return ASSOCIATIONS.compute(xid, f); + } catch (RuntimeException e) { + throw this.convert(xaRoutine, e); + } + } + + private Association computeAssociation(XARoutine xaRoutine, + Xid xid, + EnumSet legalBranchStates, + UnaryOperator f, + boolean removeAssociationOnError) + throws XAException { + try { + return ASSOCIATIONS.compute(xid, (x, a) -> remap(x, a, legalBranchStates, f)); + } catch (RuntimeException e) { + if (removeAssociationOnError) { + ASSOCIATIONS.remove(xid); + } + throw this.convert(xaRoutine, e); + } + } + + private XAException convert(XARoutine xaRoutine, Throwable e) { + XAException returnValue; + if (e == null) { + returnValue = new XAException(XAER_RMERR); + } else if (e instanceof XAException xaException) { + // Obviously if e is an XAException no conversion is necessary. + returnValue = xaException; + } else { + Throwable cause = e.getCause(); + if (cause instanceof XAException xaException) { + // No matter what, if the cause was an XAException then it is canonical. + returnValue = xaException; + } else if (e instanceof IllegalTransitionException) { + // Any IllegalTransitionException is by definition an XA protocol problem. + returnValue = (XAException) new XAException(XAER_PROTO).initCause(e); + } else if (e instanceof SQLException sqlException) { + returnValue = this.exceptionConverter.convert(xaRoutine, sqlException); + } else if (cause instanceof SQLException sqlException) { + returnValue = this.exceptionConverter.convert(xaRoutine, sqlException); + } else { + returnValue = (XAException) new XAException(XAER_RMERR).initCause(e); + } + } + if (returnValue == null) { + returnValue = (XAException) new XAException(XAER_RMERR).initCause(e); + } + return returnValue; + } + + + /* + * Static methods. + */ + + + private static void requireNonNullXid(Xid xid) throws XAException { + if (xid == null) { + throw (XAException) new XAException(XAER_INVAL).initCause(new NullPointerException("xid")); + } + } + + // (Used via method reference only when an exceptionConverter was not supplied at construction time. This is the + // default implementation.) + private static XAException convert0(XARoutine xaRoutine, Exception e) { + if (e == null) { + return new XAException(XAER_RMERR); + } else if (e instanceof XAException xaException) { + return xaException; + } else { + Throwable cause = e.getCause(); + if (cause instanceof XAException xaException) { + // No matter what, if the cause was an XAException then it is canonical. + return xaException; + } else { + SQLException sqlException; + if (e instanceof SQLException s) { + sqlException = s; + } else if (cause instanceof SQLException s) { + sqlException = s; + } else { + sqlException = null; + } + if (sqlException != null) { + String sqlState = sqlException.getSQLState(); + if (sqlState != null + && (sqlState.startsWith("080") + || sqlState.equalsIgnoreCase("08S01") // ("ess" not "five") + || sqlState.equalsIgnoreCase("JZ006"))) { + // Connection-related database error; might be transient; use XAER_RMFAIL instead of XAER_RMERR, + // apparently. See, for example, + // https://github.com/pgjdbc/pgjdbc/commit/e5aab1cd3e49051f46298d8f1fd9f66af1731299. Also see + // https://github.com/pgjdbc/pgjdbc/blob/98c04a0c903e90f2d5d10a09baf1f753747b2556/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java#L651-L657 + // and + // https://github.com/pgjdbc/pgjdbc/blob/98c04a0c903e90f2d5d10a09baf1f753747b2556/pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java#L553. Also + // see + // https://github.com/ironjacamar/ironjacamar/blob/ff62b8b23f59f9fbb9c15be40fef38efb872c436/core/src/main/java/org/jboss/jca/core/tx/jbossts/LocalConnectableXAResourceImpl.java#L55-L61. + // + // But also note XAER_RMERR vs. XAER_RMFAIL changes semantics depending on the routine (start, end, + // commit, rollback, prepare, forget, recover). + return (XAException) new XAException(XAER_RMFAIL).initCause(e); + } + } + } + } + return (XAException) new XAException(XAER_RMERR).initCause(e); + } + + // (Invoked only in context of a remapping BiFunction, from computeAssociation().) + private static Association remap(Xid xid, + Association a, + EnumSet legalBranchStates, + UnaryOperator remapOperator) { + if (a == null) { + throw new UncheckedXAException((XAException) new XAException(XAER_NOTA) + .initCause(new NullPointerException("xid: " + xid + "; association: null"))); + } else if (!legalBranchStates.contains(a.branchState())) { + throw new UncheckedXAException((XAException) new XAException(XAER_PROTO) + .initCause(new IllegalStateException("xid: " + xid + "; association: " + a))); + } + return remapOperator.apply(a); + } + + // (Remapping BiFunction. Used in end() above.) + private static Association activeToIdle(Xid x, Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "activeToIdle", new Object[] {x, a}); + } + a = a.activeToIdle(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "activeToIdle", a); + } + return a; + } + + // (Remapping BiFunction. Used in end() above.) + private static Association suspend(Xid x, Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "suspend", new Object[] {x, a}); + } + a = a.suspend(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "suspend", a); + } + return a; + } + + // (Remapping BiFunction. Used in start() above.) + private static Association join(Xid x, Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "join", new Object[] {x, a}); + } + if (a == null) { + throw new UncheckedXAException((XAException) new XAException(XAER_NOTA) + .initCause(new NullPointerException("xid: " + x + "; association: null"))); + } else if (a.suspended()) { + assert a.branchState() == Association.BranchState.IDLE; + throw new UncheckedXAException((XAException) new XAException(XAER_PROTO) + .initCause(new IllegalStateException("xid: " + x + "; association: " + a))); + } + switch (a.branchState()) { + case ACTIVE: + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "join", + "Joining Association ({0}) in state ACTIVE", a); + } + break; + case IDLE: + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "join", + "Joining Association ({0}) and transitioning it from state IDLE to state ACTIVE", a); + } + a = a.idleToActive(); + break; + default: + throw new IllegalTransitionException("xid: " + x + "; association: " + a); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "join", a); + } + return a; + } + + // (Remapping BiFunction. Used in start() above.) + private static Association resume(Xid x, Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "resume", new Object[] {x, a}); + } + if (a == null) { + throw new UncheckedXAException((XAException) new XAException(XAER_NOTA) + .initCause(new NullPointerException("xid: " + x + "; association: null"))); + } + a = a.resume(); + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "resume", a); + } + return a; + } + + // (Invoked during remap() above. Similar to the UnaryOperator-like methods, but not invoked via method reference.) + private static Association commitAndReset(Association a, boolean onePhase) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "commitAndReset", new Object[] {a, onePhase}); + } + assert a != null; // already vetted + try { + a = a.commitAndReset(onePhase); + } catch (SQLException e) { + throw new UncheckedSQLException(e); + } catch (XAException e) { + throw new UncheckedXAException(e); + } + assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "commitAndReset", + "Removing association {0}", a); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "commitAndReset", "null"); + } + // Critically important: remove the association. + return null; + } + + // (UnaryOperator for supplying via method reference to remap() above.) + private static Association rollbackAndReset(Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "rollbackAndReset", a); + } + assert a != null; // already vetted + try { + a = a.rollbackAndReset(); + } catch (SQLException sqlException) { + throw new UncheckedSQLException(sqlException); + } + assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "rollbackAndReset", + "Removing association {0}", a); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "rollbackAndReset", "null"); + } + // Critically important: remove the association. + return null; + } + + // (UnaryOperator for supplying via method reference to remap() above.) + private static Association prepare(Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "prepare", a); + } + assert a != null; // already vetted + assert !a.suspended(); // can't be in T2 + try { + if (a.connection().isReadOnly()) { + a = a.reset(); + assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "prepare", + "Removing association {0}", a); + } + // Critically important: remove the association. + a = null; + } + } catch (SQLException e) { + throw new UncheckedSQLException(e); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "preapre", a); + } + return a; + } + + // (UnaryOperator for supplying via method reference to remap() above.) + private static Association forgetAndReset(Association a) { + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.entering(Association.class.getName(), "forgetAndReset", a); + } + assert a != null; // already vetted + try { + a = a.forgetAndReset(); + } catch (SQLException e) { + throw new UncheckedSQLException(e); + } + assert a.branchState() == Association.BranchState.NON_EXISTENT_TRANSACTION; + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "forgetAndReset", + "Removing association {0}", a); + } + if (LOGGER.isLoggable(Level.FINER)) { + LOGGER.exiting(Association.class.getName(), "forgetAndReset", "null"); + } + // Critically important: remove the association. + return null; + } + + private static String flagsToString(int flags) { + switch (flags) { + case TMENDRSCAN: + return "TMENDRSCAN (" + flags + ")"; + case TMFAIL: + return "TMFAIL (" + flags + ")"; + case TMJOIN: + return "TMJOIN (" + flags + ")"; + case TMNOFLAGS: + return "TMNOFLAGS (" + flags + ")"; + case TMONEPHASE: + return "TMONEPHASE (" + flags + ")"; + case TMRESUME: + return "TMRESUME (" + flags + ")"; + case TMSTARTRSCAN: + return "TMSTARTRSCAN (" + flags + ")"; + case TMSUCCESS: + return "TMSUCCESS (" + flags + ")"; + case TMSUSPEND: + return "TMSUSPEND (" + flags + ")"; + default: + return String.valueOf(flags); + } + } + + + /* + * Inner and nested classes. + */ + + + static record Association(BranchState branchState, + Xid xid, + boolean suspended, + Connection connection, + boolean priorAutoCommit) { + + private static final Logger LOGGER = Logger.getLogger(Association.class.getName()); + + // Branch Association States: (XA specification, table 6-2) + // T0: Not Associated + // T1: Associated + // T2: Association Suspended + + // Branch States: (XA specification, table 6-4) + // S0: Non-existent Transaction + // S1: Active + // S2: Idle + // S3: Prepared + // S4: Rollback Only + // S5: Heuristically Completed + + Association(BranchState branchState, Xid xid, Connection connection) { + this(branchState, xid, false, connection); + } + + Association(BranchState branchState, Xid xid, boolean suspended, Connection connection) { + this(branchState, xid, suspended, connection, true /* JDBC default; will be set from connection anyway */); + } + + Association { + Objects.requireNonNull(xid, "xid"); + switch (branchState) { + case IDLE: + break; + case ACTIVE: + case HEURISTICALLY_COMPLETED: + case NON_EXISTENT_TRANSACTION: + case PREPARED: + case ROLLBACK_ONLY: + if (suspended) { + throw new IllegalArgumentException("suspended"); + } + break; + default: + throw new IllegalArgumentException("branchState: " + branchState); + } + try { + priorAutoCommit = connection.getAutoCommit(); + if (priorAutoCommit) { + connection.setAutoCommit(false); + } + } catch (SQLException sqlException) { + throw new UncheckedSQLException(sqlException); + } + // T0, T1 or T2; S0 or S2 + } + + public boolean suspended() { + assert this.suspended ? this.branchState() == BranchState.IDLE : true; + return this.suspended; + } + + private Association activeToIdle() { + if (!this.suspended()) { + switch (this.branchState()) { + case ACTIVE: + // OK; end(*) was called and didn't fail with an XAER_RB* code and we are not suspended + // + // Associated -> Associated (T1 -> T1; unchanged) + // Active -> Idle (S1 -> S2) + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "activeToIdle", + "Transitioning Association ({0}) from state ACTIVE to state IDLE", this); + } + return new Association(BranchState.IDLE, + this.xid(), + false, + this.connection(), + this.priorAutoCommit()); + default: + break; + } + } + throw new IllegalTransitionException(this.toString()); + } + + private Association activeToRollbackOnly() { + if (!this.suspended()) { + switch (this.branchState()) { + case ACTIVE: + // OK; end(*) was called and failed with an XAER_RB* code and we are not suspended + // + // Associated -> Associated (T1 -> T1; unchanged) + // Active -> Rollback Only (S1 -> S4) + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "activeToRollbackOnly", + "Transitioning Association ({0}) from state ACTIVE to state ROLLBACK_ONLY", this); + } + return new Association(BranchState.ROLLBACK_ONLY, + this.xid(), + false, + this.connection(), + this.priorAutoCommit()); + default: + break; + } + } + throw new IllegalTransitionException(this.toString()); + } + + private Association idleToActive() { + if (!this.suspended()) { + switch (this.branchState()) { + case IDLE: + // OK; start(TMJOIN) was called and didn't fail with an XAER_RB* code and we are not suspended + // + // Associated -> Associated (T1 -> T1; unchanged) + // Idle -> Active (S2 -> S1) + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "idleToActive", + "Transitioning Association ({0}) from state IDLE to state ACTIVE", this); + } + return new Association(BranchState.ACTIVE, + this.xid(), + false, + this.connection(), + this.priorAutoCommit()); + default: + break; + } + } + throw new IllegalTransitionException(this.toString()); + } + + private Association idleToRollbackOnly() { + if (!this.suspended()) { + switch (this.branchState()) { + case IDLE: + // OK; start(*) was called and failed with an XAER_RB* code and we are not suspended + // + // Associated -> Associated (T1 -> T1; unchanged) + // Idle -> Rollback Only (S2 -> S4) + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "idleToRollbackOnly", + "Transitioning Association ({0}) from state IDLE to state ROLLBACK_ONLY", this); + } + return new Association(BranchState.ROLLBACK_ONLY, + this.xid(), + false, + this.connection(), + this.priorAutoCommit()); + default: + break; + } + } + throw new IllegalTransitionException(this.toString()); + } + + private Association suspend() { + if (!this.suspended()) { + switch (this.branchState()) { + case ACTIVE: + // OK; end(TMSUSPEND) was called and we are not suspended + // + // Associated -> Association Suspended (T1 -> T2) + // Active -> Idle (S1 -> S2) + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "suspend", + "Suspending Association ({0}) and transitioning from state ACTIVE to state IDLE", this); + } + return new Association(BranchState.IDLE, + this.xid(), + true, + this.connection(), + this.priorAutoCommit()); + default: + break; + } + } + throw new IllegalTransitionException(this.toString()); + } + + private Association resume() { + if (this.suspended()) { + switch (this.branchState()) { + case IDLE: + // OK; start(TMRESUME) was called and we are suspended + // + // Association Suspended -> Associated (T2 -> T1) + // Idle -> Active (S2 -> S1) + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, Association.class.getName(), "resume", + "Resuming Association ({0}) from state IDLE to state ACTIVE", this); + } + return new Association(BranchState.ACTIVE, + this.xid(), + false, + this.connection(), + this.priorAutoCommit()); + default: + break; + } + } + throw new IllegalTransitionException(this.toString()); + } + + private Association commitAndReset(boolean onePhase) throws SQLException, XAException { + Connection c = this.connection(); + return this.runAndReset(c::commit, c::rollback, onePhase); + } + + private Association rollbackAndReset() throws SQLException { + try { + return this.runAndReset(this.connection()::rollback, null, false); + } catch (XAException e) { + throw new AssertionError(e.getMessage(), e); + } + } + + private Association runAndReset(SQLRunnable r, SQLRunnable rollbackRunnable, boolean onePhaseCommit) + throws SQLException, XAException { + // If rollbackRunnable is non-null, then we're doing a commit. + Association a; + SQLException sqlException = null; + try { + r.run(); + if (LOGGER.isLoggable(Level.FINE)) { + boolean committed = rollbackRunnable != null; + StringBuilder message = new StringBuilder(committed ? "Committed " : "Rolled back "); + message.append("connection (").append(this.connection()).append(") "); + if (committed) { + message.append("with ").append(onePhaseCommit ? "one-phase " : "two-phase ").append("semantics "); + } + message.append("in Association ").append(this); + LOGGER.logp(Level.FINE, this.getClass().getName(), "runAndReset", + message.toString()); + } + } catch (SQLException e) { + sqlException = e; + if (rollbackRunnable != null) { + try { + rollbackRunnable.run(); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "runAndReset", + "Rolled back connection ({0}) in Association {2}", + new Object[] {this.connection(), this}); + } + if (onePhaseCommit) { + // localXAResource.commit(someXid, true) caused us to try to call + // someConnection.commit(). That failed, and we successfully rolled back. Now we have to + // throw an XAException that indicates all this. + throw (XAException) new XAException(XA_RBROLLBACK).initCause(e); + } + } catch (SQLException e2) { + e.setNextException(e2); + } + } + } finally { + try { + a = this.reset(); + } catch (SQLException e) { + a = null; + if (sqlException == null) { + sqlException = e; + } else if (sqlException != e) { + sqlException.setNextException(e); + } + } finally { + if (sqlException != null) { + throw sqlException; + } + } + } + return a; + } + + private Association forgetAndReset() throws SQLException { + return this.reset(); + } + + private Association reset() throws SQLException { + Connection connection = this.connection(); + connection.setAutoCommit(this.priorAutoCommit()); + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.logp(Level.FINE, this.getClass().getName(), "reset", + "Resetting; restored autoCommit to {0} on connection {1}", + new Object[] {this.priorAutoCommit(), connection}); + LOGGER.logp(Level.FINE, this.getClass().getName(), "reset", + "Transitioning Association {0} to NON_EXISTENT_TRANSACTION", this); + } + return new Association(BranchState.NON_EXISTENT_TRANSACTION, + this.xid(), + false, + connection, + this.priorAutoCommit()); + } + + // Transaction Branch States (XA specification, table 6-4): + // S0: Non-existent Transaction + // S1: Active + // S2: Idle + // S3: Prepared + // S4: Rollback Only + // S5: Heuristically Completed + enum BranchState { + NON_EXISTENT_TRANSACTION, // S0 + ACTIVE, // S1 + IDLE, // S2 + PREPARED, // S3 + ROLLBACK_ONLY, // S4 + HEURISTICALLY_COMPLETED; // S5 + } + + } + + private static final class IllegalTransitionException extends IllegalStateException { + + private static final long serialVersionUID = 1L; + + private IllegalTransitionException(String message) { + super(message); + } + + } + +} diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/TransactionSupplier.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/TransactionSupplier.java new file mode 100644 index 00000000000..689ef50188a --- /dev/null +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/TransactionSupplier.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; + +/** + * A supplier of {@link Transaction}s. + * + * @see Transaction + * + * @see jakarta.transaction.TransactionManager#getTransaction() + */ +@FunctionalInterface +public interface TransactionSupplier { + + /** + * Returns the current {@link Transaction} representing the transaction context of the calling thread, or {@code + * null} if there is no such context at invocation time. + * + * @return the current {@link Transaction} representing the transaction context of the calling thread, or {@code + * null} if there is no such context at invocation time + * + * @exception SystemException if there was an unexpected error condition + */ + Transaction getTransaction() throws SystemException; + +} diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/UncheckedXAException.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/UncheckedXAException.java new file mode 100644 index 00000000000..92abee631dd --- /dev/null +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/UncheckedXAException.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import javax.transaction.xa.XAException; + +final class UncheckedXAException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + UncheckedXAException(XAException cause) { + super(cause); + } + + @Override + public XAException getCause() { + return (XAException) super.getCause(); + } + +} diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/XADataSourceWrappingDataSource.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/XADataSourceWrappingDataSource.java index 0366466041c..20523f9abca 100644 --- a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/XADataSourceWrappingDataSource.java +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/XADataSourceWrappingDataSource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,9 +17,12 @@ import java.sql.Connection; import java.sql.SQLException; +import java.sql.SQLTransientException; import java.util.Objects; import java.util.function.Consumer; +import javax.sql.ConnectionEvent; +import javax.sql.ConnectionEventListener; import javax.sql.XAConnection; import javax.sql.XADataSource; import javax.transaction.xa.XAResource; @@ -30,14 +33,9 @@ * An {@link AbstractDataSource} that adapts an {@link XADataSource} * to the {@link javax.sql.DataSource} contract. * - *

The {@link XADataSource} being adapted must guarantee that when - * {@link Connection#close() close()} is called on any {@link - * Connection} that an {@link XAConnection} supplied by the {@link - * XADataSource#getXAConnection()} method {@linkplain - * XAConnection#getConnection() supplies}, the closing operation is - * {@linkplain XAConnection#close() propagated to the - * XAConnection}, or undefined behavior will result.

+ * @deprecated This class is slated for removal with no replacement. */ +@Deprecated(forRemoval = true, since = "3.0.3") public final class XADataSourceWrappingDataSource extends AbstractDataSource { private final XADataSource xaDataSource; @@ -54,8 +52,8 @@ public final class XADataSourceWrappingDataSource extends AbstractDataSource { * XAResource} instances that enlists them in an active XA * transaction; must not be {@code null} */ - public XADataSourceWrappingDataSource(final XADataSource xaDataSource, - final Consumer resourceEnlister) { + public XADataSourceWrappingDataSource(XADataSource xaDataSource, + Consumer resourceEnlister) { super(); this.xaDataSource = Objects.requireNonNull(xaDataSource, "xaDataSource"); this.resourceEnlister = Objects.requireNonNull(resourceEnlister, "resourceEnlister"); @@ -67,34 +65,62 @@ public Connection getConnection() throws SQLException { } @Override // AbstractDataSource - public Connection getConnection(final String username, final String password) throws SQLException { + public Connection getConnection(String username, String password) throws SQLException { return this.getConnection(username, password, false); } - private Connection getConnection(final String username, - final String password, - final boolean useZeroArgumentForm) + private Connection getConnection(String username, + String password, + boolean useZeroArgumentForm) throws SQLException { - final XAConnection xaConnection; - if (useZeroArgumentForm) { - xaConnection = this.xaDataSource.getXAConnection(); - } else { - xaConnection = this.xaDataSource.getXAConnection(username, password); - } + XAConnection xaConnection = + useZeroArgumentForm ? this.xaDataSource.getXAConnection() : this.xaDataSource.getXAConnection(username, password); + ConnectionEventListener l = new ConnectionEventListener() { + @Override + public void connectionClosed(ConnectionEvent event) { + try { + ((XAConnection) event.getSource()).close(); + } catch (SQLException e) { + try { + ((XAConnection) event.getSource()).removeConnectionEventListener(this); + } catch (RuntimeException e2) { + e.addSuppressed(e2); + } + throw new IllegalStateException(e.getMessage(), e); + } + ((XAConnection) event.getSource()).removeConnectionEventListener(this); + } + @Override + public void connectionErrorOccurred(ConnectionEvent event) { + try { + ((XAConnection) event.getSource()).close(); + } catch (SQLException e) { + SQLException original = event.getSQLException(); + if (original != null) { + original.addSuppressed(e); + e = original; + } + try { + ((XAConnection) event.getSource()).removeConnectionEventListener(this); + } catch (RuntimeException e2) { + e.addSuppressed(e2); + } + throw new IllegalStateException(e.getMessage(), e); + } + ((XAConnection) event.getSource()).removeConnectionEventListener(this); + } + }; + xaConnection.addConnectionEventListener(l); try { this.resourceEnlister.accept(xaConnection.getXAResource()); - } catch (final RuntimeException e) { - throw new SQLException(e.getMessage(), e); + } catch (RuntimeException e) { + try { + xaConnection.removeConnectionEventListener(l); + } catch (RuntimeException e2) { + e.addSuppressed(e2); + } + throw new SQLTransientException(e.getMessage(), e); } - // I am not confident about this. Note that the xaConnection - // is not closed. And yet, the end consumer knows nothing of - // XAConnections so cannot close it herself. So is - // Connection#close() invoked on the return value of - // XAConnection#getConnection() guaranteed to call through to - // XAConnection#close()? Using H2 as an arbitrary example, we - // can see this is the case: - // https://github.com/h2database/h2database/blob/12fcf4c219e26176d4027e72eb5f9f0c797f0152/h2/src/main/org/h2/jdbcx/JdbcXAConnection.java#L74-L89 - // Is that mandated anywhere? I'm honestly not sure. return xaConnection.getConnection(); } diff --git a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/package-info.java b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/package-info.java index 9d74c379037..576f373602e 100644 --- a/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/package-info.java +++ b/integrations/jta/jdbc/src/main/java/io/helidon/integrations/jta/jdbc/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/integrations/jta/jdbc/src/main/java/module-info.java b/integrations/jta/jdbc/src/main/java/module-info.java index 73c695afe86..37292dd5a7e 100644 --- a/integrations/jta/jdbc/src/main/java/module-info.java +++ b/integrations/jta/jdbc/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Oracle and/or its affiliates. + * Copyright (c) 2021, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ @SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) module io.helidon.integrations.jta.jdbc { + requires transitive java.rmi; // jakarta.transaction.TransactionRequiredException extends RemoteException (!) + requires transitive jakarta.transaction; // automatic module requires transitive io.helidon.integrations.jdbc; diff --git a/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/NoOpXAResource.java b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/NoOpXAResource.java new file mode 100644 index 00000000000..f6c20e6bb09 --- /dev/null +++ b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/NoOpXAResource.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.util.logging.Logger; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import jakarta.transaction.Synchronization; +import jakarta.transaction.TransactionManager; + +import static javax.transaction.xa.XAResource.TMENDRSCAN; +import static javax.transaction.xa.XAResource.TMFAIL; +import static javax.transaction.xa.XAResource.TMJOIN; +import static javax.transaction.xa.XAResource.TMNOFLAGS; +import static javax.transaction.xa.XAResource.TMONEPHASE; +import static javax.transaction.xa.XAResource.TMRESUME; +import static javax.transaction.xa.XAResource.TMSTARTRSCAN; +import static javax.transaction.xa.XAResource.TMSUCCESS; +import static javax.transaction.xa.XAResource.TMSUSPEND; + +final class NoOpXAResource implements Synchronization, XAResource { + + private static final Logger LOGGER = Logger.getLogger(NoOpXAResource.class.getName()); + + private static final Xid[] EMPTY_XID_ARRAY = new Xid[0]; + + protected final TransactionManager tm; + + NoOpXAResource() { + this(null); + } + + NoOpXAResource(TransactionManager tm) { + super(); + this.tm = tm; + } + + @Override + public void start(Xid xid, int flags) throws XAException { + LOGGER.info("start " + xid + " (" + flagsToString(flags) + ")"); + } + + @Override // XAResource + public void end(Xid xid, int flags) throws XAException { + LOGGER.info("end " + xid + " (" + flagsToString(flags) + ")"); + } + + @Override // XAResource + public int prepare(Xid xid) throws XAException { + LOGGER.info("prepare " + xid); + return XAResource.XA_OK; + } + + @Override // XAResource + public void commit(Xid xid, boolean onePhase) throws XAException { + LOGGER.info("commit " + xid + (onePhase ? " one phase" : " two phase")); + } + + @Override // XAResource + public void rollback(Xid xid) throws XAException { + LOGGER.info("rollback " + xid); + } + + @Override // XAResource + public void forget(Xid xid) throws XAException { + LOGGER.info("forget " + xid); + } + + @Override // XAResource + public Xid[] recover(int flags) throws XAException { + LOGGER.info("recover " + flagsToString(flags)); + return EMPTY_XID_ARRAY; + } + + @Override // XAResource + public boolean isSameRM(XAResource xaResource) throws XAException { + LOGGER.info("isSameRM? this: " + this + "; xaResource: " + xaResource); + return this == xaResource; + } + + @Override // XAResource + public int getTransactionTimeout() { + LOGGER.info("getTransactionTimeout()"); + return 0; + } + + @Override // XAResource + public boolean setTransactionTimeout(int transactionTimeoutInSeconds) { + LOGGER.info("setTransactionTimeout(" + transactionTimeoutInSeconds + ")"); + return false; + } + + @Override // Synchronization + public void beforeCompletion() { + LOGGER.info("beforeCompletion()"); + if (this.tm != null) { + try { + this.tm.getTransaction().enlistResource(this); // Is this legal? + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException(e.getMessage(), e); + } + } + } + + @Override // Synchronization + public void afterCompletion(int status) { + LOGGER.info("afterCompletion(" + status + ")"); + } + + private static String flagsToString(int flags) { + switch (flags) { + case TMENDRSCAN: + return "TMENDRSCAN (" + flags + ")"; + case TMFAIL: + return "TMFAIL (" + flags + ")"; + case TMJOIN: + return "TMJOIN (" + flags + ")"; + case TMNOFLAGS: + return "TMNOFLAGS (" + flags + ")"; + case TMONEPHASE: + return "TMONEPHASE (" + flags + ")"; + case TMRESUME: + return "TMRESUME (" + flags + ")"; + case TMSTARTRSCAN: + return "TMSTARTRSCAN (" + flags + ")"; + case TMSUCCESS: + return "TMSUCCESS (" + flags + ")"; + case TMSUSPEND: + return "TMSUSPEND (" + flags + ")"; + default: + return String.valueOf(flags); + } + } + +} diff --git a/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestJtaConnection.java b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestJtaConnection.java new file mode 100644 index 00000000000..b714009e52c --- /dev/null +++ b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestJtaConnection.java @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.sql.SQLException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +import io.helidon.integrations.jdbc.ConditionallyCloseableConnection; +import io.helidon.integrations.jta.jdbc.LocalXAResource.Association; +import io.helidon.integrations.jta.jdbc.LocalXAResource.Association.BranchState; + +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.InvalidTransactionException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.Synchronization; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; + +import com.arjuna.ats.arjuna.common.Uid; +import com.arjuna.ats.arjuna.coordinator.TransactionReaper; +import com.arjuna.ats.arjuna.coordinator.listener.ReaperMonitor; +import com.arjuna.ats.jta.common.JTAEnvironmentBean; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static io.helidon.integrations.jta.jdbc.LocalXAResource.Association.BranchState.ACTIVE; +import static io.helidon.integrations.jta.jdbc.LocalXAResource.Association.BranchState.IDLE; +import static io.helidon.integrations.jta.jdbc.LocalXAResource.ASSOCIATIONS; +import static jakarta.transaction.Status.STATUS_NO_TRANSACTION; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class TestJtaConnection { + + private static final Logger LOGGER = Logger.getLogger(TestJtaConnection.class.getName()); + + private static final JTAEnvironmentBean jtaEnvironmentBean = new JTAEnvironmentBean(); + + private JdbcDataSource h2ds; + + private TransactionManager tm; + + private TransactionSynchronizationRegistry tsr; + + private TestJtaConnection() throws SQLException { + super(); + } + + @BeforeEach + final void initializeH2DataSource() throws SQLException { + JdbcDataSource ds = new JdbcDataSource(); + // Use TRACE_LEVEL_FILE=4 to turn on slf4j logging. + ds.setURL("jdbc:h2:mem:test;INIT=SET TRACE_LEVEL_FILE=4"); + ds.setUser("sa"); + ds.setPassword("sa"); + this.h2ds = ds; + } + + @BeforeEach + final void initializeTransactionManager() throws SystemException { + this.tm = jtaEnvironmentBean.getTransactionManager(); // com.arjuna.ats.jta.TransactionManager.transactionManager(); + this.tm.setTransactionTimeout(20 * 60); // 20 minutes for debugging + } + + @BeforeEach + final void initializeTransactionSynchronizationRegistry() throws SystemException { + this.tsr = jtaEnvironmentBean.getTransactionSynchronizationRegistry(); + } + + @AfterEach + final void rollback() throws SQLException, SystemException { + switch (this.tm.getStatus()) { + case STATUS_NO_TRANSACTION: + break; + default: + this.tm.rollback(); + break; + } + this.tm.setTransactionTimeout(0); + // assertThat(ASSOCIATIONS.size(), is(0)); + } + + @DisplayName("Spike") + @SuppressWarnings("try") + @Test + final void testSpike() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SQLException, + SystemException { + LOGGER.info("Starting testSpike()"); + tm.begin(); + + Transaction t = tm.getTransaction(); + + // For this test, where transaction reaping is set to happen eons in the future, we can make this assertion. + // For real-world scenarios, the reaper might have ended the transaction immediately on another thread. + assertThat(t.getStatus(), is(Status.STATUS_ACTIVE)); + + try (Connection physicalConnection = h2ds.getConnection(); + JtaConnection logicalConnection = new JtaConnection(tm::getTransaction, + tsr, + true, + null, + physicalConnection, + null, + x -> tsr.putResource("xid", x), + false)) { + + // Make sure everything is hooked up properly. + assertThat(logicalConnection.delegate(), sameInstance(physicalConnection)); + + // Trigger an Object method; make sure nothing blows up (in case we're using proxies). + logicalConnection.toString(); + + // Up until this point, the connection should not be enlisted. (delegate() and toString() must not cause + // enlistment.) + assertThat(logicalConnection.enlisted(), is(false)); + + // (Calling enlisted() itself must not cause enlistment.) + assertThat(logicalConnection.enlisted(), is(false)); + + // Since it's not enlisted, it should be closeable (but it has not yet been closed). + assertThat(logicalConnection.isCloseable(), is(true)); + assertThat(logicalConnection.isClosed(), is(false)); + + // Trigger a harmless Connection-related method; make sure nothing blows up. + logicalConnection.getHoldability(); + + // Almost all Connection methods, including that one, will cause enlistment to happen. + assertThat(logicalConnection.enlisted(), is(true)); + + // Make sure the XAResource recorded the association. + Xid xid = (Xid) this.tsr.getResource("xid"); + assertThat(ASSOCIATIONS.get(xid).branchState(), is(ACTIVE)); + + // That means the Connection is no longer closeable. + assertThat(logicalConnection.isCloseable(), is(false)); + + // Ensure JDBC constructs' backlinks work correctly. + try (Statement s = logicalConnection.createStatement()) { + assertThat(s, notNullValue()); + assertThat(s.getConnection(), sameInstance(logicalConnection)); + try (ResultSet rs = s.executeQuery("SHOW TABLES")) { + assertThat(rs.getStatement(), sameInstance(s)); + } + } + + // close() should "close" the logical connection, but not close the physical connection. + logicalConnection.close(); + assertThat(logicalConnection.isClosed(), is(true)); + assertThat(physicalConnection.isClosed(), is(false)); + + // Make sure a close() attempt was recorded. + assertThat(logicalConnection.isClosePending(), is(true)); + + // But "closing" should have disassociated the XAResource. + assertThat(ASSOCIATIONS.get(xid).branchState(), is(IDLE)); + + // What happens when we do re-enlisting behavior? First, we'd better appear closed since we called close() + // above: + assertThrows(SQLException.class, () -> logicalConnection.getHoldability()); + + // Let's reenable closeability for this test, which is something users won't do: + logicalConnection.setCloseable(true); + assertThat(logicalConnection.isCloseable(), is(true)); + assertThat(logicalConnection.isClosePending(), is(false)); + assertThat(logicalConnection.isClosed(), is(false)); + + // Assert after all of this we're still IDLE, because we don't remove the XAResource from the + // TransactionSynchronizationRegistry, so the enlisted() method thinks we're already enlisted, so + // getHoldability() skips enlistment, so everything stays as it was. + assertThat(ASSOCIATIONS.get(xid).branchState(), is(IDLE)); + + // Commit (we didn't actually do any work) AND DISASSOCIATE the transaction, which can only happen with a + // call to TransactionManager.commit(), not just Transaction.commit(). + tm.commit(); + + // Transaction is over; the connection should be closeable again. + assertThat(logicalConnection.isCloseable(), is(true)); + + // Transaction is over; make sure the XAResource removed the association. + assertThat(ASSOCIATIONS.size(), is(0)); + + // We should be able to actually close it early. The auto-close should not fail, either. + logicalConnection.close(); + assertThat(logicalConnection.isClosed(), is(true)); + assertThat(logicalConnection.delegate().isClosed(), is(true)); + } + + LOGGER.info("Ending testSpike()"); + } + + @Test + final void testTimeout() throws InterruptedException, NotSupportedException, RollbackException, SystemException { + LOGGER.info("Starting testTimeout()"); + + tm.setTransactionTimeout(1); // 1 second; the minimum settable value (0 means "use the default" (!)) + tm.begin(); + + // For this test, where transaction reaping is set to happen soon, it still won't happen in under 1000 + // milliseconds, so this assertion is OK unless the testing environment is completely pathological. For + // real-world scenarios, you shouldn't assume anything about the initial status. + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + + CountDownLatch latch = new CountDownLatch(1); + Thread mainThread = Thread.currentThread(); + + tsr.registerInterposedSynchronization(new Synchronization() { + public void beforeCompletion() { + + } + public void afterCompletion(int status) { + try { + assertThat(status, is(Status.STATUS_ROLLEDBACK)); + assertThat(Thread.currentThread(), not(mainThread)); + } finally { + latch.countDown(); + } + } + }); + + // Wait for the transaction to roll back on the reaper thread. The transaction timeout is 1 second (see above); + // this waits for 2 seconds max. If this fails with an InterruptedException, which should be impossible, check + // the logs for Narayana warning that the assertions in the synchronization above failed. + latch.await(2000L, TimeUnit.MILLISECONDS); + + // In this case, we never issued a rollback ourselves and we never acquired a Transaction. Here we show that + // you can get a Transaction that is initially in a rolled back state. + // + // Verify there *was* a transaction but now it is rolled back, so is essentially useless other than as a + // tombstone. + Transaction t = tm.getTransaction(); + assertThat(t, notNullValue()); + assertThat(t.getStatus(), is(Status.STATUS_ROLLEDBACK)); + + // Verify that all the other status accessors return the same thing. + assertThat(tm.getStatus(), is(Status.STATUS_ROLLEDBACK)); + assertThat(tsr.getTransactionStatus(), is(Status.STATUS_ROLLEDBACK)); + + // Verify that indeed you cannot enlist any XAResource in the transaction when it is in the rolled back state. + assertThrows(IllegalStateException.class, () -> t.enlistResource(new NoOpXAResource())); + + // Verify that even though the current transaction is rolled back you can still roll it back again (no-op) and + // disassociate it from the current thread. + tm.rollback(); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + assertThat(tsr.getTransactionStatus(), is(Status.STATUS_NO_TRANSACTION)); + + // The Transaction itself remains in status Status.STATUS_ROLLEDBACK. Notably it never enters + // Status.STATUS_NO_TRANSACTION. + assertThat(t.getStatus(), is(Status.STATUS_ROLLEDBACK)); + + assertThat(ASSOCIATIONS.size(), is(0)); + + LOGGER.info("Ending testTimeout()"); + } + + @Test + final void testBeginSuspendBeginCommitResumeCommit() + throws HeuristicMixedException, + HeuristicRollbackException, + InvalidTransactionException, + NotSupportedException, + RollbackException, + SQLException, + SystemException { + LOGGER.info("Starting testBeginSuspendBeginCommitResumeCommit()"); + + tm.begin(); + + Transaction t = tm.getTransaction(); + assertThat(t.getStatus(), is(Status.STATUS_ACTIVE)); + + Connection physicalConnection = h2ds.getConnection(); + JtaConnection logicalConnection = + new JtaConnection(tm::getTransaction, + tsr, + true, + null, + physicalConnection, + true); + + assertThat(logicalConnection.enlisted(), is(true)); + assertThat(logicalConnection.delegate(), sameInstance(physicalConnection)); + + // Suspend the current transaction. It will stay in Status.STATUS_ACTIVE state, because suspension has no + // effect on the actual state of the *Transaction*, only on the state of its association with the current + // thread. + Transaction s = tm.suspend(); + assertThat(s, sameInstance(t)); + assertThat(s.getStatus(), is(Status.STATUS_ACTIVE)); + + // The TransactionManager will report that there is no transaction. + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + + assertThat(logicalConnection.isCloseable(), is(false)); // we're still enlisted in a suspended transaction + + logicalConnection.close(); // doesn't really close, but the caller thinks it did, which is what we want + + // We record that a close was pending. + assertThat(logicalConnection.isClosePending(), is(true)); + + // A pending close looks to the caller like a real one. + assertThat(logicalConnection.isClosed(), is(true)); + + // But it's not. + assertThat(physicalConnection.isClosed(), is(false)); + + tm.begin(); + t = tm.getTransaction(); + assertThat(t, not(s)); + assertThat(t.getStatus(), is(Status.STATUS_ACTIVE)); + assertThat(s.getStatus(), is(Status.STATUS_ACTIVE)); + assertThat(tm.getStatus(), is(Status.STATUS_ACTIVE)); + + Connection physicalConnection2 = h2ds.getConnection(); + JtaConnection logicalConnection2 = + new JtaConnection(tm::getTransaction, + tsr, + true, + null, + physicalConnection2, + true); + + assertThat(logicalConnection2.enlisted(), is(true)); + assertThat(logicalConnection2.delegate(), sameInstance(physicalConnection2)); + + tm.commit(); + + assertThat(t.getStatus(), is(Status.STATUS_COMMITTED)); + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + + assertThat(logicalConnection2.enlisted(), is(false)); // we can call this because we haven't closed yet + + assertThat(logicalConnection2.isCloseable(), is(true)); + logicalConnection2.close(); + assertThat(logicalConnection2.isClosed(), is(true)); + assertThat(physicalConnection2.isClosed(), is(true)); + + tm.resume(s); + + t = tm.getTransaction(); + assertThat(t, sameInstance(s)); + assertThat(t.getStatus(), is(Status.STATUS_ACTIVE)); + + // The first logical connection still looks closed. + assertThat(logicalConnection.isClosed(), is(true)); + + // But it's not. + assertThat(logicalConnection.isClosePending(), is(true)); + assertThat(physicalConnection.isClosed(), is(false)); + + tm.commit(); + + // Now it should be closed for real. + assertThat(logicalConnection.isClosePending(), is(false)); + assertThat(logicalConnection.isClosed(), is(true)); + assertThat(logicalConnection.delegate().isClosed(), is(true)); + assertThat(physicalConnection.isClosed(), is(true)); + + assertThat(logicalConnection2.isClosed(), is(true)); + assertThat(physicalConnection2.isClosed(), is(true)); + + LOGGER.info("Ending testBeginSuspendBeginCommitResumeCommit()"); + } + +} diff --git a/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestTransactionSpecificConnection.java b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestTransactionSpecificConnection.java new file mode 100644 index 00000000000..9792ea77f55 --- /dev/null +++ b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestTransactionSpecificConnection.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import com.zaxxer.hikari.pool.ProxyConnection; +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.Status; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +@Deprecated(forRemoval = true, since = "3.1.1") +final class TestTransactionSpecificConnection { + + private JdbcDataSource h2ds; + + private TransactionManager tm; + + private TestTransactionSpecificConnection() { + super(); + } + + @BeforeEach + void initializeH2DataSource() throws SQLException, SystemException { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:test"); + ds.setUser("sa"); + ds.setPassword("sa"); + this.h2ds = ds; + this.tm = com.arjuna.ats.jta.TransactionManager.transactionManager(); + this.tm.setTransactionTimeout(20 * 60); // 20 minutes for debugging + } + + @Test + void testConnectionPoolSemantics() throws IllegalAccessException, NoSuchFieldException, SQLException { + Field delegate = ProxyConnection.class.getDeclaredField("delegate"); + assertThat(delegate.trySetAccessible(), is(true)); + HikariConfig hc = new HikariConfig(); + hc.setDataSource(this.h2ds); + try (HikariDataSource ds = new HikariDataSource(hc)) { + Connection c1 = ds.getConnection(); + Connection d1 = (Connection) delegate.get(c1); + c1.close(); + assertThat(c1.isClosed(), is(true)); + assertThat(d1.isClosed(), is(false)); + + Connection c2 = ds.getConnection(); + Connection d2 = (Connection) delegate.get(c2); + c2.close(); + assertThat(c2.isClosed(), is(true)); + assertThat(c1, not(sameInstance(c2))); + assertThat(d1, sameInstance(d2)); + } + } + + @Test + void testCloseableAndClosedBehavior() throws SQLException { + @SuppressWarnings("removal") + io.helidon.integrations.jta.jdbc.JtaDataSource.TransactionSpecificConnection c = + new io.helidon.integrations.jta.jdbc.JtaDataSource.TransactionSpecificConnection(this.h2ds.getConnection()); + + assertThat(c.isCloseable(), is(false)); + assertThat(c.isClosed(), is(false)); + assertThat(c.getAutoCommit(), is(false)); + + c.close(); // no-op + assertThat(c.isCloseable(), is(false)); + assertThat(c.isClosed(), is(false)); + assertThat(c.isCloseCalled(), is(true)); + + c.setCloseable(true); + assertThat(c.isCloseable(), is(true)); + assertThat(c.isCloseCalled(), is(true)); // still + assertThat(c.isClosed(), is(false)); + + c.close(); // the real thing + assertThat(c.isClosed(), is(true)); + assertThat(c.isCloseCalled(), is(true)); + assertThat(c.isCloseable(), is(false)); + } + + @Test + void testTransactionManagerSemantics() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SQLException, + SystemException { + TransactionManager tm = com.arjuna.ats.jta.TransactionManager.transactionManager(); + tm.setTransactionTimeout(20 * 60); // 20 minutes for debugging + tm.begin(); + Transaction t = tm.getTransaction(); + + // The TransactionSpecificConnection class does not interact + // with the TransactionManager in any way by itself; prove + // that this is the case. + @SuppressWarnings("removal") + io.helidon.integrations.jta.jdbc.JtaDataSource.TransactionSpecificConnection c + = new io.helidon.integrations.jta.jdbc.JtaDataSource.TransactionSpecificConnection(this.h2ds.getConnection()); + + // (The jakarta.transaction.Transaction/TransactionManager + // state machine is weirder than weird.) + t.commit(); // unnecessary + tm.commit(); + + // Committing the Transaction results in STATUS_COMMITTED. + // Makes sense. + assertThat(t.getStatus(), is(Status.STATUS_COMMITTED)); + + // Committing the TransactionManager results in + // STATUS_NO_TRANSACTION. + // (https://jakarta.ee/specifications/transactions/2.0/jakarta-transactions-spec-2.0.html#completing-a-transaction). + // Also makes sense. What may not immediately make sense is + // why they're not the same state. See + // https://groups.google.com/g/narayana-users/c/eYVUmhE9QZg. + assertThat(tm.getStatus(), is(Status.STATUS_NO_TRANSACTION)); + + assertThat(c.isCloseable(), is(false)); + + c.close(); + assertThat(c.isClosed(), is(false)); + + c.setCloseable(true); + assertThat(c.isCloseable(), is(true)); + + c.close(); + assertThat(c.isClosed(), is(true)); + } + +} diff --git a/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestXAStartCommitEnd.java b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestXAStartCommitEnd.java new file mode 100644 index 00000000000..a71686b614b --- /dev/null +++ b/integrations/jta/jdbc/src/test/java/io/helidon/integrations/jta/jdbc/TestXAStartCommitEnd.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * 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.jdbc; + +import java.sql.SQLException; +import java.util.logging.Logger; + +import javax.transaction.xa.XAResource; + +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; + +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static javax.transaction.xa.XAResource.TMSUCCESS; +import static javax.transaction.xa.XAResource.TMFAIL; +import static jakarta.transaction.Status.STATUS_COMMITTED; +import static jakarta.transaction.Status.STATUS_NO_TRANSACTION; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class TestXAStartCommitEnd { + + private static final Logger LOGGER = Logger.getLogger(TestXAStartCommitEnd.class.getName()); + + private JdbcDataSource h2ds; + + private TransactionManager tm; + + private TestXAStartCommitEnd() throws SQLException { + super(); + } + + @BeforeEach + final void initializeH2DataSource() throws SQLException { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:test"); + ds.setUser("sa"); + ds.setPassword("sa"); + this.h2ds = ds; + } + + @BeforeEach + final void initializeTransactionManager() throws SystemException { + this.tm = com.arjuna.ats.jta.TransactionManager.transactionManager(); + this.tm.setTransactionTimeout(20 * 60); // 20 minutes for debugging + } + + @AfterEach + final void rollback() throws SQLException, SystemException { + switch (this.tm.getStatus()) { + case STATUS_NO_TRANSACTION: + break; + default: + this.tm.rollback(); + break; + } + this.tm.setTransactionTimeout(0); + } + + @Test + final void testEnlistXARTwice() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testEnlistXARTwice()"); + tm.begin(); + Transaction t = tm.getTransaction(); + XAResource xaResource = new NoOpXAResource(); + t.enlistResource(xaResource); + + // This is actually interesting. Narayana checks to see if an + // XAResource that is equal (using Object#equals(Object)) to + // the supplied XAResource exists in its guts. This is + // effectively testing to see if the *resource* has been seen + // before. + // + // If one does, then obviously it is not reenlisted and this + // second enlistment request is effectively ignored. In our + // case, this is all true, so this second enlistment is a + // no-op. + // + // If one does not, then TransactionImple#isNewRM(XAResource) + // is called. It's not a boolean-returning method. It loops + // over all known XAResources and calls their + // isSameRM(XAResource) method. This is effectively testing to + // see if the *system to which the resource is connected* (the + // resource manager for which the XA resource is an adapter) + // has been seen before. + // + // If it finds one, then the "first registered RM instance + // [the existing one, not the new one] will be used to drive + // the transaction completion. We add it [the new one, not the + // existing one] to the duplicateResource list so we can + // delist it correctly later though." This means more or less + // whatever you do there will only be one XAResource that + // deals with the prepare/commit/rollback cycle, but there + // could potentially be several that take part in the + // start/end cycle. + // + // In this second case described above, start would be called + // this time with TMJOIN. See also: + // https://github.com/jbosstm/narayana/blob/c5f02d07edb34964b64341974ab689ea44536603/ArjunaJTA/jdbc/classes/com/arjuna/ats/internal/jdbc/drivers/modifiers/ConnectionModifier.java#L82-L89 + t.enlistResource(xaResource); + tm.commit(); + assertThat(t.getStatus(), is(STATUS_COMMITTED)); + assertThat(tm.getStatus(), is(STATUS_NO_TRANSACTION)); + LOGGER.info("Ending testEnlistXARTwice()"); + } + + @Test + final void testTransactionCommitInsteadOfTransactionManagerCommitBlocksThings() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testTransactionCommitInsteadOfTransactionManagerCommitBlocksThings()"); + + tm.begin(); + Transaction t = tm.getTransaction(); + t.commit(); + assertThat(t.getStatus(), is(STATUS_COMMITTED)); + assertThat(tm.getStatus(), is(STATUS_COMMITTED)); + assertThrows(NotSupportedException.class, tm::begin); + tm.commit(); + assertThat(t.getStatus(), is(STATUS_COMMITTED)); + assertThat(tm.getStatus(), is(STATUS_NO_TRANSACTION)); + + LOGGER.info("Ending testTransactionCommitInsteadOfTransactionManagerCommitBlocksThings()"); + } + + @Test + final void testBeginCommitEnlistAndDelistWithTMSUCCESS() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testBeginCommitEnlistAndDelistWithTMSUCCESS()"); + + XAResource nop = new NoOpXAResource(); + tm.begin(); + Transaction t = tm.getTransaction(); + + // https://jakarta.ee/specifications/transactions/2.0/jakarta-transactions-spec-2.0.html#resource-enlistment + // + // "The enlistResource request results in the transaction + // manager informing the resource manager to start associating + // the transaction with the work performed through the + // corresponding resource—by invoking the XAResource.start + // method [on the same thread]." So this causes + // XAResource#start to be invoked. + t.enlistResource(nop); + + // Note that (at least with TMSUCCESS) explicit delisting does + // NOT prevent the XAResource from being enrolled! + assertThat(t.delistResource(nop, TMSUCCESS), is(true)); + tm.commit(); + + LOGGER.info("Ending testBeginCommitEnlistAndDelistWithTMSUCCESS()"); + } + + @Test + final void testBeginCommitDelistWithTMSUCCESS() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testBeginCommitDelistWithTMSUCCESS()"); + + XAResource nop = new NoOpXAResource(); + tm.begin(); + Transaction t = tm.getTransaction(); + // Try delisting before ever enlisting; should be false + assertThat(t.delistResource(nop, TMSUCCESS), is(false)); + tm.commit(); + + LOGGER.info("Ending testBeginCommitDelistWithTMSUCCESS()"); + } + + @Test + final void testBeginCommitEnlistAndDelistWithTMFAIL() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testBeginCommitEnlistAndDelistWithTMFAIL()"); + + XAResource nop = new NoOpXAResource(); + tm.begin(); + Transaction t = tm.getTransaction(); + t.enlistResource(nop); + // Note that (at least with TMFAIL) explicit delisting does + // NOT prevent the XAResource from being enrolled! + assertThat(t.delistResource(nop, TMFAIL), is(true)); + tm.commit(); + + LOGGER.info("Ending testBeginCommitEnlistAndDelistWithTMFAIL()"); + } + + @Test + final void testBeginCommitWithXAResourceThatEnlistsItselfThenRegistersAsASynchronization() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testBeginCommitWithXAResourceThatEnlistsItselfThenRegistersAsASynchronization()"); + + NoOpXAResource noOpXAResource = new NoOpXAResource(); + tm.begin(); + Transaction t = tm.getTransaction(); + t.enlistResource(noOpXAResource); + t.registerSynchronization(noOpXAResource); + + // There is no need, by spec, to delist a resource explicitly. + // assertThat(t.delistResource(noOpXAResource, TMSUCCESS), is(true)); + + // Don't call Transaction#commit(); that does not perform + // disassociation. Only TransactionManager#commit() does + // that. It is also obliged by spec to delist resources. + tm.commit(); + + // You can't delist a resource when the transaction is no + // longer active. + assertThrows(IllegalStateException.class, () -> t.delistResource(noOpXAResource, TMSUCCESS)); + + LOGGER.info("Ending testBeginCommitWithXAResourceThatEnlistsItselfThenRegistersAsASynchronization()"); + } + + @Test + final void testBeginCommitWithSynchronizationThatEnlistsItselfDuringBeforeCompletion() + throws HeuristicMixedException, + HeuristicRollbackException, + NotSupportedException, + RollbackException, + SystemException { + LOGGER.info("Starting testBeginCommitWithSynchronizationThatEnlistsItselfDuringBeforeCompletion()"); + + NoOpXAResource noOpXAResource = new NoOpXAResource(this.tm); + tm.begin(); + Transaction t = tm.getTransaction(); + t.registerSynchronization(noOpXAResource); + tm.commit(); + + LOGGER.info("Ending testBeginCommitWithSynchronizationThatEnlistsItselfDuringBeforeCompletion()"); + } + +} diff --git a/integrations/jta/jdbc/src/test/resources/logging.properties b/integrations/jta/jdbc/src/test/resources/logging.properties new file mode 100644 index 00000000000..068b6b702a3 --- /dev/null +++ b/integrations/jta/jdbc/src/test/resources/logging.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# +# 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=INFO +com.arjuna.ats.level=FINER +#com.arjuna.ats.jdbc.level=FINER +h2database.level=FINE +handlers=java.util.logging.ConsoleHandler +io.helidon.integrations.jta.jdbc.level=FINER +java.util.logging.ConsoleHandler.level=FINEST +org.h2.level=FINE