5050import java .sql .SQLException ;
5151import java .sql .Wrapper ;
5252import java .time .Duration ;
53+ import java .util .Arrays ;
5354import java .util .Objects ;
5455import java .util .Set ;
5556import java .util .concurrent .CompletableFuture ;
5960import java .util .function .Function ;
6061import 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+
6271import static java .sql .Statement .RETURN_GENERATED_KEYS ;
6372import static oracle .r2dbc .impl .OracleR2dbcExceptions .getOrHandleSQLException ;
6473import 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 (
0 commit comments