* Composes an Oracle JDBC URL from {@code ConnectionFactoryOptions}, as
* specified in the javadoc of
* {@link #createDataSource(ConnectionFactoryOptions)}
+ *
+ * If the {@link ConnectionFactoryOptions#SSL} option is set, then the JDBC
+ * URL is composed with the tcps protocol, as in:
+ * {@code jdbc:oracle:thins:@tcps:...}. The {@code SSL} option is interpreted
+ * as a strict directive to use TLS, and so it takes precedence over any value
+ * that may otherwise be specified by the {@code PROTOCOL} option.
+ *
+ * If the {@code SSL} option is not set, then URL is composed with the any
+ * value set for {@link ConnectionFactoryOptions#PROTOCOL} option. For
+ * instance, if the {@code PROTOCOL} option is set to "ldap" then the URL
+ * is composed as: {@code jdbc:oracle:thins:@ldap://...}.
+ *
+ * For consistency with the Oracle JDBC URL, an Oracle R2DBC URL might include
+ * multiple space separated LDAP addresses, like this:
+ *
+ * The %20 encoding of the space character must be used in order for
+ * {@link ConnectionFactoryOptions#parse(CharSequence)} to recognize the URL
+ * syntax. When multiple address are specified this way, the {@code DATABASE}
+ * option will have the value of:
+ *
* @param options R2DBC options. Not null.
* @return An Oracle JDBC URL composed from R2DBC options
* @throws IllegalArgumentException If the {@code oracleNetDescriptor}
@@ -448,15 +480,18 @@ private static String composeJdbcUrl(ConnectionFactoryOptions options) {
return "jdbc:oracle:thin:@" + descriptor.toString();
}
else {
+ Object protocol =
+ Boolean.TRUE.equals(parseOptionValue(
+ SSL, options, Boolean.class, Boolean::valueOf))
+ ? "tcps"
+ : options.getValue(PROTOCOL);
Object host = options.getRequiredValue(HOST);
Integer port = parseOptionValue(
PORT, options, Integer.class, Integer::valueOf);
Object serviceName = options.getValue(DATABASE);
- Boolean isTcps = parseOptionValue(
- SSL, options, Boolean.class, Boolean::valueOf);
return String.format("jdbc:oracle:thin:@%s%s%s%s",
- Boolean.TRUE.equals(isTcps) ? "tcps:" : "",
+ protocol == null ? "" : protocol + "://",
host,
port != null ? (":" + port) : "",
serviceName != null ? ("/" + serviceName) : "");
diff --git a/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryImplTest.java
index e7cd707..0dd9c0f 100644
--- a/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryImplTest.java
+++ b/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryImplTest.java
@@ -36,6 +36,7 @@
import java.util.HashSet;
import java.util.Set;
+import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -105,16 +106,7 @@ public void testDiscovery() {
@Test
public void testCreate() {
Publisher extends Connection> connectionPublisher =
- new OracleConnectionFactoryImpl(
- ConnectionFactoryOptions.builder()
- .option(ConnectionFactoryOptions.DRIVER, "oracle")
- .option(ConnectionFactoryOptions.HOST, DatabaseConfig.host())
- .option(ConnectionFactoryOptions.PORT, DatabaseConfig.port())
- .option(ConnectionFactoryOptions.DATABASE, DatabaseConfig.serviceName())
- .option(ConnectionFactoryOptions.USER, DatabaseConfig.user())
- .option(ConnectionFactoryOptions.PASSWORD, DatabaseConfig.password())
- .build())
- .create();
+ new OracleConnectionFactoryImpl(connectionFactoryOptions()).create();
// Expect publisher to emit one connection to each subscriber
Set connections = new HashSet<>();
@@ -145,16 +137,11 @@ public void testCreate() {
public void testCreateFailure() {
// Connect with the wrong username
Publisher extends Connection> connectionPublisher =
- new OracleConnectionFactoryImpl(
- ConnectionFactoryOptions.builder()
- .option(ConnectionFactoryOptions.DRIVER, "oracle")
- .option(ConnectionFactoryOptions.HOST, DatabaseConfig.host())
- .option(ConnectionFactoryOptions.PORT, DatabaseConfig.port())
- .option(ConnectionFactoryOptions.DATABASE, DatabaseConfig.serviceName())
- .option(ConnectionFactoryOptions.USER,
- "Wrong" + DatabaseConfig.user())
- .option(ConnectionFactoryOptions.PASSWORD, DatabaseConfig.password())
- .build())
+ new OracleConnectionFactoryImpl(connectionFactoryOptions()
+ .mutate()
+ .option(ConnectionFactoryOptions.USER,
+ "Wrong" + DatabaseConfig.user())
+ .build())
.create();
// Expect publisher to signal onError with an R2DBCException
diff --git a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java
index bdd2b55..0255d54 100644
--- a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java
+++ b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java
@@ -43,6 +43,8 @@
import java.nio.file.StandardOpenOption;
import java.sql.SQLException;
import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -66,9 +68,11 @@
import static io.r2dbc.spi.ConnectionFactoryOptions.USER;
import static java.lang.String.format;
import static oracle.r2dbc.test.DatabaseConfig.connectTimeout;
+import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions;
import static oracle.r2dbc.test.DatabaseConfig.host;
import static oracle.r2dbc.test.DatabaseConfig.password;
import static oracle.r2dbc.test.DatabaseConfig.port;
+import static oracle.r2dbc.test.DatabaseConfig.protocol;
import static oracle.r2dbc.test.DatabaseConfig.serviceName;
import static oracle.r2dbc.test.DatabaseConfig.sharedConnection;
import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout;
@@ -213,9 +217,11 @@ public void testTnsAdmin() throws IOException {
// Create an Oracle Net Descriptor
String descriptor = format(
- "(DESCRIPTION=(ADDRESS=(HOST=%s)(PORT=%d)(PROTOCOL=tcp))" +
+ "(DESCRIPTION=(ADDRESS=(HOST=%s)(PORT=%d)(PROTOCOL=%s))" +
"(CONNECT_DATA=(SERVICE_NAME=%s)))",
- host(), port(), serviceName());
+ host(), port(),
+ Objects.requireNonNullElse(protocol(), "tcp"),
+ serviceName());
// Create a tnsnames.ora file with an alias for the descriptor
Files.writeString(Path.of("tnsnames.ora"),
@@ -365,14 +371,8 @@ public void testConnectTimeout()
@Test
public void testStatementTimeout() {
Connection connection0 =
- Mono.from(ConnectionFactories.get(ConnectionFactoryOptions
- .builder()
- .option(DRIVER, "oracle")
- .option(HOST, host())
- .option(PORT, port())
- .option(DATABASE, serviceName())
- .option(USER, user())
- .option(PASSWORD, password())
+ Mono.from(ConnectionFactories.get(connectionFactoryOptions()
+ .mutate()
.option(STATEMENT_TIMEOUT, Duration.ofSeconds(2))
// Disable OOB to support testing with an 18.x database
.option(Option.valueOf(
@@ -442,14 +442,9 @@ public void testExecutorOption() {
// Create a connection that is configured to use the custom executor
Connection connection = awaitOne(ConnectionFactories.get(
- ConnectionFactoryOptions.builder()
+ connectionFactoryOptions()
+ .mutate()
.option(OracleR2dbcOptions.EXECUTOR, testExecutor)
- .option(DRIVER, "oracle")
- .option(HOST, host())
- .option(PORT, port())
- .option(DATABASE, serviceName())
- .option(USER, user())
- .option(PASSWORD, password())
.build())
.create());
@@ -490,12 +485,15 @@ public void testVSessionOptions() {
// Verify configuration with URL parameters
Connection connection = awaitOne(ConnectionFactories.get(
ConnectionFactoryOptions.parse(
- format("r2dbc:oracle://%s:%d/%s" +
+ format("r2dbc:oracle:%s//%s:%d/%s" +
"?v$session.osuser=%s" +
"&v$session.terminal=%s" +
"&v$session.process=%s" +
"&v$session.program=%s" +
"&v$session.machine=%s",
+ Optional.ofNullable(protocol())
+ .map(protocol -> protocol + ":")
+ .orElse(""),
host(), port(), serviceName(),
osuser, terminal, process, program, machine))
.mutate()
diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
index 09723b8..e9f5578 100644
--- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
+++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
@@ -61,6 +61,7 @@
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static oracle.r2dbc.test.DatabaseConfig.connectTimeout;
+import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions;
import static oracle.r2dbc.test.DatabaseConfig.host;
import static oracle.r2dbc.test.DatabaseConfig.newConnection;
import static oracle.r2dbc.test.DatabaseConfig.password;
@@ -2280,17 +2281,10 @@ public Object getValue() {
* @return Connection that uses the {@code executor}
*/
private static Publisher extends Connection> connect(Executor executor) {
- return ConnectionFactories.get(
- ConnectionFactoryOptions.parse(format(
- "r2dbc:oracle://%s:%d/%s", host(), port(), serviceName()))
- .mutate()
- .option(
- ConnectionFactoryOptions.USER, user())
- .option(
- ConnectionFactoryOptions.PASSWORD, password())
- .option(
- OracleR2dbcOptions.EXECUTOR, executor)
- .build())
+ return ConnectionFactories.get(connectionFactoryOptions()
+ .mutate()
+ .option(OracleR2dbcOptions.EXECUTOR, executor)
+ .build())
.create();
}
diff --git a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java
index e7e4729..133833e 100644
--- a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java
+++ b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java
@@ -49,6 +49,16 @@ public final class DatabaseConfig {
private DatabaseConfig() {}
+ /**
+ * Returns the protocol used to connect with a database, specified as
+ * {@code PROTOCOL} in the "config.properties" file.
+ * @return Connection protocol for the test database. May be {@code null} if
+ * no protocol is configured.
+ */
+ public static String protocol() {
+ return PROTOCOL;
+ }
+
/**
* Returns the hostname of the server where a test database listens for
* connections, specified as {@code HOST} in the "config.properties" file.
@@ -203,6 +213,40 @@ public static void showErrors(Connection connection) {
.forEach(System.err::println);
}
+ /**
+ * Returns the options parsed from the "config.properties" resource.
+ */
+ public static ConnectionFactoryOptions connectionFactoryOptions() {
+
+ ConnectionFactoryOptions.Builder optionsBuilder =
+ ConnectionFactoryOptions.builder()
+ .option(ConnectionFactoryOptions.DRIVER, "oracle")
+ .option(ConnectionFactoryOptions.HOST, HOST)
+ .option(ConnectionFactoryOptions.PORT, PORT)
+ .option(ConnectionFactoryOptions.DATABASE, SERVICE_NAME)
+ .option(ConnectionFactoryOptions.USER, USER)
+ .option(ConnectionFactoryOptions.PASSWORD, PASSWORD)
+ // Disable statement caching in order to verify cursor closing;
+ // Cached statements don't close their cursors
+ .option(Option.valueOf(
+ OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE),
+ 0)
+ // Disable out-of-band breaks to support testing with the 18.x
+ // database. The 19.x database will automatically detect when it's
+ // running on a system where OOB is not supported, but the 18.x
+ // database does not do this and so statement timeout tests will
+ // hang if the database system does not support OOB
+ .option(Option.valueOf(
+ OracleConnection.CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK),
+ "true");
+
+ if (PROTOCOL != null)
+ optionsBuilder.option(ConnectionFactoryOptions.PROTOCOL, PROTOCOL);
+
+ return optionsBuilder.build();
+ }
+
+ private static final String PROTOCOL;
private static final String HOST;
private static final int PORT;
private static final String SERVICE_NAME;
@@ -237,30 +281,9 @@ public static void showErrors(Connection connection) {
Long.parseLong(prop.getProperty("CONNECT_TIMEOUT")));
SQL_TIMEOUT = Duration.ofSeconds(
Long.parseLong(prop.getProperty("SQL_TIMEOUT")));
+ PROTOCOL = prop.getProperty("PROTOCOL");
- CONNECTION_FACTORY = ConnectionFactories.get(
- ConnectionFactoryOptions.builder()
- .option(ConnectionFactoryOptions.DRIVER, "oracle")
- .option(ConnectionFactoryOptions.HOST, HOST)
- .option(ConnectionFactoryOptions.PORT, PORT)
- .option(ConnectionFactoryOptions.DATABASE, SERVICE_NAME)
- .option(ConnectionFactoryOptions.USER, USER)
- .option(ConnectionFactoryOptions.PASSWORD, PASSWORD)
- // Disable statement caching in order to verify cursor closing;
- // Cached statements don't close their cursors
- .option(Option.valueOf(
- OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE),
- 0)
- // Disable out-of-band breaks to support testing with the 18.x
- // database. The 19.x database will automatically detect when it's
- // running on a system where OOB is not supported, but the 18.x
- // database does not do this and so statement timeout tests will
- // hang if the database system does not support OOB
- .option(Option.valueOf(
- OracleConnection.CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK),
- "true")
- .build());
-
+ CONNECTION_FACTORY = ConnectionFactories.get(connectionFactoryOptions());
SHARED_CONNECTION_FACTORY = new SharedConnectionFactory(
CONNECTION_FACTORY.create(),
CONNECTION_FACTORY.getMetadata());
diff --git a/src/test/java/oracle/r2dbc/test/OracleTestKit.java b/src/test/java/oracle/r2dbc/test/OracleTestKit.java
index c526fff..c2fc5a2 100755
--- a/src/test/java/oracle/r2dbc/test/OracleTestKit.java
+++ b/src/test/java/oracle/r2dbc/test/OracleTestKit.java
@@ -45,6 +45,7 @@
import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Arrays;
+import java.util.Optional;
import java.util.function.Function;
import java.util.stream.IntStream;
@@ -54,9 +55,11 @@
import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
import static io.r2dbc.spi.ConnectionFactoryOptions.PORT;
import static io.r2dbc.spi.ConnectionFactoryOptions.USER;
+import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions;
import static oracle.r2dbc.test.DatabaseConfig.host;
import static oracle.r2dbc.test.DatabaseConfig.password;
import static oracle.r2dbc.test.DatabaseConfig.port;
+import static oracle.r2dbc.test.DatabaseConfig.protocol;
import static oracle.r2dbc.test.DatabaseConfig.serviceName;
import static oracle.r2dbc.test.DatabaseConfig.user;
@@ -91,7 +94,10 @@ public class OracleTestKit implements TestKit {
{
try {
OracleDataSource dataSource = new oracle.jdbc.pool.OracleDataSource();
- dataSource.setURL(String.format("jdbc:oracle:thin:@%s:%d/%s",
+ dataSource.setURL(String.format("jdbc:oracle:thin:@%s%s:%d/%s",
+ Optional.ofNullable(protocol())
+ .map(protocol -> protocol + ":")
+ .orElse(""),
host(), port(), serviceName()));
dataSource.setUser(user());
dataSource.setPassword(password());
@@ -102,18 +108,8 @@ public class OracleTestKit implements TestKit {
}
}
- private final ConnectionFactory connectionFactory;
- {
- connectionFactory = ConnectionFactories.get(
- ConnectionFactoryOptions.builder()
- .option(DRIVER, "oracle")
- .option(DATABASE, serviceName())
- .option(HOST, host())
- .option(PORT, port())
- .option(PASSWORD, password())
- .option(USER, user())
- .build());
- }
+ private final ConnectionFactory connectionFactory =
+ ConnectionFactories.get(connectionFactoryOptions());
public JdbcOperations getJdbcOperations() {
return jdbcOperations;
From 6fb27068261ca301933a118e7379f9af09e4ddf2 Mon Sep 17 00:00:00 2001
From: Michael-A-McMahon
Date: Mon, 11 Jul 2022 13:08:00 -0700
Subject: [PATCH 2/5] Documenting LDAP URLs
---
README.md | 28 ++++++++++++++++++++++------
1 file changed, 22 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index eb1aa45..d8df2ab 100644
--- a/README.md
+++ b/README.md
@@ -191,22 +191,22 @@ are supported by Oracle R2DBC:
- `HOST`
- `PORT`
- `DATABASE`
+ - The database option is interpreted as the
+ [service name](https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/identifying-and-accessing-database.html#GUID-153861C1-16AD-41EC-A179-074146B722E6)
+ of an Oracle Database instance. _System Identifiers (SID) are not recognized_.
- `USER`
- `PASSWORD`
- `SSL`
- `CONNECT_TIMEOUT`
- `STATEMENT_TIMEOUT`.
-
-> Oracle R2DBC interprets the `DATABASE` option as the
-> [service name](https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/identifying-and-accessing-database.html#GUID-153861C1-16AD-41EC-A179-074146B722E6)
-> of an Oracle Database instance. _System Identifiers (SID) are not recognized_.
+ - `PROTOCOL`
+ - (For inclusion in the next release) Accepted protocol values are "tcps", "ldap", and "ldaps"
#### Support for Extended R2DBC Options
Oracle R2DBC extends the standard set of R2DBC options to offer functionality
that is specific to Oracle Database and the Oracle JDBC Driver. Extended options
are declared in the
-[OracleR2dbcOptions](src/main/java/oracle/r2dbc/OracleR2dbcOptions.java)
-class.
+[OracleR2dbcOptions](src/main/java/oracle/r2dbc/OracleR2dbcOptions.java) class.
#### Configuring an Oracle Net Descriptor
The `oracle.r2dbc.OracleR2dbcOptions.DESCRIPTOR` option may be used to configure
@@ -234,6 +234,22 @@ located:
r2dbc:oracle://?oracle.r2dbc.descriptor=myAlias&TNS_ADMIN=/path/to/tnsnames/
```
+#### (For inclusion in the next release) Configuring an LDAP URL
+Use `ldap` or `ldaps` as the URL protocol to have an Oracle Net Descriptor
+retrieved from an LDAP server:
+```
+r2dbc:oracle:ldap://ldap.example.com:7777/sales,cn=OracleContext,dc=com
+r2dbc:oracle:ldaps://ldap.example.com:7778/sales,cn=OracleContext,dc=com
+```
+Use a space separated list of LDAP URIs for fail over and load balancing:
+```
+r2dbc:oracle:ldap://ldap1.example.com:7777/sales,cn=OracleContext,dc=com%20ldap://ldap2.example.com:7777/sales,cn=OracleContext,dc=com%20ldap://ldap3.example.com:7777/sales,cn=OracleContext,dc=com
+```
+> Space characters in a URL must be percent encoded as `%20`
+
+An LDAP server request will **block a thread for network I/O** when Oracle R2DBC
+creates a new connection.
+
#### Configuring a java.util.concurrent.Executor
The `oracle.r2dbc.OracleR2dbcOptions.EXECUTOR` option configures a
`java.util.concurrent.Executor` for executing asynchronous callbacks. The
From 8cf89fc825514b48828ffd860eb880ed85590ba8 Mon Sep 17 00:00:00 2001
From: Michael-A-McMahon
Date: Wed, 13 Jul 2022 12:43:23 -0700
Subject: [PATCH 3/5] Add LDAP tests
---
pom.xml | 13 +-
src/main/java/module-info.java | 1 +
.../impl/OracleReactiveJdbcAdapterTest.java | 106 +++-
.../oracle/r2dbc/util/TestContextFactory.java | 468 ++++++++++++++++++
4 files changed, 581 insertions(+), 7 deletions(-)
create mode 100644 src/test/java/oracle/r2dbc/util/TestContextFactory.java
diff --git a/pom.xml b/pom.xml
index 9f54c67..77260e6 100755
--- a/pom.xml
+++ b/pom.xml
@@ -87,6 +87,17 @@
-Xlint:-processing-Xlint:-serial
+
+
true${java.version}
@@ -150,6 +161,7 @@
maven-surefire-plugin3.0.0-M5
+
**/*Test.java**/*TestKit.java
@@ -278,7 +290,6 @@
reactor-testtest
-
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 796c99e..ce9c4bc 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -30,6 +30,7 @@
with oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl;
requires java.sql;
+ requires java.naming;
requires com.oracle.database.jdbc;
requires reactor.core;
requires transitive org.reactivestreams;
diff --git a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java
index 0255d54..4a01fe6 100644
--- a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java
+++ b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java
@@ -23,6 +23,7 @@
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactories;
+import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Option;
import io.r2dbc.spi.R2dbcTimeoutException;
@@ -31,10 +32,12 @@
import oracle.jdbc.datasource.OracleDataSource;
import oracle.r2dbc.OracleR2dbcOptions;
import oracle.r2dbc.test.DatabaseConfig;
+import oracle.r2dbc.util.TestContextFactory;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import javax.naming.spi.NamingManager;
import java.io.IOException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
@@ -216,12 +219,7 @@ public void testCreateDataSource() throws SQLException {
public void testTnsAdmin() throws IOException {
// Create an Oracle Net Descriptor
- String descriptor = format(
- "(DESCRIPTION=(ADDRESS=(HOST=%s)(PORT=%d)(PROTOCOL=%s))" +
- "(CONNECT_DATA=(SERVICE_NAME=%s)))",
- host(), port(),
- Objects.requireNonNullElse(protocol(), "tcp"),
- serviceName());
+ String descriptor = createDescriptor();
// Create a tnsnames.ora file with an alias for the descriptor
Files.writeString(Path.of("tnsnames.ora"),
@@ -525,6 +523,102 @@ public void testVSessionOptions() {
tryAwaitNone(connection.close());
}
}
+ /**
+ * Verifies the use of the LDAP protocol in an r2dbc:oracle URL.
+ */
+ @Test
+ public void testLdapUrl() throws Exception {
+
+ // Configure Oracle R2DBC with an R2DBC URL having the LDAP protocol and the
+ // given path.
+ String ldapPath = "sales,cn=OracleContext,dc=com";
+ ConnectionFactory ldapConnectionFactory = ConnectionFactories.get(
+ ConnectionFactoryOptions.parse(format(
+ "r2dbc:oracle:ldap://ldap.example.com:9999/%s", ldapPath))
+ .mutate()
+ .option(ConnectionFactoryOptions.USER, DatabaseConfig.user())
+ .option(ConnectionFactoryOptions.PASSWORD, DatabaseConfig.password())
+ .build());
+
+ // Set up the mock LDAP context factory. See JavaDoc of TestContextFactory
+ // for details about this.
+ NamingManager.setInitialContextFactoryBuilder(environment ->
+ new TestContextFactory());
+ TestContextFactory.bind(ldapPath, createDescriptor());
+
+ // Now verify that the LDAP URL is resolved to the descriptor
+ Connection ldapConnection = awaitOne(ldapConnectionFactory.create());
+ try {
+ assertEquals(
+ "Hello, LDAP",
+ awaitOne(
+ awaitOne(ldapConnection.createStatement(
+ "SELECT 'Hello, LDAP' FROM sys.dual")
+ .execute())
+ .map(row -> row.get(0))));
+ }
+ finally {
+ tryAwaitNone(ldapConnection.close());
+ }
+ }
+
+ /**
+ * Verifies the use of the LDAP protocol in an r2dbc:oracle URL having
+ * multiple LDAP endpoints
+ */
+ @Test
+ public void testMultiLdapUrl() throws Exception {
+
+ // Configure Oracle R2DBC with an R2DBC URL having the LDAP protocol and
+ // multiple LDAP endpoints. Only the last endpoint will contain the given
+ // path, and so the previous endpoints are invalid.
+ String ldapPath = "cn=salesdept,cn=OracleContext,dc=com/salesdb";
+ ConnectionFactory ldapConnectionFactory = ConnectionFactories.get(
+ ConnectionFactoryOptions.parse(format(
+ "r2dbc:oracle:" +
+ "ldap://ldap1.example.com:7777/cn=salesdept0,cn=OracleContext,dc=com/salesdb" +
+ "%%20ldap://ldap1.example.com:7777/cn=salesdept1,cn=OracleContext,dc=com/salesdb" +
+ "%%20ldap://ldap3.example.com:7777/%s", ldapPath))
+ .mutate()
+ .option(ConnectionFactoryOptions.USER, DatabaseConfig.user())
+ .option(ConnectionFactoryOptions.PASSWORD, DatabaseConfig.password())
+ .build());
+
+ // Set up the mock LDAP context factory. A descriptor is bound to the last
+ // endpoint only. See JavaDoc of TestContextFactory for details about this.
+ NamingManager.setInitialContextFactoryBuilder(environment ->
+ new TestContextFactory());
+ TestContextFactory.bind("salesdb", createDescriptor());
+
+ // Now verify that the LDAP URL is resolved to the descriptor
+ Connection ldapConnection = awaitOne(ldapConnectionFactory.create());
+ try {
+ assertEquals(
+ "Hello, LDAP",
+ awaitOne(
+ awaitOne(ldapConnection.createStatement(
+ "SELECT 'Hello, LDAP' FROM sys.dual")
+ .execute())
+ .map(row -> row.get(0))));
+ }
+ finally {
+ tryAwaitNone(ldapConnection.close());
+ }
+ }
+
+ /**
+ * Returns an Oracle Net Descriptor having the values configured by
+ * {@link DatabaseConfig}
+ * @return An Oracle Net Descriptor for the test database.
+ */
+ private static String createDescriptor() {
+ return format(
+ "(DESCRIPTION=(ADDRESS=(HOST=%s)(PORT=%d)(PROTOCOL=%s))" +
+ "(CONNECT_DATA=(SERVICE_NAME=%s)))",
+ host(), port(),
+ Objects.requireNonNullElse(protocol(), "tcp"),
+ serviceName());
+ }
/**
* Verifies that an attempt to connect with a {@code listeningChannel}
diff --git a/src/test/java/oracle/r2dbc/util/TestContextFactory.java b/src/test/java/oracle/r2dbc/util/TestContextFactory.java
new file mode 100644
index 0000000..e0c2f1e
--- /dev/null
+++ b/src/test/java/oracle/r2dbc/util/TestContextFactory.java
@@ -0,0 +1,468 @@
+/*
+ Copyright (c) 2020, 2022, Oracle and/or its affiliates.
+
+ This software is dual-licensed to you under the Universal Permissive License
+ (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License
+ 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
+ either license.
+
+ 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
+
+ https://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 oracle.r2dbc.util;
+
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.NameClassPair;
+import javax.naming.NameParser;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import java.util.HashMap;
+import java.util.Hashtable;
+
+/**
+ *
+ * A mock implementation of the JNDI
+ * {@link javax.naming.spi.InitialContextFactory} SPI. This class is used for
+ * testing LDAP URLs with Oracle R2DBC.
+ *
+ *
Registering this Factory
+ * When an LDAP URL is configured, the underlying Oracle JDBC Driver will do
+ * something like this:
+ *
{@code
+ * var properties = new Properties()
+ * properties.setProperty(
+ * Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ * DirContext dirContext = new InitialDirContext(properties);
+ * }
+ * Note how JDBC has hardcoded the Sun LDAP factory. Test code needs to override
+ * this, such that the DirContext object seen above will delegate to an instance
+ * created by this test factory. This can be accomplished by registering a
+ * factory builder that outputs an instance of this class:
+ *
+ * When a factory builder is registered, the InitialDirContext constructor
+ * ignores JDBC's configuration. Instead of using the Sun factory, the
+ * constructor will use this test factory.
+ *
+ *
Binding Oracle Net Descriptors
+ * After Oracle JDBC gets the DirContext object (see previous section), it will
+ * query it for an Oracle Net Descriptor. Oracle JDBC then uses this descriptor
+ * to connect to Oracle Database (just as if the descriptor had been given as
+ * a URL or as a tnsnames.ora entry). The name that JDBC queries for is
+ * extracted from the path component of the LDAP URL. For example, if JDBC is
+ * given an LDAP URL of:
+ *
+ * In the returned Attributes object, Oracle JDBC reads the descriptor
+ * from the first value of the first attribute. An invocation of
+ * {@link TestContextFactory#bind(String, String)} will bind a single attribute
+ * with a single value to a given name. For example:
+ *
+ * The code above would have JDBC connect to the given descriptor.
+ *
+ */
+public final class TestContextFactory
+ implements javax.naming.spi.InitialContextFactory {
+
+ /**
+ * The {@code DirContext} object constructed by Oracle JDBC will delegate to
+ * this instance of {@code TestDirContext}.
+ */
+ private static final TestDirContext DIR_CONTEXT = new TestDirContext();
+
+ /**
+ * {@inheritDoc}
+ * Ignores the {@code environment} configured by Oracle JDBC, and just returns
+ * the fixed instance of {@code TestDirContext}
+ */
+ @Override
+ public Context getInitialContext(Hashtable, ?> environment)
+ throws NamingException {
+ TestDirContext.environment = (Hashtable, ?>) environment.clone();
+ return DIR_CONTEXT;
+ }
+
+ /**
+ * {@inheritDoc}
+ * Binds the given name to the given value. This can be called to map an
+ * Oracle Net Descriptor to the path of an LDAP URL.
+ */
+ public static void bind(String name, String value) {
+ TestDirContext.ATTRIBUTES.put(name, new BasicAttributes(name, value));
+ }
+
+ /**
+ * Implements the methods of the {@code DirContext} SPI which are called by
+ * Oracle JDBC. All other method will throw an exception. This implementation
+ * is simply backed by a mapping between names and attributes.
+ */
+ private static final class TestDirContext implements DirContext {
+
+ /** Maps names to attributes */
+ private static final HashMap ATTRIBUTES =
+ new HashMap<>();
+
+ /**
+ * The environment of this context. It is passed to
+ * {@link TestDirContext#getInitialContext(Hashtable)} when returning this
+ * context.
+ */
+ private static Hashtable, ?> environment = new Hashtable<>();
+
+ /**
+ * {@inheritDoc}
+ * Returns the attribute that has been mapped to a given name. Oracle JDBC
+ * calls this method to get an Oracle Net Descriptor.
+ */
+ @Override
+ public Attributes getAttributes(String name, String[] attrIds)
+ throws NamingException {
+ Attributes attributes = ATTRIBUTES.get(name);
+
+ // It is noted that JDBC will prefix "cn=" on the path component of an
+ // LDAP URL. Check if this is why the look up fails.
+ if (attributes == null && name.startsWith("cn="))
+ attributes = ATTRIBUTES.get(name.substring("cn=".length()));
+
+ if (attributes == null)
+ throw new NamingException("No attribute found for name: " + name);
+
+ return attributes;
+ }
+
+ /**
+ * {@inheritDoc}
+ * JDBC calls this method when resolving a multi-endpoint LDAP URL.
+ */
+ @Override
+ public Hashtable, ?> getEnvironment() throws NamingException {
+
+ String providerUrl = (String)environment.get(Context.PROVIDER_URL);
+ if (providerUrl != null) {
+
+ // Replicating com.sun.jndi.ldap.LdapCtxFactory. When the provider URL is
+ // a space-separated list, it returns the first working URL. JDBC then
+ // calls getAttributes with the last path element that was mapped to this
+ // URL. So if the working URL was: ldap://host:port/cn=.../db, then JDBC
+ // calls getAttributes with "cn=db"
+ String[] urls = providerUrl.split(" ");
+
+ if (urls.length == 1)
+ return environment;
+
+ // Return the first URL. Don't include the ldap: scheme.
+ var url = urls[0].substring(urls[0].indexOf('/'));
+ @SuppressWarnings("unchecked")
+ Hashtable