Skip to content

Commit 50c85a6

Browse files
Merge pull request #137 from oracle/84-option-publisher
Option Values from Supplier and Publisher
2 parents da33589 + 2cfdd28 commit 50c85a6

9 files changed

+757
-31
lines changed

README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,93 @@ that is specific to Oracle Database and the Oracle JDBC Driver. Extended options
214214
are declared in the
215215
[OracleR2dbcOptions](src/main/java/oracle/r2dbc/OracleR2dbcOptions.java) class.
216216

217+
#### Support for Supplier and Publisher as Option Values
218+
Most options can have a value provided by a `Supplier` or `Publisher`.
219+
220+
Oracle R2DBC requests the value of an `Option` from a `Supplier` or `Publisher`
221+
each time the `Publisher` returned by `ConnectionFactory.create()` creates a new
222+
`Connection`. Each `Connection` can then be configured with values that change
223+
over time, such as a password which is periodically rotated.
224+
225+
If a `Supplier` provides the value of an `Option`, then Oracle R2DBC requests
226+
the value by invoking `Supplier.get()`. If `get()` returns `null`,
227+
then no value is configured for the `Option`. If `get()` throws a
228+
`RuntimeException`, then it is set as the initial cause of an
229+
`R2dbcException` emitted by the `Publisher` returned by
230+
`ConnectionFactory.create()`. The `Supplier` must have a thread safe `get()`
231+
method, as multiple subscribers may request connections concurrently.
232+
233+
If a `Publisher` provides the value of an `Option`, then Oracle R2DBC requests
234+
the value by subscribing to the `Publisher` and signalling demand.
235+
The first value emitted to `onNext` will be used as the value of the `Option`.
236+
If the `Publisher` emits `onComplete` before `onNext`, then no value is
237+
configured for the `Option`. If the `Publisher` emits `onError` before `onNext`,
238+
then the `Throwable` is set as the initial cause of an
239+
`R2dbcException` emitted by the `Publisher` returned by
240+
`ConnectionFactory.create()`.
241+
242+
The following example configures the `PASSWORD` option with a `Supplier`:
243+
```java
244+
void configurePassword(ConnectionFactoryOptions.Builder optionsBuilder) {
245+
246+
// Cast the PASSWORD option
247+
Option<Supplier<CharSequence>> suppliedOption = OracleR2dbcOptions.supplied(PASSWORD);
248+
249+
// Supply a password
250+
Supplier<CharSequence> supplier = () -> getPassword();
251+
252+
// Configure the builder
253+
optionsBuilder.option(suppliedOption, supplier);
254+
}
255+
```
256+
A more concise example configures `TLS_WALLET_PASSWORD` as a `Publisher`
257+
```java
258+
void configurePassword(ConnectionFactoryOptions.Builder optionsBuilder) {
259+
optionsBuilder.option(
260+
OracleR2dbcOptions.published(TLS_WALLET_PASSWORD),
261+
Mono.fromSupplier(() -> getWalletPassword()));
262+
}
263+
```
264+
These examples use the `supplied(Option)` and `published(Option)` methods
265+
declared by `oracle.r2dbc.OracleR2dbcOptions`. These methods cast an `Option<T>`
266+
to `Option<Supplier<T>>` and `Option<Publisher<T>>`, respectively. It is
267+
necessary to cast the generic type of the `Option` when calling
268+
`ConnectionFactoryOptions.Builder.option(Option<T>, T)` in order for the call to
269+
compile and not throw a `ClassCastException` at runtime. It is not strictly
270+
required that `supplied(Option)` or `published(Option)` be used to cast the
271+
`Option`. These methods are only meant to offer code readability and
272+
convenience.
273+
274+
Note that the following code would compile, but fails at runtime with a
275+
`ClassCastException`:
276+
```java
277+
void configurePassword(ConnectionFactoryOptions.Builder optionsBuilder) {
278+
Publisher<CharSequence> publisher = Mono.fromSupplier(() -> getPassword());
279+
// Doesn't work. Throws ClassCastException at runtime:
280+
optionsBuilder.option(PASSWORD, PASSWORD.cast(publisher));
281+
}
282+
```
283+
To avoid a `ClassCastException`, the generic type of an `Option` must match the
284+
actual type of the value passed to
285+
`ConnectionFactoryOptions.Builder.option(Option<T>, T)`.
286+
287+
For a small set of options, providing values with a `Supplier` or `Publisher`
288+
is not supported:
289+
- `DRIVER`
290+
- `PROTOCOL`
291+
292+
Providing values for these options would not be interoperable with
293+
`io.r2dbc.spi.ConnectionFactories` and `r2dbc-pool`.
294+
295+
Normally, Oracle R2DBC will not retain references to `Option` values after
296+
`ConnectionFactories.create(ConnectionFactoryOptions)` returns. However, if
297+
the value of at least one `Option` is provided by a `Supplier` or `Publisher`,
298+
then Oracle R2DBC will retain a reference to all `Option` values until the
299+
`ConnectionFactory.create()` `Publisher` emits a `Connection` or error. This is
300+
important to keep in mind when `Option` values may be mutated. In particular,
301+
a password may only be cleared from memory after the `create()` `Publisher`
302+
emits a `Connection` or error.
303+
217304
#### Configuring an Oracle Net Descriptor
218305
The `oracle.r2dbc.OracleR2dbcOptions.DESCRIPTOR` option may be used to configure
219306
an Oracle Net Descriptor of the form ```(DESCRIPTION=...)```. If this option is

src/main/java/oracle/r2dbc/OracleR2dbcOptions.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222

2323
import io.r2dbc.spi.Option;
2424
import oracle.jdbc.OracleConnection;
25+
import org.reactivestreams.Publisher;
2526

2627
import java.util.Set;
2728
import java.util.concurrent.Executor;
2829
import java.util.concurrent.ForkJoinPool;
30+
import java.util.function.Supplier;
2931

3032
/**
3133
* Extended {@link Option}s supported by the Oracle R2DBC Driver.
@@ -522,4 +524,66 @@ public static Set<Option<?>> options() {
522524
return OPTIONS;
523525
}
524526

527+
/**
528+
* <p>
529+
* Casts an <code>Option&lt;T&gt;</code> to
530+
* <code>Option&lt;Supplier&lt;T&gt;&gt;</code>. For instance, if an
531+
* <code>Option&lt;CharSequence&gt;</code> is passed to this method, it is
532+
* returned as an
533+
* <code>Option&lt;Supplier&lt;CharSequence&gt;&gt;</code>.
534+
* </p><p>
535+
* This method can used when configuring an <code>Option</code> with values
536+
* from a <code>Supplier</code>:
537+
* <pre>{@code
538+
* void configurePassword(ConnectionFactoryOptions.Builder optionsBuilder) {
539+
* optionsBuilder.option(supplied(PASSWORD), () -> getPassword());
540+
* }
541+
*
542+
* CharSequence getPassword() {
543+
* // ... return a database password ...
544+
* }
545+
* }</pre>
546+
* </p><p>
547+
* It is not strictly necessary to use this method when configuring an
548+
* <code>Option</code> with a value from a <code>Supplier</code>. This method
549+
* is offered for code readability and convenience.
550+
* </p>
551+
*/
552+
public static <T> Option<Supplier<T>> supplied(Option<T> option) {
553+
@SuppressWarnings("unchecked")
554+
Option<Supplier<T>> supplierOption = (Option<Supplier<T>>)option;
555+
return supplierOption;
556+
}
557+
558+
/**
559+
* <p>
560+
* Casts an <code>Option&lt;T&gt;</code> to
561+
* <code>Option&lt;Publisher&lt;T&gt;&gt;</code>. For instance, if an
562+
* <code>Option&lt;CharSequence&gt;</code> is passed to this method, it
563+
* is returned as an
564+
* <code>Option&lt;Publisher&lt;CharSequence&gt;&gt;</code>.
565+
* </p><p>
566+
* This method can used when configuring an <code>Option</code> with values
567+
* from a <code>Publisher</code>:
568+
* <pre>{@code
569+
* void configurePassword(ConnectionFactoryOptions.Builder optionsBuilder) {
570+
* optionsBuilder.option(published(PASSWORD), getPasswordPublisher());
571+
* }
572+
*
573+
* Publisher<CharSequence> getPasswordPublisher() {
574+
* // ... publish a database password ...
575+
* }
576+
* }</pre>
577+
* </p><p>
578+
* It is not strictly necessary to use this method when configuring an
579+
* <code>Option</code> with a value from a <code>Publisher</code>. This method
580+
* is offered for code readability and convenience.
581+
* </p>
582+
*/
583+
public static <T> Option<Publisher<T>> published(Option<T> option) {
584+
@SuppressWarnings("unchecked")
585+
Option<Publisher<T>> publisherOption = (Option<Publisher<T>>)option;
586+
return publisherOption;
587+
}
588+
525589
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public Publisher<Connection> create() {
279279
*/
280280
@Override
281281
public ConnectionFactoryMetadata getMetadata() {
282-
return () -> "Oracle Database";
282+
return OracleConnectionFactoryMetadataImpl.INSTANCE;
283283
}
284284

285285
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright (c) 2020, 2021, Oracle and/or its affiliates.
3+
4+
This software is dual-licensed to you under the Universal Permissive License
5+
(UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License
6+
2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
7+
either license.
8+
9+
Licensed under the Apache License, Version 2.0 (the "License");
10+
you may not use this file except in compliance with the License.
11+
You may obtain a copy of the License at
12+
13+
https://www.apache.org/licenses/LICENSE-2.0
14+
15+
Unless required by applicable law or agreed to in writing, software
16+
distributed under the License is distributed on an "AS IS" BASIS,
17+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
See the License for the specific language governing permissions and
19+
limitations under the License.
20+
*/
21+
22+
package oracle.r2dbc.impl;
23+
24+
import io.r2dbc.spi.ConnectionFactoryMetadata;
25+
26+
/**
27+
* Implementation of {@code ConnectionFactoryMetaData} which names
28+
* "Oracle Database" as the database product that a
29+
* {@link io.r2dbc.spi.ConnectionFactory} connects to.
30+
*/
31+
final class OracleConnectionFactoryMetadataImpl
32+
implements ConnectionFactoryMetadata {
33+
34+
static final OracleConnectionFactoryMetadataImpl INSTANCE =
35+
new OracleConnectionFactoryMetadataImpl();
36+
37+
private OracleConnectionFactoryMetadataImpl() {}
38+
39+
@Override
40+
public String getName() {
41+
return "Oracle Database";
42+
}
43+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ public OracleConnectionFactoryProviderImpl() { }
9696
public ConnectionFactory create(ConnectionFactoryOptions options) {
9797
assert supports(options) : "Options are not supported: " + options;
9898
requireNonNull(options, "options must not be null.");
99-
return new OracleConnectionFactoryImpl(options);
99+
100+
if (SuppliedOptionConnectionFactory.containsSuppliedValue(options))
101+
return new SuppliedOptionConnectionFactory(options);
102+
else
103+
return new OracleConnectionFactoryImpl(options);
100104
}
101105

102106
/**

0 commit comments

Comments
 (0)