All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.clickhouse.client.http.ApacheHttpConnectionImpl Maven / Gradle / Ivy

There is a newer version: 0.7.1-patch1
Show newest version
package com.clickhouse.client.http;

import com.clickhouse.client.AbstractSocketClient;

import com.clickhouse.client.ClickHouseClient;
import com.clickhouse.client.ClickHouseConfig;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseRequest;
import com.clickhouse.client.ClickHouseSslContextProvider;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseSslMode;
import com.clickhouse.client.http.config.ClickHouseHttpOption;
import com.clickhouse.data.ClickHouseChecker;
import com.clickhouse.data.ClickHouseExternalTable;
import com.clickhouse.data.ClickHouseFormat;
import com.clickhouse.data.ClickHouseInputStream;
import com.clickhouse.data.ClickHouseOutputStream;
import com.clickhouse.data.ClickHouseUtils;
import com.clickhouse.logging.Logger;
import com.clickhouse.logging.LoggerFactory;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.Timeout;
import org.apache.hc.core5.util.VersionInfo;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;

/**
 * Created by wujianchao on 2022/12/1.
 */
public class ApacheHttpConnectionImpl extends ClickHouseHttpConnection {
    private static final Logger log = LoggerFactory.getLogger(ApacheHttpConnectionImpl.class);

    private final AtomicBoolean busy;
    private final CloseableHttpClient client;

    protected ApacheHttpConnectionImpl(ClickHouseNode server, ClickHouseRequest request, ExecutorService executor)
            throws IOException {
        super(server, request);

        busy = new AtomicBoolean(false);
        client = newConnection(config);
    }

    private CloseableHttpClient newConnection(ClickHouseConfig c) throws IOException {
        RegistryBuilder r = RegistryBuilder.create()
                .register("http", SocketFactory.create(c));
        if (c.isSsl()) {
            r.register("https", SSLSocketFactory.create(c));
        }
        return HttpClientBuilder.create().setConnectionManager(new HttpConnectionManager(r.build(), c))
                .disableContentCompression().build();
    }

    private ClickHouseHttpResponse buildResponse(ClickHouseConfig config, CloseableHttpResponse response,
            ClickHouseOutputStream output, Runnable postCloseAction) throws IOException {
        // X-ClickHouse-Server-Display-Name: xxx
        // X-ClickHouse-Query-Id: xxx
        // X-ClickHouse-Format: RowBinaryWithNamesAndTypes
        // X-ClickHouse-Timezone: UTC
        // X-ClickHouse-Summary:
        // {"read_rows":"0","read_bytes":"0","written_rows":"0","written_bytes":"0","total_rows_to_read":"0"}
        String displayName = getResponseHeader(response, "X-ClickHouse-Server-Display-Name", server.getHost());
        String queryId = getResponseHeader(response, "X-ClickHouse-Query-Id", "");
        String summary = getResponseHeader(response, "X-ClickHouse-Summary", "{}");

        ClickHouseFormat format = config.getFormat();
        TimeZone timeZone = config.getServerTimeZone();
        boolean hasCustomOutput = output != null && output.getUnderlyingStream().hasOutput();
        boolean hasQueryResult = false;
        // queryId, format and timeZone are only available for queries
        if (!ClickHouseChecker.isNullOrEmpty(queryId)) {
            String value = getResponseHeader(response, "X-ClickHouse-Format", "");
            if (!ClickHouseChecker.isNullOrEmpty(value)) {
                format = ClickHouseFormat.valueOf(value);
                hasQueryResult = true;
            }
            value = getResponseHeader(response, "X-ClickHouse-Timezone", "");
            timeZone = !ClickHouseChecker.isNullOrEmpty(value) ? TimeZone.getTimeZone(value)
                    : timeZone;
        }

        final InputStream source;
        final Runnable action;
        if (output != null) {
            source = ClickHouseInputStream.empty();
            action = () -> {
                try (OutputStream o = output) {
                    ClickHouseInputStream.pipe(response.getEntity().getContent(), o, config.getWriteBufferSize());
                    if (postCloseAction != null) {
                        postCloseAction.run();
                    }
                } catch (IOException e) {
                    throw new UncheckedIOException("Failed to redirect response to given output stream", e);
                }
            };
        } else {
            source = response.getEntity().getContent();
            action = postCloseAction;
        }
        return new ClickHouseHttpResponse(this,
                hasCustomOutput ? ClickHouseInputStream.of(source, config.getReadBufferSize(), action)
                        : (hasQueryResult ? ClickHouseClient.getAsyncResponseInputStream(config, source, action)
                                : ClickHouseClient.getResponseInputStream(config, source, action)),
                displayName, queryId, summary, format, timeZone);
    }

    private String getResponseHeader(CloseableHttpResponse response, String header, String defaultValue) {
        Header h = response.getFirstHeader(header);
        return h == null ? defaultValue : h.getValue();
    }

    private void setHeaders(HttpRequest request, Map headers) {
        headers = mergeHeaders(headers);

        if (headers != null && !headers.isEmpty()) {
            for (Map.Entry header : headers.entrySet()) {
                request.setHeader(header.getKey(), header.getValue());
            }
        }
    }

    private void checkResponse(ClickHouseConfig config, CloseableHttpResponse response) throws IOException {
        if (response.getCode() == HttpURLConnection.HTTP_OK) {
            return;
        }

        final Header errorCode = response.getFirstHeader("X-ClickHouse-Exception-Code");
        final Header serverName = response.getFirstHeader("X-ClickHouse-Server-Display-Name");
        if (response.getEntity() == null) {
            throw new ConnectException(
                    ClickHouseUtils.format("HTTP response %d %s(code %s returned from server %s)",
                            response.getCode(), response.getReasonPhrase(),
                            errorCode == null ? null : errorCode.getValue(),
                            serverName == null ? null : serverName.getValue()));
        }

        String errorMsg;

        int bufferSize = config.getReadBufferSize();
        ByteArrayOutputStream output = new ByteArrayOutputStream(bufferSize);
        ClickHouseInputStream.pipe(response.getEntity().getContent(), output, bufferSize);
        byte[] bytes = output.toByteArray();

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(
                ClickHouseClient.getResponseInputStream(config, new ByteArrayInputStream(bytes), null),
                StandardCharsets.UTF_8))) {
            StringBuilder builder = new StringBuilder();
            while ((errorMsg = reader.readLine()) != null) {
                builder.append(errorMsg).append('\n');
            }
            errorMsg = builder.toString();
        } catch (IOException e) {
            errorMsg = parseErrorFromException(errorCode != null ? errorCode.getValue() : null,
                    serverName != null ? serverName.getValue() : null, e, bytes);
        }
        throw new IOException(errorMsg);
    }

    @Override
    protected final String getDefaultUserAgent() {
        return HttpConnectionManager.USER_AGENT;
    }

    @Override
    protected boolean isReusable() {
        return busy.get();
    }

    @Override
    protected ClickHouseHttpResponse post(ClickHouseConfig config, String sql, ClickHouseInputStream data,
            List tables, ClickHouseOutputStream output, String url,
            Map headers, Runnable postCloseAction) throws IOException {
        if (!busy.compareAndSet(false, true)) {
            throw new ConnectException("Connection is busy");
        }

        try {
            HttpPost post = new HttpPost(url == null ? this.url : url);
            setHeaders(post, headers);
            byte[] boundary = null;
            String contentType = "text/plain; charset=UTF-8";
            if (tables != null && !tables.isEmpty()) {
                String uuid = rm.createUniqueId();
                contentType = "multipart/form-data; boundary=".concat(uuid);
                boundary = uuid.getBytes(StandardCharsets.US_ASCII);
            }
            post.setHeader("Content-Type", contentType);

            String contentEncoding = headers == null ? null : headers.getOrDefault("content-encoding", null);
            ClickHouseHttpEntity postBody = new ClickHouseHttpEntity(config, contentType, contentEncoding, boundary,
                    sql, data, tables);
            post.setEntity(postBody);
            CloseableHttpResponse response = client.execute(post);

            checkResponse(config, response);
            // buildResponse should use the config of current request in case of reusable
            // connection.
            return buildResponse(config, response, output, postCloseAction);
        } finally {
            busy.set(false);
        }
    }

    @Override
    public boolean ping(int timeout) {
        String url = getBaseUrl().concat("ping");
        HttpGet ping = new HttpGet(url);

        ClickHouseConfig c = config;
        try (CloseableHttpClient httpClient = newConnection(c);
                CloseableHttpResponse response = httpClient.execute(ping)) {
            // TODO set timeout
            checkResponse(c, response);
            String ok = c.getStrOption(ClickHouseHttpOption.DEFAULT_RESPONSE);
            return ok.equals(EntityUtils.toString(response.getEntity()));

        } catch (Exception e) {
            log.debug("Failed to ping url %s due to: %s", url, e.getMessage());
        }

        return false;
    }

    @Override
    public void close() throws IOException {
        client.close();
    }

    static class SocketFactory extends PlainConnectionSocketFactory {
        private final ClickHouseConfig config;

        private SocketFactory(ClickHouseConfig config) {
            this.config = config;
        }

        @Override
        public Socket createSocket(final HttpContext context) throws IOException {
            return AbstractSocketClient.setSocketOptions(config, new Socket());
        }

        public static SocketFactory create(ClickHouseConfig config) {
            return new SocketFactory(config);
        }
    }

    static class SSLSocketFactory extends SSLConnectionSocketFactory {
        private final ClickHouseConfig config;

        private SSLSocketFactory(ClickHouseConfig config) throws SSLException {
            super(ClickHouseSslContextProvider.getProvider().getSslContext(SSLContext.class, config)
                    .orElse(SSLContexts.createDefault()),
                    config.getSslMode() == ClickHouseSslMode.STRICT
                            ? new DefaultHostnameVerifier()
                            : (hostname, session) -> true); // NOSONAR
            this.config = config;
        }

        @Override
        public Socket createSocket(HttpContext context) throws IOException {
            return AbstractSocketClient.setSocketOptions(config, new Socket());
        }

        public static SSLSocketFactory create(ClickHouseConfig config) throws SSLException {
            return new SSLSocketFactory(config);
        }
    }

    static class HttpConnectionManager extends PoolingHttpClientConnectionManager {
        private static final String PROVIDER = "Apache-HttpClient";
        private static final String USER_AGENT;
        static {
            String versionInfo = null;
            try {
                String pkg = VersionInfo.class.getPackage().getName();
                pkg = pkg.substring(0, pkg.lastIndexOf('.'));
                versionInfo = VersionInfo.getSoftwareInfo(PROVIDER, pkg, HttpClientBuilder.class).split("\\s")[0];
            } catch (Throwable e) { // NOSONAR
                // ignore
            }

            USER_AGENT = ClickHouseClientOption.buildUserAgent(null,
                    versionInfo != null && !versionInfo.isEmpty() ? versionInfo : PROVIDER);
        }

        public HttpConnectionManager(Registry socketFactory, ClickHouseConfig config) {
            super(socketFactory);

            ConnectionConfig connConfig = ConnectionConfig.custom()
                    .setConnectTimeout(Timeout.of(config.getConnectionTimeout(), TimeUnit.MILLISECONDS))
                    .build();
            setDefaultConnectionConfig(connConfig);

            SocketConfig.Builder builder = SocketConfig.custom()
                    .setSoTimeout(Timeout.of(config.getSocketTimeout(), TimeUnit.MILLISECONDS))
                    .setRcvBufSize(config.getReadBufferSize())
                    .setSndBufSize(config.getWriteBufferSize());
            if (config.hasOption(ClickHouseClientOption.SOCKET_KEEPALIVE)) {
                builder.setSoKeepAlive(config.getBoolOption(ClickHouseClientOption.SOCKET_KEEPALIVE));
            }
            if (config.hasOption(ClickHouseClientOption.SOCKET_LINGER)) {
                int solinger = config.getIntOption(ClickHouseClientOption.SOCKET_LINGER);
                builder.setSoLinger(solinger, TimeUnit.SECONDS);
            }
            if (config.hasOption(ClickHouseClientOption.SOCKET_REUSEADDR)) {
                builder.setSoReuseAddress(config.getBoolOption(ClickHouseClientOption.SOCKET_REUSEADDR));
            }
            if (config.hasOption(ClickHouseClientOption.SOCKET_RCVBUF)) {
                int bufferSize = config.getIntOption(ClickHouseClientOption.SOCKET_RCVBUF);
                builder.setRcvBufSize(bufferSize > 0 ? bufferSize : config.getReadBufferSize());
            }
            if (config.hasOption(ClickHouseClientOption.SOCKET_SNDBUF)) {
                int bufferSize = config.getIntOption(ClickHouseClientOption.SOCKET_SNDBUF);
                builder.setSndBufSize(bufferSize > 0 ? bufferSize : config.getWriteBufferSize());
            }
            if (config.hasOption(ClickHouseClientOption.SOCKET_TCP_NODELAY)) {
                builder.setTcpNoDelay(config.getBoolOption(ClickHouseClientOption.SOCKET_TCP_NODELAY));
            }
            setDefaultSocketConfig(builder.build());
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy