Skip to content

Commit 1fc6709

Browse files
Add oracle-net-descriptor option
1 parent a4c8dc4 commit 1fc6709

File tree

4 files changed

+158
-78
lines changed

4 files changed

+158
-78
lines changed

sample/src/main/java/oracle/r2dbc/samples/DescriptorURL.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import io.r2dbc.spi.ConnectionFactories;
2525
import io.r2dbc.spi.ConnectionFactoryOptions;
26+
import io.r2dbc.spi.Option;
2627
import reactor.core.publisher.Mono;
2728

2829
import static oracle.r2dbc.samples.DatabaseConfig.HOST;
@@ -53,8 +54,8 @@ public class DescriptorURL {
5354
"(CONNECT_DATA=(SERVICE_NAME="+SERVICE_NAME+")))";
5455

5556
public static void main(String[] args) {
56-
// A descriptor may appear in the host section of an R2DBC URL:
57-
String r2dbcUrl = "r2dbc:oracle://"+DESCRIPTOR;
57+
// A descriptor may appear in the query section of an R2DBC URL:
58+
String r2dbcUrl = "r2dbc:oracle://?oracle-net-descriptor="+DESCRIPTOR;
5859
Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.parse(r2dbcUrl)
5960
.mutate()
6061
.option(ConnectionFactoryOptions.USER, USER)
@@ -71,11 +72,10 @@ public static void main(String[] args) {
7172
.toStream()
7273
.forEach(System.out::println);
7374

74-
// A descriptor may also be specified as the value of
75-
// ConnectionFactoryOptions.HOST
75+
// A descriptor may also be specified as an Option
7676
Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.builder()
7777
.option(ConnectionFactoryOptions.DRIVER, "oracle")
78-
.option(ConnectionFactoryOptions.HOST, DESCRIPTOR)
78+
.option(Option.valueOf("oracle-net-descriptor"), DESCRIPTOR)
7979
.option(ConnectionFactoryOptions.USER, USER)
8080
.option(ConnectionFactoryOptions.PASSWORD, PASSWORD)
8181
.build())

src/main/java/oracle/r2dbc/impl/OracleConnectionFactoryImpl.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,19 @@
5959
* <li>{@link ConnectionFactoryOptions#DATABASE}</li>
6060
* <li>{@link ConnectionFactoryOptions#USER}</li>
6161
* <li>{@link ConnectionFactoryOptions#PASSWORD}</li>
62-
* </ul>
62+
* <li>{@link ConnectionFactoryOptions#CONNECT_TIMEOUT}</li>
63+
* <li>{@link ConnectionFactoryOptions#SSL}</li>
64+
* </ul><p>
65+
* A value set for {@code CONNECT_TIMEOUT} will be rounded up to the nearest
66+
* whole second. When a value is set, any connection request that exceeds the
67+
* specified duration of seconds will automatically be cancelled. The
68+
* cancellation will result in an {@code onError} signal delivering an
69+
* {@link io.r2dbc.spi.R2dbcTimeoutException} to a connection {@code
70+
* Subscriber}.
71+
* </p><p>
72+
* A value of {@code true} set for {@code SSL} will configure the Oracle
73+
* JDBC Driver to connect using the TCPS protocol (ie: SSL/TLS).
74+
* </p>
6375
*
6476
* @author harayuanwang, michael-a-mcmahon
6577
* @since 0.1.0

src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java

Lines changed: 100 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.sql.SQLException;
5151
import java.sql.Wrapper;
5252
import java.time.Duration;
53+
import java.util.Arrays;
5354
import java.util.Objects;
5455
import java.util.Set;
5556
import java.util.concurrent.CompletableFuture;
@@ -59,6 +60,14 @@
5960
import java.util.function.Function;
6061
import java.util.function.Supplier;
6162

63+
import static io.r2dbc.spi.ConnectionFactoryOptions.HOST;
64+
import static io.r2dbc.spi.ConnectionFactoryOptions.PORT;
65+
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
66+
import static io.r2dbc.spi.ConnectionFactoryOptions.USER;
67+
import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
68+
import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
69+
import static io.r2dbc.spi.ConnectionFactoryOptions.SSL;
70+
6271
import static java.sql.Statement.RETURN_GENERATED_KEYS;
6372
import static oracle.r2dbc.impl.OracleR2dbcExceptions.getOrHandleSQLException;
6473
import static oracle.r2dbc.impl.OracleR2dbcExceptions.runOrHandleSQLException;
@@ -186,9 +195,19 @@ final class OracleReactiveJdbcAdapter implements ReactiveJdbcAdapter {
186195

187196
);
188197

198+
/**
199+
* Extended {@code Option} that specifies an Oracle Net Connect Descriptor
200+
* of the form "(DESCRIPTION=...)"
201+
*/
202+
private static final Option<CharSequence> DESCRIPTOR =
203+
Option.valueOf("oracle-net-descriptor");
204+
189205
/**
190206
* Java object types that are supported by
191-
* {@link OraclePreparedStatement#setObject(int, Object)} in 21c.
207+
* {@link OraclePreparedStatement#setObject(int, Object)} in 21c. This set
208+
* was not derived from a comprehensive analysis of Oracle JDBC; It may be
209+
* missing some supported types. Additional types should be added to this
210+
* set if needed.
192211
*/
193212
private static final Set<Class<?>> SUPPORTED_BIND_TYPES = Set.of(
194213
// The following types are listed in Table B-4 of the JDBC 4.3
@@ -251,53 +270,39 @@ static OracleReactiveJdbcAdapter getInstance() {
251270
* {@link OracleDataSource} that implements the Reactive Extensions APIs for
252271
* creating connections.
253272
* </p>
254-
*
255-
* <h3>Required Standard Options</h3>
273+
* <h3>Composing a JDBC URL</h3>
256274
* <p>
257-
* This implementation requires values to be set for the following options:
258-
* </p><ul>
259-
* <li>{@link ConnectionFactoryOptions#HOST}</li>
260-
* </ul><p>
261-
* The values set for these options are used to compose an Oracle JDBC URL as:
275+
* The {@code options} provided to this method are used to compose a URL
276+
* for the JDBC {@code DataSource}. Values for standard
277+
* {@link ConnectionFactoryOptions} of {@code HOST}, {@code PORT}, and
278+
* {@code DATABASE} are used to compose the JDBC URL with {@code DATABASE}
279+
* interpreted as a service name (not a system identifier (SID)):
262280
* </p><pre>
263-
* jdbc:oracle:thin:@HOST
281+
* jdbc:oracle:thin:@HOST:PORT/DATABASE
264282
* </pre><p>
265-
* This minimal JDBC URL may specify a TNS alias as a the HOST value when
266-
* Oracle JDBC is configured to read a tnsnames.ora file.
267-
* </p>
268-
*
269-
* <h3>Optional Standard Options</h3>
270-
* <p>
271-
* This implementation supports optional values that are set for the
272-
* following options:
273-
* </p><ul>
274-
* <li>{@link ConnectionFactoryOptions#PORT}</li>
275-
* <li>{@link ConnectionFactoryOptions#DATABASE}</li>
276-
* <li>{@link ConnectionFactoryOptions#USER}</li>
277-
* <li>{@link ConnectionFactoryOptions#PASSWORD}</li>
278-
* <li>{@link ConnectionFactoryOptions#CONNECT_TIMEOUT}</li>
279-
* <li>{@link ConnectionFactoryOptions#SSL}</li>
280-
* </ul><p>
281-
* When PORT and DATABASE are present, an Oracle JDBC URL is composed as:
283+
* Alternatively, the host, port, and service name may be specified using an
284+
* <a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/identifying-and-accessing-database.html#GUID-8D28E91B-CB72-4DC8-AEFC-F5D583626CF6"></a>
285+
* Oracle Net Descriptor</a>. The descriptor may be set as the value of an
286+
* {@link Option} having the name "descriptor". When the descriptor option is
287+
* present, the JDBC URL is composed as:
282288
* </p><pre>
283-
* jdbc:oracle:thin:@HOST:PORT/DATABASE
289+
* jdbc:oracle:thin:@(DESCRIPTION=...)
284290
* </pre><p>
285-
* Note that {@code DATABASE} is interpreted as the service name of an Oracle
286-
* Database; It is not interpreted as a system identifier (SID).
287-
* </p><p>
288-
* Values set for {@code USER} and {@code PASSWORD} options are used to
289-
* authenticate with an Oracle Database.
291+
* When the "descriptor" option is provided, it is invalid to specify any
292+
* other options that might conflict with values also specified in the
293+
* descriptor. For instance, the descriptor element of
294+
* {@code (ADDRESSS=(HOST=...)(PORT=...)(PROTOCOL=...))} specifies values
295+
* that overlap with the standard {@code Option}s of {@code HOST}, {@code
296+
* PORT}, and {@code SSL}. An {@code IllegalStateException} is thrown
297+
* when the descriptor is provided with any overlapping {@code Option}s.
290298
* </p><p>
291-
* A value set for {@code CONNECT_TIMEOUT} will be rounded up to the nearest
292-
* whole second. When a value is set, any connection request that exceeds the
293-
* specified duration of seconds will automatically be cancelled. The
294-
* cancellation will result in an {@code onError} signal delivering an
295-
* {@link io.r2dbc.spi.R2dbcTimeoutException} to a connection {@code
296-
* Subscriber}.
297-
* </p><p>
298-
* A value of {@code true} set for {@code SSL} will configure the Oracle
299-
* JDBC Driver to connect using the TCPS protocol (ie: SSL/TLS).
300-
* </p>
299+
* Note that the alias of a descriptor within a tnsnames.ora file may be
300+
* specified as the descriptor {@code Option} as well. Where "db1" is an
301+
* alias value set by the descriptor {@code Option}, a JDBC URL is composed
302+
* as:
303+
* </p><pre>
304+
* jdbc:oracle:thin:@db1
305+
* </pre>
301306
*
302307
* <h3>Extended Options</h3>
303308
* <p>
@@ -319,8 +324,8 @@ static OracleReactiveJdbcAdapter getInstance() {
319324
* section of an R2DBC URL, Oracle R2DBC programmers are advised to use a
320325
* more secure method whenever possible.
321326
* </p><p>
322-
* Non-sensitive options may be configured either programmatically by
323-
* with {@link Option#valueOf(String)}, or by including name=value pairs
327+
* Non-sensitive options may be configured either programmatically using
328+
* {@link Option#valueOf(String)}, or by including name=value pairs
324329
* in the query section of an R2DBC URL. For example, a wallet location
325330
* could be configured programmatically as:
326331
* </p><pre>
@@ -439,17 +444,54 @@ public DataSource createDataSource(ConnectionFactoryOptions options) {
439444
* @return An Oracle JDBC URL composed from R2DBC options
440445
*/
441446
private static String composeJdbcUrl(ConnectionFactoryOptions options) {
442-
String host = options.getRequiredValue(ConnectionFactoryOptions.HOST);
443-
Integer port = options.getValue(ConnectionFactoryOptions.PORT);
444-
String serviceName = options.getValue(ConnectionFactoryOptions.DATABASE);
445-
Boolean isTcps = parseOptionValue(
446-
ConnectionFactoryOptions.SSL, options, Boolean.class, Boolean::valueOf);
447-
448-
return String.format("jdbc:oracle:thin:@%s%s%s%s",
449-
Boolean.TRUE.equals(isTcps) ? "tcps:" : "",
450-
host,
451-
port != null ? (":" + port) : "",
452-
serviceName != null ? ("/" + serviceName) : "");
447+
Object descriptor = options.getValue(DESCRIPTOR);
448+
449+
if (descriptor != null) {
450+
validateDescriptorOptions(options);
451+
return "jdbc:oracle:thin:@" + descriptor.toString();
452+
}
453+
else {
454+
String host = options.getRequiredValue(HOST);
455+
Integer port = options.getValue(PORT);
456+
String serviceName = options.getValue(DATABASE);
457+
Boolean isTcps = parseOptionValue(
458+
SSL, options, Boolean.class, Boolean::valueOf);
459+
460+
return String.format("jdbc:oracle:thin:@%s%s%s%s",
461+
Boolean.TRUE.equals(isTcps) ? "tcps:" : "",
462+
host,
463+
port != null ? (":" + port) : "",
464+
serviceName != null ? ("/" + serviceName) : "");
465+
}
466+
}
467+
468+
/**
469+
* Validates {@code options} when the {@link #DESCRIPTOR} {@code Option} is
470+
* present. It is invalid to specify any other options having information
471+
* that overlaps with information in the descriptor, such as
472+
* {@link ConnectionFactoryOptions#HOST}.
473+
* @param options Options to validate
474+
* @throws IllegalStateException If {@code options} are invalid
475+
*/
476+
private static void validateDescriptorOptions(
477+
ConnectionFactoryOptions options) {
478+
Option<?>[] overlappingOptions =
479+
Set.of(HOST, PORT, DATABASE, SSL)
480+
.stream()
481+
.filter(options::hasOption)
482+
.filter(option ->
483+
// Ignore options having a value that can be represented as a
484+
// zero-length String; It may be necessary to include a zero-length
485+
// host name in an R2DBC URL:
486+
// r2dbc:oracle://user:password@?oracle-net-descriptor=...
487+
! options.getValue(option).toString().isEmpty())
488+
.toArray(Option[]::new);
489+
490+
if (overlappingOptions.length != 0) {
491+
throw new IllegalStateException(DESCRIPTOR.name()
492+
+ " Option has been specified with overlapping Options: "
493+
+ Arrays.toString(overlappingOptions));
494+
}
453495
}
454496

455497
/**
@@ -464,19 +506,18 @@ private static String composeJdbcUrl(ConnectionFactoryOptions options) {
464506
private static void configureStandardOptions(
465507
OracleDataSource oracleDataSource, ConnectionFactoryOptions options) {
466508

467-
String user = options.getValue(ConnectionFactoryOptions.USER);
509+
String user = options.getValue(USER);
468510
if (user != null)
469511
runOrHandleSQLException(() -> oracleDataSource.setUser(user));
470512

471-
CharSequence password = options.getValue(ConnectionFactoryOptions.PASSWORD);
513+
CharSequence password = options.getValue(PASSWORD);
472514
if (password != null) {
473515
runOrHandleSQLException(() ->
474516
oracleDataSource.setPassword(password.toString()));
475517
}
476518

477519
Duration timeout = parseOptionValue(
478-
ConnectionFactoryOptions.CONNECT_TIMEOUT, options, Duration.class,
479-
Duration::parse);
520+
CONNECT_TIMEOUT, options, Duration.class, Duration::parse);
480521
if (timeout != null) {
481522
runOrHandleSQLException(() ->
482523
oracleDataSource.setLoginTimeout(

src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@
3030
import org.junit.jupiter.api.Test;
3131

3232
import java.io.IOException;
33-
import java.io.UncheckedIOException;
34-
import java.net.InetSocketAddress;
35-
import java.nio.ByteBuffer;
3633
import java.nio.channels.ServerSocketChannel;
3734
import java.nio.channels.SocketChannel;
3835
import java.nio.file.Files;
@@ -53,6 +50,7 @@
5350
import static io.r2dbc.spi.ConnectionFactoryOptions.HOST;
5451
import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
5552
import static io.r2dbc.spi.ConnectionFactoryOptions.PORT;
53+
import static io.r2dbc.spi.ConnectionFactoryOptions.USER;
5654
import static oracle.r2dbc.DatabaseConfig.connectTimeout;
5755
import static oracle.r2dbc.DatabaseConfig.host;
5856
import static oracle.r2dbc.DatabaseConfig.password;
@@ -63,8 +61,6 @@
6361
import static oracle.r2dbc.util.Awaits.awaitNone;
6462
import static oracle.r2dbc.util.Awaits.awaitOne;
6563
import static org.junit.jupiter.api.Assertions.assertEquals;
66-
import static org.junit.jupiter.api.Assertions.assertNull;
67-
import static org.junit.jupiter.api.Assertions.fail;
6864

6965
/**
7066
* Verifies that
@@ -187,22 +183,53 @@ public void testCreateDataSource() throws SQLException {
187183
@Test
188184
public void testTnsAdmin() throws IOException {
189185

190-
// Create a tnsnames.ora file with a the TNS descriptor
186+
// Create an Oracle Net Descriptor
187+
String descriptor = String.format(
188+
"(DESCRIPTION=(ADDRESS=(HOST=%s)(PORT=%d)(PROTOCOL=tcp))" +
189+
"(CONNECT_DATA=(SERVICE_NAME=%s)))",
190+
host(), port(), serviceName());
191+
192+
// Create a tnsnames.ora file with an alias for the descriptor
191193
Files.writeString(Path.of("tnsnames.ora"),
192-
String.format("test_alias=" +
193-
"(DESCRIPTION=(ADDRESS=(HOST=%s)(PORT=%d)(PROTOCOL=tcp))" +
194-
"(CONNECT_DATA=(SERVICE_NAME=%s)))",
195-
host(), port(), serviceName()),
194+
String.format("test_alias=" + descriptor),
196195
StandardOpenOption.CREATE_NEW);
196+
197197
try {
198+
// Expect to connect with the descriptor in the R2DBC URL
199+
awaitNone(awaitOne(
200+
ConnectionFactories.get(String.format(
201+
"r2dbc:oracle://%s:%s@?oracle-net-descriptor=%s",
202+
user(), password(), descriptor))
203+
.create())
204+
.close());
205+
awaitNone(awaitOne(
206+
ConnectionFactories.get(ConnectionFactoryOptions.parse(String.format(
207+
"r2dbc:oracle://@?oracle-net-descriptor=%s", descriptor))
208+
.mutate()
209+
.option(USER, user())
210+
.option(PASSWORD, password())
211+
.build())
212+
.create())
213+
.close());
214+
198215
// Expect to connect with the tnsnames.ora file, when a URL specifies
199-
// it's path and an alias, along with a user and password.
216+
// the file path and an alias
200217
awaitNone(awaitOne(
201218
ConnectionFactories.get(String.format(
202-
"r2dbc:oracle://%s:%s@%s?TNS_ADMIN=%s",
219+
"r2dbc:oracle://%s:%s@?oracle-net-descriptor=%s&TNS_ADMIN=%s",
203220
user(), password(), "test_alias", System.getProperty("user.dir")))
204221
.create())
205222
.close());
223+
awaitNone(awaitOne(
224+
ConnectionFactories.get(ConnectionFactoryOptions.parse(String.format(
225+
"r2dbc:oracle://@?oracle-net-descriptor=%s&TNS_ADMIN=%s",
226+
"test_alias", System.getProperty("user.dir")))
227+
.mutate()
228+
.option(USER, user())
229+
.option(PASSWORD, password())
230+
.build())
231+
.create())
232+
.close());
206233

207234
// Create an ojdbc.properties file containing the user name
208235
Files.writeString(Path.of("ojdbc.properties"),
@@ -214,7 +241,7 @@ public void testTnsAdmin() throws IOException {
214241
// specifies a user, and a standard option specifies the password.
215242
awaitNone(awaitOne(
216243
ConnectionFactories.get(ConnectionFactoryOptions.parse(String.format(
217-
"r2dbc:oracle://%s?TNS_ADMIN=%s",
244+
"r2dbc:oracle://?oracle-net-descriptor=%s&TNS_ADMIN=%s",
218245
"test_alias", System.getProperty("user.dir")))
219246
.mutate()
220247
.option(PASSWORD, password())

0 commit comments

Comments
 (0)