Skip to content

Commit ba34660

Browse files
committed
Add configuration for TimeZone.
[resolves #520] Signed-off-by: Mark Paluch <[email protected]>
1 parent 2e0969e commit ba34660

19 files changed

+496
-109
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Optional)_
106106
| `targetServerType` | Type of server to use when using multi-host operations. Supported values: `ANY`, `PRIMARY`, `SECONDARY`, `PREFER_SECONDARY`. Defaults to `ANY`. _(Optional)_
107107
| `tcpNoDelay` | Enable/disable TCP NoDelay. Enabled by default. _(Optional)_
108108
| `tcpKeepAlive` | Enable/disable TCP KeepAlive. Disabled by default. _(Optional)_
109+
| `timeZone` | Configure the session timezone to control conversion of local temporal representations. Defaults to `TimeZone.getDefault()` _(Optional)_
109110

110111
**Programmatic Configuration**
111112

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionConfiguration.java

+41-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import java.util.List;
5555
import java.util.Map;
5656
import java.util.ServiceLoader;
57+
import java.util.TimeZone;
5758
import java.util.function.Function;
5859
import java.util.function.Supplier;
5960
import java.util.function.ToIntFunction;
@@ -120,6 +121,8 @@ public final class PostgresqlConnectionConfiguration {
120121

121122
private final boolean tcpNoDelay;
122123

124+
private final TimeZone timeZone;
125+
123126
private final String username;
124127

125128
private PostgresqlConnectionConfiguration(String applicationName, boolean autodetectExtensions, @Nullable boolean compatibilityMode, @Nullable Duration connectTimeout, @Nullable String database,
@@ -130,7 +133,7 @@ private PostgresqlConnectionConfiguration(String applicationName, boolean autode
130133
LogLevel noticeLogLevel, @Nullable Map<String, String> options, @Nullable CharSequence password, boolean preferAttachedBuffers,
131134
int preparedStatementCacheQueries, @Nullable String schema,
132135
@Nullable SingleHostConfiguration singleHostConfiguration, SSLConfig sslConfig, @Nullable Duration statementTimeout,
133-
boolean tcpKeepAlive, boolean tcpNoDelay,
136+
boolean tcpKeepAlive, boolean tcpNoDelay, TimeZone timeZone,
134137
String username) {
135138
this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null");
136139
this.autodetectExtensions = autodetectExtensions;
@@ -167,6 +170,7 @@ private PostgresqlConnectionConfiguration(String applicationName, boolean autode
167170
this.sslConfig = sslConfig;
168171
this.tcpKeepAlive = tcpKeepAlive;
169172
this.tcpNoDelay = tcpNoDelay;
173+
this.timeZone = timeZone;
170174
this.username = Assert.requireNonNull(username, "username must not be null");
171175
}
172176

@@ -202,6 +206,7 @@ public String toString() {
202206
", statementTimeout=" + this.statementTimeout +
203207
", tcpKeepAlive=" + this.tcpKeepAlive +
204208
", tcpNoDelay=" + this.tcpNoDelay +
209+
", timeZone=" + this.timeZone +
205210
", username='" + this.username + '\'' +
206211
'}';
207212
}
@@ -305,6 +310,10 @@ boolean isTcpNoDelay() {
305310
return this.tcpNoDelay;
306311
}
307312

313+
TimeZone getTimeZone() {
314+
return this.timeZone;
315+
}
316+
308317
SSLConfig getSslConfig() {
309318
return this.sslConfig;
310319
}
@@ -408,6 +417,8 @@ public static final class Builder {
408417

409418
private boolean tcpNoDelay = true;
410419

420+
private TimeZone timeZone = TimeZone.getDefault();
421+
411422
@Nullable
412423
private LoopResources loopResources = null;
413424

@@ -467,7 +478,7 @@ public PostgresqlConnectionConfiguration build() {
467478
this.extensions, this.fetchSize, this.forceBinary, this.lockWaitTimeout, this.loopResources, multiHostConfiguration,
468479
this.noticeLogLevel, this.options, this.password, this.preferAttachedBuffers,
469480
this.preparedStatementCacheQueries, this.schema, singleHostConfiguration,
470-
this.createSslConfig(), this.statementTimeout, this.tcpKeepAlive, this.tcpNoDelay, this.username);
481+
this.createSslConfig(), this.statementTimeout, this.tcpKeepAlive, this.tcpNoDelay, this.timeZone, this.username);
471482
}
472483

473484
/**
@@ -977,6 +988,33 @@ public Builder tcpNoDelay(boolean enabled) {
977988
return this;
978989
}
979990

991+
/**
992+
* Configure the session timezone.
993+
*
994+
* @param timeZone the timeZone identifier
995+
* @return this {@link Builder}
996+
* @throws IllegalArgumentException if {@code timeZone} is empty or {@code null}
997+
* @see TimeZone#getTimeZone(String)
998+
* @since 1.0
999+
*/
1000+
public Builder timeZone(String timeZone) {
1001+
return timeZone(TimeZone.getTimeZone(Assert.requireNotEmpty(timeZone, "timeZone must not be empty")));
1002+
}
1003+
1004+
/**
1005+
* Configure the session timezone.
1006+
*
1007+
* @param timeZone the timeZone identifier
1008+
* @return this {@link Builder}
1009+
* @throws IllegalArgumentException if {@code timeZone} is {@code null}
1010+
* @see TimeZone#getTimeZone(String)
1011+
* @since 1.0
1012+
*/
1013+
public Builder timeZone(TimeZone timeZone) {
1014+
this.timeZone = Assert.requireNonNull(timeZone, "timeZone must not be null");
1015+
return this;
1016+
}
1017+
9801018
/**
9811019
* Configure the username.
9821020
*
@@ -1019,6 +1057,7 @@ public String toString() {
10191057
", sslHostnameVerifier='" + this.sslHostnameVerifier + '\'' +
10201058
", tcpKeepAlive='" + this.tcpKeepAlive + '\'' +
10211059
", tcpNoDelay='" + this.tcpNoDelay + '\'' +
1060+
", timeZone='" + this.timeZone + '\'' +
10221061
", username='" + this.username + '\'' +
10231062
'}';
10241063
}

src/main/java/io/r2dbc/postgresql/PostgresqlConnectionFactoryProvider.java

+21
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131

3232
import javax.net.ssl.HostnameVerifier;
3333
import java.time.Duration;
34+
import java.time.ZoneId;
3435
import java.util.Collection;
3536
import java.util.LinkedHashMap;
3637
import java.util.Map;
38+
import java.util.TimeZone;
3739
import java.util.function.Function;
3840

3941
import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
@@ -245,6 +247,13 @@ public final class PostgresqlConnectionFactoryProvider implements ConnectionFact
245247
*/
246248
public static final Option<Boolean> TCP_NODELAY = Option.valueOf("tcpNoDelay");
247249

250+
/**
251+
* Configure the session time zone.
252+
*
253+
* @since 1.0
254+
*/
255+
public static final Option<TimeZone> TIME_ZONE = Option.valueOf("timeZone");
256+
248257
/**
249258
* Returns a new {@link PostgresqlConnectionConfiguration.Builder} configured with the given {@link ConnectionFactoryOptions}.
250259
*
@@ -342,6 +351,18 @@ private static PostgresqlConnectionConfiguration.Builder fromConnectionFactoryOp
342351
mapper.from(STATEMENT_TIMEOUT).map(OptionMapper::toDuration).to(builder::statementTimeout);
343352
mapper.from(TCP_KEEPALIVE).map(OptionMapper::toBoolean).to(builder::tcpKeepAlive);
344353
mapper.from(TCP_NODELAY).map(OptionMapper::toBoolean).to(builder::tcpNoDelay);
354+
mapper.from(TIME_ZONE).map(it -> {
355+
356+
if (it instanceof TimeZone) {
357+
return (TimeZone) it;
358+
}
359+
360+
if (it instanceof ZoneId) {
361+
return TimeZone.getTimeZone((ZoneId) it);
362+
}
363+
364+
return TimeZone.getTimeZone(it.toString());
365+
}).to(builder::timeZone);
345366
builder.username("" + options.getRequiredValue(USER));
346367

347368
return builder;

src/main/java/io/r2dbc/postgresql/SingleHostConnectionFunction.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.r2dbc.postgresql.authentication.SASLAuthenticationHandler;
2222
import io.r2dbc.postgresql.client.Client;
2323
import io.r2dbc.postgresql.client.ConnectionSettings;
24+
import io.r2dbc.postgresql.client.PostgresStartupParameterProvider;
2425
import io.r2dbc.postgresql.client.StartupMessageFlow;
2526
import io.r2dbc.postgresql.message.backend.AuthenticationMessage;
2627
import io.r2dbc.postgresql.util.Assert;
@@ -44,10 +45,15 @@ public Mono<Client> connect(SocketAddress endpoint, ConnectionSettings settings)
4445

4546
return this.upstreamFunction.connect(endpoint, settings)
4647
.delayUntil(client -> StartupMessageFlow
47-
.exchange(this.configuration.getApplicationName(), this::getAuthenticationHandler, client, this.configuration.getDatabase(), this.configuration.getUsername(), settings)
48+
.exchange(this::getAuthenticationHandler, client, this.configuration.getDatabase(), this.configuration.getUsername(),
49+
getParameterProvider(this.configuration, settings))
4850
.handle(ExceptionFactory.INSTANCE::handleErrorResponse));
4951
}
5052

53+
private static PostgresStartupParameterProvider getParameterProvider(PostgresqlConnectionConfiguration configuration, ConnectionSettings settings) {
54+
return new PostgresStartupParameterProvider(configuration.getApplicationName(), configuration.getTimeZone(), settings);
55+
}
56+
5157
protected AuthenticationHandler getAuthenticationHandler(AuthenticationMessage message) {
5258
if (PasswordAuthenticationHandler.supports(message)) {
5359
CharSequence password = Assert.requireNonNull(this.configuration.getPassword(), "Password must not be null");

src/main/java/io/r2dbc/postgresql/client/Client.java

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import reactor.core.publisher.Mono;
3131

3232
import java.util.Optional;
33+
import java.util.TimeZone;
3334
import java.util.function.Consumer;
3435
import java.util.function.Predicate;
3536

@@ -127,6 +128,14 @@ default Flux<BackendMessage> exchange(Publisher<FrontendMessage> requests) {
127128
*/
128129
Optional<Integer> getSecretKey();
129130

131+
/**
132+
* Returns the current time zone.
133+
*
134+
* @return the current time zone
135+
* @since 1.0
136+
*/
137+
Optional<TimeZone> getTimeZone();
138+
130139
/**
131140
* Returns the current transaction status.
132141
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.r2dbc.postgresql.client;
18+
19+
import io.r2dbc.postgresql.message.frontend.StartupMessage;
20+
import io.r2dbc.postgresql.util.Assert;
21+
import reactor.util.annotation.Nullable;
22+
23+
import java.util.Map;
24+
import java.util.TimeZone;
25+
26+
/**
27+
* {@link StartupMessage.StartupParameterProvider} for generic Postgres options.
28+
*
29+
* @since 1.0
30+
*/
31+
public final class PostgresStartupParameterProvider implements StartupMessage.StartupParameterProvider {
32+
33+
private final String applicationName;
34+
35+
private final TimeZone timeZone;
36+
37+
@Nullable
38+
private final Map<String, String> options;
39+
40+
public PostgresStartupParameterProvider(String applicationName, TimeZone timeZone, @Nullable Map<String, String> options) {
41+
this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null");
42+
this.timeZone = Assert.requireNonNull(timeZone, "timeZone must not be null");
43+
this.options = options;
44+
}
45+
46+
public PostgresStartupParameterProvider(String applicationName, TimeZone timeZone, ConnectionSettings settings) {
47+
this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null");
48+
this.timeZone = Assert.requireNonNull(timeZone, "timeZone must not be null");
49+
this.options = settings.getStartupOptions();
50+
}
51+
52+
@Override
53+
public void accept(StartupMessage.ParameterWriter writer) {
54+
55+
writer.write("application_name", this.applicationName);
56+
writer.write("client_encoding", "utf8");
57+
writer.write("DateStyle", "ISO");
58+
writer.write("extra_float_digits", "2");
59+
writer.write("TimeZone", TimeZoneUtils.createPostgresTimeZone(this.timeZone));
60+
61+
if (this.options != null) {
62+
for (Map.Entry<String, String> option : this.options.entrySet()) {
63+
writer.write(option.getKey(), option.getValue());
64+
}
65+
}
66+
67+
}
68+
69+
}

src/main/java/io/r2dbc/postgresql/client/ReactorNettyClient.java

+27-11
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import java.util.Optional;
7171
import java.util.Queue;
7272
import java.util.StringJoiner;
73+
import java.util.TimeZone;
7374
import java.util.concurrent.atomic.AtomicBoolean;
7475
import java.util.concurrent.atomic.AtomicLong;
7576
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
@@ -116,6 +117,8 @@ public final class ReactorNettyClient implements Client {
116117

117118
private volatile Integer secretKey;
118119

120+
private volatile TimeZone timeZone;
121+
119122
private volatile TransactionStatus transactionStatus = IDLE;
120123

121124
private volatile Version version = new Version("", 0);
@@ -298,24 +301,32 @@ private boolean consumeMessage(BackendMessage message) {
298301

299302
private void handleParameterStatus(ParameterStatus message) {
300303

301-
Version existingVersion = this.version;
304+
String name = message.getName();
302305

303-
String versionString = existingVersion.getVersion();
304-
int versionNum = existingVersion.getVersionNumber();
306+
if (name.equals("server_version_num") || name.equals("server_version")) {
307+
Version existingVersion = this.version;
305308

306-
if (message.getName().equals("server_version_num")) {
307-
versionNum = Integer.parseInt(message.getValue());
308-
}
309+
String versionString = existingVersion.getVersion();
310+
int versionNum = existingVersion.getVersionNumber();
309311

310-
if (message.getName().equals("server_version")) {
311-
versionString = message.getValue();
312+
if (name.equals("server_version_num")) {
313+
versionNum = Integer.parseInt(message.getValue());
314+
}
312315

313-
if (versionNum == 0) {
314-
versionNum = Version.parseServerVersionStr(versionString);
316+
if (name.equals("server_version")) {
317+
versionString = message.getValue();
318+
319+
if (versionNum == 0) {
320+
versionNum = Version.parseServerVersionStr(versionString);
321+
}
315322
}
323+
324+
this.version = new Version(versionString, versionNum);
316325
}
317326

318-
this.version = new Version(versionString, versionNum);
327+
if (name.equals("TimeZone")) {
328+
this.timeZone = TimeZoneUtils.parseBackendTimeZone(message.getValue());
329+
}
319330
}
320331

321332
/**
@@ -446,6 +457,11 @@ public Optional<Integer> getSecretKey() {
446457
return Optional.ofNullable(this.secretKey);
447458
}
448459

460+
@Override
461+
public Optional<TimeZone> getTimeZone() {
462+
return Optional.ofNullable(this.timeZone);
463+
}
464+
449465
@Override
450466
public TransactionStatus getTransactionStatus() {
451467
return this.transactionStatus;

0 commit comments

Comments
 (0)