Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions client-v2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,6 @@
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand All @@ -231,6 +225,19 @@
<release>8</release>
</configuration>
</plugin>
<plugin>
<!-- generates classes from src/main/java-templates, for example the versions of the client and its dependencies -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>templating-maven-plugin</artifactId>
<executions>
<execution>
<id>filter-sources</id>
<goals>
<goal>filter-sources</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.clickhouse.client.api.internal;

/**
* Versions of the client and of the libraries that the client is built with. The build generates this class from a
* template, so the versions are correct also when the libraries are shaded into the client and cannot report their
* own version.
*/
public final class Versions {

/**
* Version of the client.
*/
public static final String CLIENT = "${revision}";

/**
* Version of the Apache HttpClient library.
*/
public static final String APACHE_HTTP_CLIENT = "${apache.httpclient.version}";

private Versions() {
}
}
68 changes: 58 additions & 10 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import com.clickhouse.client.api.internal.MapUtils;
import com.clickhouse.client.api.internal.TableSchemaParser;
import com.clickhouse.client.api.internal.ValidationUtils;
import com.clickhouse.client.api.internal.Versions;
import com.clickhouse.client.api.internal.WorkloadIdentitySupport;
import com.clickhouse.client.api.metadata.ColumnToMethodMatchingStrategy;
import com.clickhouse.client.api.metadata.DefaultColumnToMethodMatchingStrategy;
import com.clickhouse.client.api.metadata.TableSchema;
Expand All @@ -49,7 +51,6 @@
import com.clickhouse.client.api.transport.HttpEndpoint;
import com.clickhouse.client.api.transport.internal.TransportRequest;
import com.clickhouse.client.api.transport.internal.TransportResponse;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.data.ClickHouseColumn;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.data.ClickHouseFormat;
Expand Down Expand Up @@ -129,6 +130,8 @@

private HttpAPIClientHelper httpClientHelper = null;

private final WorkloadIdentitySupport workloadIdentitySupport;

private final List<Endpoint> endpoints;
private final Map<String, Object> configuration;
private final Session session;
Expand Down Expand Up @@ -235,8 +238,9 @@
this.lz4Factory = LZ4Factory.fastestJavaInstance();
}

this.workloadIdentitySupport = new WorkloadIdentitySupport(this.configuration);
this.httpClientHelper = new HttpAPIClientHelper(this.configuration, metricsRegistry, initSslContext, lz4Factory,
this.spanRecorder);
this.spanRecorder, this.workloadIdentitySupport);
this.serverVersion = configuration.getOrDefault(ClientConfigProperties.SERVER_VERSION.getKey(), "unknown");
this.dbUser = configuration.getOrDefault(ClientConfigProperties.USER.getKey(), ClientConfigProperties.USER.getDefObjVal());
this.typeHintMapping = (Map<ClickHouseDataType, Class<?>>) this.configuration.get(ClientConfigProperties.TYPE_HINT_MAPPING.getKey());
Expand Down Expand Up @@ -1004,6 +1008,8 @@
* Defines list of headers that should be sent with each request. The Client will use a header value
* defined in {@code headers} instead of any other.
* Operation settings may override these headers.
* The {@code User-Agent} header is different: the client adds its own part to the end of the value. To identify
* the application, use {@link #setClientName(String)} instead of the {@code User-Agent} header.
*
* @see InsertSettings#httpHeaders(Map)
* @see QuerySettings#httpHeaders(Map)
Expand Down Expand Up @@ -1139,20 +1145,55 @@
}

/**
* Sets additional information about calling application. This string will be passed to server as a client name.
* In case of HTTP protocol it will be passed as a {@code User-Agent} header.
* Warn: If custom value of User-Agent header is set it will override this value for HTTP transport
* Client name is used by server to identify client application when investigating {@code system.query_log}. In case of HTTP
* transport this value will be in the {@code system.query_log.http_user_agent} column. Currently only HTTP transport is used.
* Sets the client name, which identifies the application to the server. The client sends it in the
* {@code User-Agent} header, before its own part {@code clickhouse-java-v2/<version> (...)}. The server records
* the header in the {@code http_user_agent} column of {@code system.query_log}.
* <p>
* To identify the application, set the client name. Do not set the {@code User-Agent} header with
* {@link #httpHeader(String, String)}: a custom {@code User-Agent} header replaces the client name.
* <p>
* This method does not change the client metadata. To also add key/value pairs to the {@code User-Agent}
* header, use {@link #setClientName(String, Map)}.
*
* @param clientName - client application display name.
* @param clientName - client application name, for example {@code my-app/1.0}
* @return same instance of the builder
*/
public Builder setClientName(String clientName) {
this.configuration.put(ClientConfigProperties.CLIENT_NAME.getKey(), clientName);
return this;
}

/**
* Sets the client name and the client metadata, which identify the application to the server. See
* {@link #setClientName(String)} for the client name. The client adds the metadata to the comment group of the
* {@code User-Agent} header, in the iteration order of {@code metadata}, and then its own {@code lv},
* {@code os} and {@code hc} (HTTP client) entries if the metadata does not set these keys. For example, the
* client name {@code my-app/1.0} and the metadata {@code fv=flink/2.0.0} and {@code api=table} give
* {@code my-app/1.0 clickhouse-java-v2/<version> (fv:flink/2.0.0; api:table; lv:java/<java.version>;
* os:<os.name>; hc:ahc_<Apache HttpClient version>)}.
* <p>
* Keys and values are trimmed, and entries with a {@code null} or empty key or value are ignored. Keys and
* values must be printable ASCII, a key must not contain spaces or any of {@code :;(),=}, and a value must not
* contain any of {@code ;()}; otherwise {@link #build()} throws {@link ClientMisconfigurationException}. The
* metadata is a client-level setting: it is kept when the client name changes with
* {@link Client#updateClientName(String)}.
*
* @param clientName - client application name, for example {@code my-app/1.0}
* @param metadata - key/value pairs; {@code null} or empty removes the previously set metadata
* @return same instance of the builder
* @see ClientConfigProperties#CLIENT_ID_METADATA
*/
public Builder setClientName(String clientName, Map<String, String> metadata) {
setClientName(clientName);
if (metadata == null || metadata.isEmpty()) {
this.configuration.remove(ClientConfigProperties.CLIENT_ID_METADATA.getKey());
} else {
this.configuration.put(ClientConfigProperties.CLIENT_ID_METADATA.getKey(),
ClientConfigProperties.fromKeyValuePairs(metadata));
}
return this;
}

/**
* Sets client options from provided map. Values are copied as is
* @param options - map of client options
Expand Down Expand Up @@ -2563,8 +2604,15 @@
this.unmodifiableDbRolesView = ImmutableList.copyOf(tmp);
}

/**
* Updates the client name for all subsequent requests created by this client. The client composes the value of
* the {@code User-Agent} header again, and keeps it.
*
* @param name - client application name, for example {@code my-app/1.0}
*/
public void updateClientName(String name) {
this.configuration.put(ClientConfigProperties.CLIENT_NAME.getKey(), name);
this.workloadIdentitySupport.updateClientName(this.configuration);
}

/**
Expand All @@ -2575,8 +2623,8 @@
this.session.updateSessionId(sessionId);
}

public static final String clientVersion =
ClickHouseClientOption.readVersionFromResource("client-v2-version.properties");
// not a compile-time constant, so that code compiled against another version reads the version of this client
public static final String clientVersion = String.valueOf(Versions.CLIENT);

Check failure on line 2627 in client-v2/src/main/java/com/clickhouse/client/api/Client.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this constant name to match the regular expression '^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$'.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AaFXolVdt8cFz0n0uMrp&open=AaFXolVdt8cFz0n0uMrp&pullRequest=3213
public static final String CLIENT_USER_AGENT = "clickhouse-java-v2/";

private Collection<String> unmodifiableDbRolesView = Collections.emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader;
import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.internal.CompressedBlockOutputStream;
import com.clickhouse.client.api.internal.WorkloadIdentitySupport;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.data.ClickHouseFormat;
import org.slf4j.Logger;
Expand Down Expand Up @@ -257,6 +258,24 @@ public Object parseValue(String value) {
.collect(Collectors.toList());
}
},

/**
* Key/value pairs, written as {@code key1=value1,key2=value2}, that the client adds to the comment group of the
* {@code User-Agent} header. A {@code ,} or {@code \} in a value is escaped with {@code \}. This is a client-level
* setting: it is not read from operation settings. See {@link Client.Builder#setClientName(String, Map)} for the
* format and the rules for keys and values.
*/
CLIENT_ID_METADATA("client_id_metadata", Map.class) {
@Override
@SuppressWarnings("unchecked")
public Object parseValue(String value) {
Map<String, String> metadata = (Map<String, String>) super.parseValue(value);
if (metadata != null) {
metadata.forEach(WorkloadIdentitySupport::checkClientMetadataEntry);
}
return metadata;
}
},
;

private static final Logger LOG = LoggerFactory.getLogger(ClientConfigProperties.class);
Expand Down Expand Up @@ -506,6 +525,36 @@ public static Map<String, String> toKeyValuePairs(String str) {



/**
* Inverse of {@link #toKeyValuePairs(String)}: escapes {@code \}, {@code ,} and {@code =} with {@code \} and
* skips entries with a {@code null} key or value.
*/
static String fromKeyValuePairs(Map<String, String> map) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : map.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null) {
continue;
}
if (sb.length() > 0) {
sb.append(',');
}
appendEscaped(sb, entry.getKey()).append('=');
appendEscaped(sb, entry.getValue());
}
return sb.toString();
}

private static StringBuilder appendEscaped(StringBuilder sb, String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (ch == '\\' || ch == ',' || ch == '=') {
sb.append('\\');
}
sb.append(ch);
}
return sb;
}

public static String mapToString(Map<?,?> map, Function<Object, String> valueConverter) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<?, ?> entry : map.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import org.slf4j.Logger;

import java.io.Closeable;
import java.util.Map;
import java.util.Objects;

/**
* Class containing utility methods used across the client.
Expand All @@ -28,4 +30,24 @@ public static void quietClose(Closeable closeable, Logger log) {
}
}
}

/**
* Puts the entry into the map if the map does not have the key. Keys are compared ignoring case, so the map is not
* changed if it has the key in a different case. A key that the map has is not replaced, also if its value is
* {@code null}.
*
* @param map - map to update
* @param key - key of the entry, not {@code null}
* @param value - value of the entry
* @param <V> - type of the values
*/
public static <V> void putIfAbsentIgnoreCase(Map<String, V> map, String key, V value) {
Objects.requireNonNull(key, "key");
for (String mapKey : map.keySet()) {
if (key.equalsIgnoreCase(mapKey)) {
return;
}
}
map.put(key, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ public String getDatabase() {
/**
* Defines list of headers that should be sent with current request. The Client will use a header value
* defined in {@code headers} instead of any other.
* The {@code User-Agent} header is different: the client adds its own part to the end of the value. To identify
* the application, use {@link Client.Builder#setClientName(String)} instead of the {@code User-Agent} header.
*
* @see Client.Builder#httpHeaders(Map)
* @param key - header name.
Expand Down
Loading
Loading