Skip to content

Commit 2cd8c4f

Browse files
authored
Merge branch 'main' into add-enum-string-v2
2 parents f3adedb + 1186ae3 commit 2cd8c4f

File tree

11 files changed

+701
-154
lines changed

11 files changed

+701
-154
lines changed

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@
118118
* ...
119119
* }
120120
* }
121-
*
122121
* }
123122
*
124123
*
@@ -132,6 +131,9 @@ public class Client implements AutoCloseable {
132131

133132
private final Set<String> endpoints;
134133
private final Map<String, String> configuration;
134+
135+
private final Map<String, String> readOnlyConfig;
136+
135137
private final List<ClickHouseNode> serverNodes = new ArrayList<>();
136138

137139
// POJO serializer mapping (class -> (schema -> (format -> serializer)))
@@ -154,10 +156,14 @@ public class Client implements AutoCloseable {
154156

155157
private final ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy;
156158

159+
// Server context
160+
private String serverVersion;
161+
157162
private Client(Set<String> endpoints, Map<String,String> configuration, boolean useNewImplementation,
158163
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy) {
159164
this.endpoints = endpoints;
160165
this.configuration = configuration;
166+
this.readOnlyConfig = Collections.unmodifiableMap(this.configuration);
161167
this.endpoints.forEach(endpoint -> {
162168
this.serverNodes.add(ClickHouseNode.of(endpoint, this.configuration));
163169
});
@@ -179,8 +185,27 @@ private Client(Set<String> endpoints, Map<String,String> configuration, boolean
179185
LOG.info("Using old http client implementation");
180186
}
181187
this.columnToMethodMatchingStrategy = columnToMethodMatchingStrategy;
188+
189+
updateServerContext();
190+
}
191+
192+
private void updateServerContext() {
193+
try (QueryResponse response = this.query("SELECT currentUser() AS user, timezone() AS timezone, version() AS version LIMIT 1").get()) {
194+
try (ClickHouseBinaryFormatReader reader = this.newBinaryFormatReader(response)) {
195+
if (reader.next() != null) {
196+
this.configuration.put(ClientConfigProperties.USER.getKey(), reader.getString("user"));
197+
this.configuration.put(ClientConfigProperties.SERVER_TIMEZONE.getKey(), reader.getString("timezone"));
198+
serverVersion = reader.getString("version");
199+
}
200+
}
201+
} catch (Exception e) {
202+
LOG.error("Failed to get server info", e);
203+
}
182204
}
183205

206+
207+
208+
184209
/**
185210
* Returns default database name that will be used by operations if not specified.
186211
*
@@ -216,6 +241,7 @@ public void close() {
216241
}
217242
}
218243

244+
219245
public static class Builder {
220246
private Set<String> endpoints;
221247

@@ -853,7 +879,7 @@ public Builder allowBinaryReaderToReuseBuffers(boolean reuse) {
853879
* @return same instance of the builder
854880
*/
855881
public Builder httpHeader(String key, String value) {
856-
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), value);
882+
this.configuration.put(ClientConfigProperties.httpHeader(key), value);
857883
return this;
858884
}
859885

@@ -864,7 +890,7 @@ public Builder httpHeader(String key, String value) {
864890
* @return same instance of the builder
865891
*/
866892
public Builder httpHeader(String key, Collection<String> values) {
867-
this.configuration.put(ClientConfigProperties.HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US), ClientConfigProperties.commaSeparated(values));
893+
this.configuration.put(ClientConfigProperties.httpHeader(key), ClientConfigProperties.commaSeparated(values));
868894
return this;
869895
}
870896

@@ -955,6 +981,19 @@ public Builder setOptions(Map<String, String> options) {
955981
return this;
956982
}
957983

984+
/**
985+
* Specifies whether to use Bearer Authentication and what token to use.
986+
* The token will be sent as is, so it should be encoded before passing to this method.
987+
*
988+
* @param bearerToken - token to use
989+
* @return same instance of the builder
990+
*/
991+
public Builder useBearerTokenAuth(String bearerToken) {
992+
// Most JWT libraries (https://jwt.io/libraries?language=Java) compact tokens in proper way
993+
this.httpHeader(HttpHeaders.AUTHORIZATION, "Bearer " + bearerToken);
994+
return this;
995+
}
996+
958997
public Client build() {
959998
setDefaults();
960999

@@ -965,8 +1004,9 @@ public Client build() {
9651004
// check if username and password are empty. so can not initiate client?
9661005
if (!this.configuration.containsKey("access_token") &&
9671006
(!this.configuration.containsKey("user") || !this.configuration.containsKey("password")) &&
968-
!MapUtils.getFlag(this.configuration, "ssl_authentication", false)) {
969-
throw new IllegalArgumentException("Username and password (or access token, or SSL authentication) are required");
1007+
!MapUtils.getFlag(this.configuration, "ssl_authentication", false) &&
1008+
!this.configuration.containsKey(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION))) {
1009+
throw new IllegalArgumentException("Username and password (or access token or SSL authentication or pre-define Authorization header) are required");
9701010
}
9711011

9721012
if (this.configuration.containsKey("ssl_authentication") &&
@@ -1012,9 +1052,11 @@ public Client build() {
10121052
throw new IllegalArgumentException("Nor server timezone nor specific timezone is set");
10131053
}
10141054

1015-
return new Client(this.endpoints, this.configuration, this.useNewImplementation, this.sharedOperationExecutor, this.columnToMethodMatchingStrategy);
1055+
return new Client(this.endpoints, this.configuration, this.useNewImplementation, this.sharedOperationExecutor,
1056+
this.columnToMethodMatchingStrategy);
10161057
}
10171058

1059+
10181060
private static final int DEFAULT_NETWORK_BUFFER_SIZE = 300_000;
10191061

10201062
private void setDefaults() {
@@ -2104,7 +2146,7 @@ public String toString() {
21042146
* @return - configuration options
21052147
*/
21062148
public Map<String, String> getConfiguration() {
2107-
return Collections.unmodifiableMap(configuration);
2149+
return readOnlyConfig;
21082150
}
21092151

21102152
/** Returns operation timeout in seconds */
@@ -2124,6 +2166,10 @@ public String getUser() {
21242166
return this.configuration.get(ClientConfigProperties.USER.getKey());
21252167
}
21262168

2169+
public String getServerVersion() {
2170+
return this.serverVersion;
2171+
}
2172+
21272173
/**
21282174
* Sets list of DB roles that should be applied to each query.
21292175
*
@@ -2151,6 +2197,10 @@ public Collection<String> getDBRoles() {
21512197
return unmodifiableDbRolesView;
21522198
}
21532199

2200+
public void updateBearerToken(String bearer) {
2201+
this.configuration.put(ClientConfigProperties.httpHeader(HttpHeaders.AUTHORIZATION), "Bearer " + bearer);
2202+
}
2203+
21542204
private ClickHouseNode getNextAliveNode() {
21552205
return serverNodes.get(0);
21562206
}

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.util.Collection;
55
import java.util.Collections;
66
import java.util.List;
7+
import java.util.Locale;
78
import java.util.stream.Collectors;
89

910
/**
@@ -157,6 +158,10 @@ public static String serverSetting(String key) {
157158
return SERVER_SETTING_PREFIX + key;
158159
}
159160

161+
public static String httpHeader(String key) {
162+
return HTTP_HEADER_PREFIX + key.toUpperCase(Locale.US);
163+
}
164+
160165
public static String commaSeparated(Collection<?> values) {
161166
StringBuilder sb = new StringBuilder();
162167
for (Object value : values) {

client-v2/src/main/java/com/clickhouse/client/api/ServerException.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,34 @@ public class ServerException extends RuntimeException {
77
public static final int TABLE_NOT_FOUND = 60;
88

99
private final int code;
10+
11+
private final int transportProtocolCode;
12+
1013
public ServerException(int code, String message) {
14+
this(code, message, 500);
15+
}
16+
17+
public ServerException(int code, String message, int transportProtocolCode) {
1118
super(message);
1219
this.code = code;
20+
this.transportProtocolCode = transportProtocolCode;
1321
}
1422

23+
/**
24+
* Returns CH server error code. May return 0 if code is unknown.
25+
* @return - error code from server response
26+
*/
1527
public int getCode() {
1628
return code;
1729
}
30+
31+
/**
32+
* Returns error code of underlying transport protocol. For example, HTTP status.
33+
* By default, will return {@code 500 } what is derived from HTTP Server Internal Error.
34+
*
35+
* @return - transport status code
36+
*/
37+
public int getTransportProtocolCode() {
38+
return transportProtocolCode;
39+
}
1840
}

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import java.util.Set;
7474
import java.util.concurrent.TimeUnit;
7575
import java.util.function.Function;
76+
import java.util.function.Supplier;
7677

7778
import static com.clickhouse.client.api.ClientConfigProperties.SOCKET_TCP_NO_DELAY_OPT;
7879

@@ -335,10 +336,13 @@ public Exception readError(ClassicHttpResponse httpResponse) {
335336

336337
String msg = msgBuilder.toString().replaceAll("\\s+", " ").replaceAll("\\\\n", " ")
337338
.replaceAll("\\\\/", "/");
338-
return new ServerException(serverCode, msg);
339+
if (msg.trim().isEmpty()) {
340+
msg = String.format(ERROR_CODE_PREFIX_PATTERN, serverCode) + " <Unreadable error message> (transport error: " + httpResponse.getCode() + ")";
341+
}
342+
return new ServerException(serverCode, msg, httpResponse.getCode());
339343
} catch (Exception e) {
340344
LOG.error("Failed to read error message", e);
341-
return new ServerException(serverCode, String.format(ERROR_CODE_PREFIX_PATTERN, serverCode) + " <Unreadable error message>");
345+
return new ServerException(serverCode, String.format(ERROR_CODE_PREFIX_PATTERN, serverCode) + " <Unreadable error message> (transport error: " + httpResponse.getCode() + ")", httpResponse.getCode());
342346
}
343347
}
344348

@@ -450,12 +454,12 @@ private void addHeaders(HttpPost req, Map<String, String> chConfig, Map<String,
450454

451455
for (Map.Entry<String, String> entry : chConfig.entrySet()) {
452456
if (entry.getKey().startsWith(ClientConfigProperties.HTTP_HEADER_PREFIX)) {
453-
req.addHeader(entry.getKey().substring(ClientConfigProperties.HTTP_HEADER_PREFIX.length()), entry.getValue());
457+
req.setHeader(entry.getKey().substring(ClientConfigProperties.HTTP_HEADER_PREFIX.length()), entry.getValue());
454458
}
455459
}
456460
for (Map.Entry<String, Object> entry : requestConfig.entrySet()) {
457461
if (entry.getKey().startsWith(ClientConfigProperties.HTTP_HEADER_PREFIX)) {
458-
req.addHeader(entry.getKey().substring(ClientConfigProperties.HTTP_HEADER_PREFIX.length()), entry.getValue().toString());
462+
req.setHeader(entry.getKey().substring(ClientConfigProperties.HTTP_HEADER_PREFIX.length()), entry.getValue().toString());
459463
}
460464
}
461465

client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import java.io.ByteArrayInputStream;
3939
import java.net.Socket;
4040
import java.nio.ByteBuffer;
41+
import java.nio.charset.StandardCharsets;
4142
import java.time.temporal.ChronoUnit;
4243
import java.util.Arrays;
4344
import java.util.Base64;
@@ -51,8 +52,11 @@
5152
import java.util.concurrent.TimeUnit;
5253
import java.util.concurrent.atomic.AtomicInteger;
5354
import java.util.regex.Pattern;
55+
import java.util.function.Supplier;
5456

5557
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
58+
import static org.junit.Assert.assertEquals;
59+
import static org.junit.Assert.assertThrows;
5660
import static org.junit.Assert.fail;
5761

5862
public class HttpTransportTests extends BaseIntegrationTest {
@@ -66,7 +70,6 @@ public void testConnectionTTL(Long connectionTtl, Long keepAlive, int openSocket
6670
ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
6771

6872
int proxyPort = new Random().nextInt(1000) + 10000;
69-
System.out.println("proxyPort: " + proxyPort);
7073
ConnectionCounterListener connectionCounter = new ConnectionCounterListener();
7174
WireMockServer proxy = new WireMockServer(WireMockConfiguration
7275
.options().port(proxyPort)
@@ -154,7 +157,6 @@ public void closed(Socket socket) {
154157
public void testConnectionRequestTimeout() {
155158

156159
int serverPort = new Random().nextInt(1000) + 10000;
157-
System.out.println("proxyPort: " + serverPort);
158160
ConnectionCounterListener connectionCounter = new ConnectionCounterListener();
159161
WireMockServer proxy = new WireMockServer(WireMockConfiguration
160162
.options().port(serverPort)
@@ -794,4 +796,75 @@ public static Object[][] testUserAgentHasCompleteProductName_dataProvider() {
794796
{ "test-client/1.0", Pattern.compile("test-client/1.0 clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")},
795797
{ "test-client/", Pattern.compile("test-client/ clickhouse-java-v2\\/.+ \\(.+\\) Apache HttpClient\\/[\\d\\.]+$")}};
796798
}
799+
800+
@Test(groups = { "integration" })
801+
public void testBearerTokenAuth() throws Exception {
802+
WireMockServer mockServer = new WireMockServer( WireMockConfiguration
803+
.options().port(9090).notifier(new ConsoleNotifier(false)));
804+
mockServer.start();
805+
806+
try {
807+
String jwtToken1 = Arrays.stream(
808+
new String[]{"header", "payload", "signature"})
809+
.map(s -> Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)))
810+
.reduce((s1, s2) -> s1 + "." + s2).get();
811+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false)
812+
.useBearerTokenAuth(jwtToken1)
813+
.compressServerResponse(false)
814+
.build()) {
815+
816+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
817+
.withHeader("Authorization", WireMock.equalTo("Bearer " + jwtToken1))
818+
.willReturn(WireMock.aResponse()
819+
.withHeader("X-ClickHouse-Summary",
820+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}")).build());
821+
822+
try (QueryResponse response = client.query("SELECT 1").get(1, TimeUnit.SECONDS)) {
823+
Assert.assertEquals(response.getReadBytes(), 10);
824+
} catch (Exception e) {
825+
Assert.fail("Unexpected exception", e);
826+
}
827+
}
828+
829+
String jwtToken2 = Arrays.stream(
830+
new String[]{"header2", "payload2", "signature2"})
831+
.map(s -> Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)))
832+
.reduce((s1, s2) -> s1 + "." + s2).get();
833+
834+
mockServer.resetAll();
835+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
836+
.withHeader("Authorization", WireMock.equalTo("Bearer " + jwtToken1))
837+
.willReturn(WireMock.aResponse()
838+
.withStatus(HttpStatus.SC_UNAUTHORIZED))
839+
.build());
840+
841+
try (Client client = new Client.Builder().addEndpoint(Protocol.HTTP, "localhost", mockServer.port(), false)
842+
.useBearerTokenAuth(jwtToken1)
843+
.compressServerResponse(false)
844+
.build()) {
845+
846+
try {
847+
client.execute("SELECT 1").get();
848+
fail("Exception expected");
849+
} catch (ServerException e) {
850+
Assert.assertEquals(e.getTransportProtocolCode(), HttpStatus.SC_UNAUTHORIZED);
851+
}
852+
853+
mockServer.resetAll();
854+
mockServer.addStubMapping(WireMock.post(WireMock.anyUrl())
855+
.withHeader("Authorization", WireMock.equalTo("Bearer " + jwtToken2))
856+
.willReturn(WireMock.aResponse()
857+
.withHeader("X-ClickHouse-Summary",
858+
"{ \"read_bytes\": \"10\", \"read_rows\": \"1\"}"))
859+
860+
.build());
861+
862+
client.updateBearerToken(jwtToken2);
863+
864+
client.execute("SELECT 1").get();
865+
}
866+
} finally {
867+
mockServer.stop();
868+
}
869+
}
797870
}

client-v2/src/test/java/com/clickhouse/client/query/QueryTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,4 +1983,19 @@ public void testReadingEnumsAsStrings() throws Exception {
19831983
Assert.assertFalse(reader.hasNext());
19841984
}
19851985
}
1986+
1987+
@Test(groups = {"integration"})
1988+
public void testServerTimezone() throws Exception {
1989+
final String sql = "SELECT now() as t, toDateTime(now(), 'UTC') as utc_time, toDateTime(now(), 'America/New_York') as est_time";
1990+
try (QueryResponse response = client.query(sql).get(1, TimeUnit.SECONDS)) {
1991+
ClickHouseBinaryFormatReader reader = client.newBinaryFormatReader(response);
1992+
Assert.assertNotNull(reader.next());
1993+
ZonedDateTime serverTime = reader.getZonedDateTime(1);
1994+
ZonedDateTime serverUtcTime = reader.getZonedDateTime(2);
1995+
ZonedDateTime serverEstTime = reader.getZonedDateTime(3);
1996+
Assert.assertEquals(serverTime.withZoneSameInstant(ZoneId.of("UTC")), serverUtcTime);
1997+
Assert.assertEquals(serverTime, serverUtcTime);
1998+
Assert.assertEquals(serverUtcTime.withZoneSameInstant(ZoneId.of("America/New_York")), serverEstTime);
1999+
}
2000+
}
19862001
}

0 commit comments

Comments
 (0)