From 1a1c5137acf253fe7704dbf8a53ddbe5d76b4e65 Mon Sep 17 00:00:00 2001 From: Laird Nelson Date: Tue, 7 Jun 2022 10:42:59 -0700 Subject: [PATCH] Adds XA support to Helidon's UCP integration (#4291) Signed-off-by: Laird Nelson --- .../ucp/cdi/UCPBackedDataSourceExtension.java | 147 +++++++++--------- .../ucp/cdi/TestDataSourceAcquisition.java | 9 +- .../src/test/resources/application.yaml | 8 +- 3 files changed, 85 insertions(+), 79 deletions(-) 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 883ac4fb0e3..e5fc759c2a8 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, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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. @@ -21,6 +21,8 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.regex.Matcher; @@ -40,7 +42,10 @@ import jakarta.enterprise.util.TypeLiteral; import jakarta.inject.Named; import oracle.ucp.jdbc.PoolDataSource; +import oracle.ucp.jdbc.PoolDataSourceFactory; import oracle.ucp.jdbc.PoolDataSourceImpl; +import oracle.ucp.jdbc.PoolXADataSource; +import oracle.ucp.jdbc.PoolXADataSourceImpl; /** * An {@link Extension} that arranges for named {@link DataSource} @@ -51,13 +56,18 @@ public class UCPBackedDataSourceExtension extends AbstractDataSourceExtension { private static final Pattern DATASOURCE_NAME_PATTERN = - Pattern.compile("^(?:javax\\.sql\\.|oracle\\.ucp\\.jdbc\\.Pool)DataSource\\.([^.]+)\\.(.*)$"); + Pattern.compile("^(?:javax\\.sql\\.|oracle\\.ucp\\.jdbc\\.Pool)(XA)?DataSource\\.([^.]+)\\.(.*)$"); + // Capturing groups: (1 ) (2 ) (3 ) + // Are we XA? DS name DS Property + + private final Map xa; /** * Creates a new {@link UCPBackedDataSourceExtension}. */ public UCPBackedDataSourceExtension() { super(); + this.xa = new HashMap<>(); } @Override @@ -77,7 +87,10 @@ protected final String getDataSourceName(final Matcher dataSourcePropertyPattern if (dataSourcePropertyPatternMatcher == null) { returnValue = null; } else { - returnValue = dataSourcePropertyPatternMatcher.group(1); + returnValue = dataSourcePropertyPatternMatcher.group(2); + // While we have the Matcher available, store whether this + // is XA or not. + this.xa.put(returnValue, dataSourcePropertyPatternMatcher.group(1) != null); } return returnValue; } @@ -88,7 +101,7 @@ protected final String getDataSourcePropertyName(final Matcher dataSourcePropert if (dataSourcePropertyPatternMatcher == null) { returnValue = null; } else { - returnValue = dataSourcePropertyPatternMatcher.group(2); + returnValue = dataSourcePropertyPatternMatcher.group(3); } return returnValue; } @@ -97,13 +110,14 @@ protected final String getDataSourcePropertyName(final Matcher dataSourcePropert protected final void addBean(final BeanConfigurator beanConfigurator, final Named dataSourceName, final Properties dataSourceProperties) { + final boolean xa = this.xa.get(dataSourceName.value()); beanConfigurator .addQualifier(dataSourceName) - .addTransitiveTypeClosure(PoolDataSourceImpl.class) + .addTransitiveTypeClosure(xa ? PoolXADataSourceImpl.class : PoolDataSourceImpl.class) .scope(ApplicationScoped.class) .produceWith(instance -> { try { - return createDataSource(instance, dataSourceName, dataSourceProperties); + return createDataSource(instance, dataSourceName, xa, dataSourceProperties); } catch (final IntrospectionException | ReflectiveOperationException | SQLException exception) { throw new CreationException(exception.getMessage(), exception); } @@ -121,14 +135,16 @@ protected final void addBean(final BeanConfigurator beanConfigurator }); } - private static PoolDataSourceImpl createDataSource(final Instance instance, - final Named dataSourceName, - final Properties properties) + private static PoolDataSource createDataSource(final Instance instance, + final Named dataSourceName, + final boolean xa, + final 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 PoolDataSourceImpl returnValue = new PoolDataSourceImpl(); + final PoolDataSource returnValue = + xa ? PoolDataSourceFactory.getPoolXADataSource() : PoolDataSourceFactory.getPoolDataSource(); final Set propertyNames = properties.stringPropertyNames(); if (!propertyNames.isEmpty()) { final Properties connectionFactoryProperties = new Properties(); @@ -139,20 +155,13 @@ private static PoolDataSourceImpl createDataSource(final Instance instan boolean handled = false; for (final 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 + // 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(); if (writeMethod != null) { @@ -174,49 +183,29 @@ private static PoolDataSourceImpl createDataSource(final Instance instan } } 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 + // (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. + // 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. connectionFactoryProperties.setProperty(propertyName, properties.getProperty(propertyName)); } } @@ -224,17 +213,13 @@ private static PoolDataSourceImpl createDataSource(final Instance instan final Object serviceName = connectionFactoryProperties.remove("serviceName"); final Object pdbRoles = connectionFactoryProperties.remove("pdbRoles"); if (!connectionFactoryProperties.stringPropertyNames().isEmpty()) { - // We found some String-typed properties that are - // destined for the underlying connection factory to + // We found some String-typed properties that are destined for the underlying connection factory to // 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 - // certain (irrelevant for Helidon) application server - // cases. + // 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 { Method m = returnValue.getClass().getDeclaredMethod("setServiceName", String.class); @@ -245,10 +230,8 @@ private static PoolDataSourceImpl createDataSource(final Instance instan } } - // 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 { @@ -266,7 +249,17 @@ private static PoolDataSourceImpl createDataSource(final Instance instan returnValue.setSSLContext(sslContextInstance.get()); } // Permit further customization before the bean is actually created - instance.select(new TypeLiteral>() {}, dataSourceName).get().fire(returnValue); + if (xa) { + instance.select(new TypeLiteral>() {}, + dataSourceName) + .get() + .fire((PoolXADataSource) returnValue); + } else { + instance.select(new TypeLiteral>() {}, + dataSourceName) + .get() + .fire(returnValue); + } return returnValue; } diff --git a/integrations/cdi/datasource-ucp/src/test/java/io/helidon/integrations/datasource/ucp/cdi/TestDataSourceAcquisition.java b/integrations/cdi/datasource-ucp/src/test/java/io/helidon/integrations/datasource/ucp/cdi/TestDataSourceAcquisition.java index 597f1c6cfe7..1a2a5f038fd 100644 --- a/integrations/cdi/datasource-ucp/src/test/java/io/helidon/integrations/datasource/ucp/cdi/TestDataSourceAcquisition.java +++ b/integrations/cdi/datasource-ucp/src/test/java/io/helidon/integrations/datasource/ucp/cdi/TestDataSourceAcquisition.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2021 Oracle and/or its affiliates. + * Copyright (c) 2019, 2022 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. @@ -29,6 +29,7 @@ import jakarta.inject.Named; import oracle.ucp.jdbc.PoolDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; +import oracle.ucp.jdbc.PoolXADataSource; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.jboss.weld.proxy.WeldClientProxy; import org.junit.jupiter.api.AfterEach; @@ -101,6 +102,12 @@ private void configure(@Observes @Named("test") final PoolDataSource pds) throws pds.setDescription("A test datasource"); } + private void configure(@Observes @Named("testxa") final PoolXADataSource pds) throws SQLException { + assertNull(pds.getDescription()); + assertFalse(pds.getClass().isSynthetic()); + pds.setDescription("A test datasource"); + } + @Test void testDataSourceAcquisition() { diff --git a/integrations/cdi/datasource-ucp/src/test/resources/application.yaml b/integrations/cdi/datasource-ucp/src/test/resources/application.yaml index ad73fed3fc5..a668a66a5d6 100644 --- a/integrations/cdi/datasource-ucp/src/test/resources/application.yaml +++ b/integrations/cdi/datasource-ucp/src/test/resources/application.yaml @@ -1,5 +1,5 @@ # -# Copyright (c) 2019, 2021 Oracle and/or its affiliates. +# Copyright (c) 2019, 2022 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,3 +23,9 @@ oracle: password: "${EMPTY}" user: sa serviceName: fred + PoolXADataSource: + testxa: + URL: jdbc:h2:mem:test + connectionFactoryClassName: org.h2.jdbcx.JdbcDataSource + password: "${EMPTY}" + user: sa