org.apache.solr.client.solrj.impl.HttpClientUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.client.solrj.impl.HttpClientUtil.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.client.solrj.impl;

import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;

import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.ssl.SSLContexts;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.ObjectReleaseTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class for creating/configuring httpclient instances. 
 * 
 * This class can touch internal HttpClient details and is subject to change.
 * 
 * @lucene.experimental
 */
public class HttpClientUtil {

    private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

    public static final int DEFAULT_CONNECT_TIMEOUT = 60000;
    public static final int DEFAULT_SO_TIMEOUT = 600000;
    public static final int DEFAULT_MAXCONNECTIONSPERHOST = 100000;
    public static final int DEFAULT_MAXCONNECTIONS = 100000;

    private static final int VALIDATE_AFTER_INACTIVITY_DEFAULT = 3000;
    private static final int EVICT_IDLE_CONNECTIONS_DEFAULT = 50000;
    private static final String VALIDATE_AFTER_INACTIVITY = "validateAfterInactivity";
    private static final String EVICT_IDLE_CONNECTIONS = "evictIdleConnections";

    // Maximum connections allowed per host
    public static final String PROP_MAX_CONNECTIONS_PER_HOST = "maxConnectionsPerHost";
    // Maximum total connections allowed
    public static final String PROP_MAX_CONNECTIONS = "maxConnections";
    // Retry http requests on error
    public static final String PROP_USE_RETRY = "retry";
    // Allow compression (deflate,gzip) if server supports it
    public static final String PROP_ALLOW_COMPRESSION = "allowCompression";
    // Basic auth username 
    public static final String PROP_BASIC_AUTH_USER = "httpBasicAuthUser";
    // Basic auth password 
    public static final String PROP_BASIC_AUTH_PASS = "httpBasicAuthPassword";

    /**
     * System property consulted to determine if the default {@link SchemaRegistryProvider} 
     * will require hostname validation of SSL Certificates.  The default behavior is to enforce 
     * peer name validation.
     * <p>
     * This property will have no effect if {@link #setSchemaRegistryProvider} is used to override
     * the default {@link SchemaRegistryProvider} 
     * </p>
     */
    public static final String SYS_PROP_CHECK_PEER_NAME = "solr.ssl.checkPeerName";

    // * NOTE* The following params configure the default request config and this
    // is overridden by SolrJ clients. Use the setters on the SolrJ clients to
    // to configure these settings if that is the intent.

    // Follow redirects
    public static final String PROP_FOLLOW_REDIRECTS = "followRedirects";

    // socket timeout measured in ms, closes a socket if read
    // takes longer than x ms to complete. throws
    // java.net.SocketTimeoutException: Read timed out exception
    public static final String PROP_SO_TIMEOUT = "socketTimeout";
    // connection timeout measures in ms, closes a socket if connection
    // cannot be established within x ms. with a
    // java.net.SocketTimeoutException: Connection timed out
    public static final String PROP_CONNECTION_TIMEOUT = "connTimeout";

    /**
     * A Java system property to select the {@linkplain HttpClientBuilderFactory} used for
     * configuring the {@linkplain HttpClientBuilder} instance by default.
     */
    public static final String SYS_PROP_HTTP_CLIENT_BUILDER_FACTORY = "solr.httpclient.builder.factory";

    static final DefaultHttpRequestRetryHandler NO_RETRY = new DefaultHttpRequestRetryHandler(0, false);

    private static volatile SolrHttpClientBuilder httpClientBuilder;

    private static SolrHttpClientContextBuilder httpClientRequestContextBuilder = new SolrHttpClientContextBuilder();

    private static volatile SchemaRegistryProvider schemaRegistryProvider;
    private static volatile String cookiePolicy;
    private static final List<HttpRequestInterceptor> interceptors = new CopyOnWriteArrayList<>();

    static {
        resetHttpClientBuilder();

        // Configure the HttpClientBuilder if user has specified the factory type.
        String factoryClassName = System.getProperty(SYS_PROP_HTTP_CLIENT_BUILDER_FACTORY);
        if (factoryClassName != null) {
            log.debug("Using " + factoryClassName);
            try {
                HttpClientBuilderFactory factory = (HttpClientBuilderFactory) Class.forName(factoryClassName)
                        .getConstructor().newInstance();
                httpClientBuilder = factory.getHttpClientBuilder(Optional.of(SolrHttpClientBuilder.create()));
            } catch (InstantiationException | IllegalAccessException | ClassNotFoundException
                    | InvocationTargetException | NoSuchMethodException e) {
                throw new RuntimeException("Unable to instantiate Solr HttpClientBuilderFactory", e);
            }
        }
    }

    public static abstract class SchemaRegistryProvider {
        /** Must be non-null */
        public abstract Registry<ConnectionSocketFactory> getSchemaRegistry();
    }

    private static class DynamicInterceptor implements HttpRequestInterceptor {

        @Override
        public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
            // don't synchronize traversal - can lead to deadlock - CopyOnWriteArrayList is critical
            // we also do not want to have to acquire the mutex when the list is empty or put a global
            // mutex around the process calls
            interceptors.forEach(new Consumer<HttpRequestInterceptor>() {

                @Override
                public void accept(HttpRequestInterceptor interceptor) {
                    try {
                        interceptor.process(request, context);
                    } catch (Exception e) {
                        log.error("", e);
                    }
                }
            });

        }
    }

    public static void setHttpClientBuilder(SolrHttpClientBuilder newHttpClientBuilder) {
        httpClientBuilder = newHttpClientBuilder;
    }

    public static void setHttpClientProvider(SolrHttpClientBuilder newHttpClientBuilder) {
        httpClientBuilder = newHttpClientBuilder;
    }

    /**
     * @see #SYS_PROP_CHECK_PEER_NAME
     */
    public static void setSchemaRegistryProvider(SchemaRegistryProvider newRegistryProvider) {
        schemaRegistryProvider = newRegistryProvider;
    }

    public static SolrHttpClientBuilder getHttpClientBuilder() {
        return httpClientBuilder;
    }

    /**
     * @see #SYS_PROP_CHECK_PEER_NAME
     */
    public static SchemaRegistryProvider getSchemaRegisteryProvider() {
        return schemaRegistryProvider;
    }

    public static void resetHttpClientBuilder() {
        schemaRegistryProvider = new DefaultSchemaRegistryProvider();
        httpClientBuilder = SolrHttpClientBuilder.create();
    }

    private static final class DefaultSchemaRegistryProvider extends SchemaRegistryProvider {
        @Override
        public Registry<ConnectionSocketFactory> getSchemaRegistry() {
            // this mimics PoolingHttpClientConnectionManager's default behavior,
            // except that we explicitly use SSLConnectionSocketFactory.getSystemSocketFactory()
            // to pick up the system level default SSLContext (where javax.net.ssl.* properties
            // related to keystore & truststore are specified)
            RegistryBuilder<ConnectionSocketFactory> builder = RegistryBuilder.<ConnectionSocketFactory>create();
            builder.register("http", PlainConnectionSocketFactory.getSocketFactory());

            // logic to turn off peer host check
            SSLConnectionSocketFactory sslConnectionSocketFactory = null;
            boolean sslCheckPeerName = toBooleanDefaultIfNull(
                    toBooleanObject(System.getProperty(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME)), true);
            if (sslCheckPeerName) {
                sslConnectionSocketFactory = SSLConnectionSocketFactory.getSystemSocketFactory();
            } else {
                sslConnectionSocketFactory = new SSLConnectionSocketFactory(SSLContexts.createSystemDefault(),
                        NoopHostnameVerifier.INSTANCE);
                log.debug(HttpClientUtil.SYS_PROP_CHECK_PEER_NAME + "is false, hostname checks disabled.");
            }
            builder.register("https", sslConnectionSocketFactory);

            return builder.build();
        }
    }

    /**
     * Creates new http client by using the provided configuration.
     * 
     * @param params
     *          http client configuration, if null a client with default
     *          configuration (no additional configuration) is created. 
     */
    public static CloseableHttpClient createClient(SolrParams params) {
        return createClient(params, createPoolingConnectionManager());
    }

    /** test usage subject to change @lucene.experimental */
    static PoolingHttpClientConnectionManager createPoolingConnectionManager() {
        return new PoolingHttpClientConnectionManager(schemaRegistryProvider.getSchemaRegistry());
    }

    public static CloseableHttpClient createClient(SolrParams params, PoolingHttpClientConnectionManager cm) {
        if (params == null) {
            params = new ModifiableSolrParams();
        }

        return createClient(params, cm, false);
    }

    public static CloseableHttpClient createClient(final SolrParams params, PoolingHttpClientConnectionManager cm,
            boolean sharedConnectionManager, HttpRequestExecutor httpRequestExecutor) {
        final ModifiableSolrParams config = new ModifiableSolrParams(params);
        if (log.isDebugEnabled()) {
            log.debug("Creating new http client, config:" + config);
        }

        cm.setMaxTotal(params.getInt(HttpClientUtil.PROP_MAX_CONNECTIONS, 10000));
        cm.setDefaultMaxPerRoute(params.getInt(HttpClientUtil.PROP_MAX_CONNECTIONS_PER_HOST, 10000));
        cm.setValidateAfterInactivity(
                Integer.getInteger(VALIDATE_AFTER_INACTIVITY, VALIDATE_AFTER_INACTIVITY_DEFAULT));

        HttpClientBuilder newHttpClientBuilder = HttpClientBuilder.create();

        if (sharedConnectionManager) {
            newHttpClientBuilder.setConnectionManagerShared(true);
        } else {
            newHttpClientBuilder.setConnectionManagerShared(false);
        }

        ConnectionKeepAliveStrategy keepAliveStrat = new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                // we only close connections based on idle time, not ttl expiration
                return -1;
            }
        };

        if (httpClientBuilder.getAuthSchemeRegistryProvider() != null) {
            newHttpClientBuilder.setDefaultAuthSchemeRegistry(
                    httpClientBuilder.getAuthSchemeRegistryProvider().getAuthSchemeRegistry());
        }
        if (httpClientBuilder.getCookieSpecRegistryProvider() != null) {
            newHttpClientBuilder.setDefaultCookieSpecRegistry(
                    httpClientBuilder.getCookieSpecRegistryProvider().getCookieSpecRegistry());
        }
        if (httpClientBuilder.getCredentialsProviderProvider() != null) {
            newHttpClientBuilder.setDefaultCredentialsProvider(
                    httpClientBuilder.getCredentialsProviderProvider().getCredentialsProvider());
        }

        newHttpClientBuilder.addInterceptorLast(new DynamicInterceptor());

        newHttpClientBuilder = newHttpClientBuilder.setKeepAliveStrategy(keepAliveStrat).evictIdleConnections(
                (long) Integer.getInteger(EVICT_IDLE_CONNECTIONS, EVICT_IDLE_CONNECTIONS_DEFAULT),
                TimeUnit.MILLISECONDS);

        if (httpRequestExecutor != null) {
            newHttpClientBuilder.setRequestExecutor(httpRequestExecutor);
        }

        HttpClientBuilder builder = setupBuilder(newHttpClientBuilder, params);

        HttpClient httpClient = builder.setConnectionManager(cm).build();

        assert ObjectReleaseTracker.track(httpClient);
        return (CloseableHttpClient) httpClient;
    }

    /**
     * Creates new http client by using the provided configuration.
     * 
     */
    public static CloseableHttpClient createClient(final SolrParams params, PoolingHttpClientConnectionManager cm,
            boolean sharedConnectionManager) {
        return createClient(params, cm, sharedConnectionManager, null);
    }

    private static HttpClientBuilder setupBuilder(HttpClientBuilder builder, SolrParams config) {

        Builder requestConfigBuilder = RequestConfig.custom()
                .setRedirectsEnabled(config.getBool(HttpClientUtil.PROP_FOLLOW_REDIRECTS, false))
                .setDecompressionEnabled(false)
                .setConnectTimeout(config.getInt(HttpClientUtil.PROP_CONNECTION_TIMEOUT, DEFAULT_CONNECT_TIMEOUT))
                .setSocketTimeout(config.getInt(HttpClientUtil.PROP_SO_TIMEOUT, DEFAULT_SO_TIMEOUT));

        String cpolicy = cookiePolicy;
        if (cpolicy != null) {
            requestConfigBuilder.setCookieSpec(cpolicy);
        }

        RequestConfig requestConfig = requestConfigBuilder.build();

        HttpClientBuilder retBuilder = builder.setDefaultRequestConfig(requestConfig);

        if (config.getBool(HttpClientUtil.PROP_USE_RETRY, true)) {
            retBuilder = retBuilder.setRetryHandler(
                    new SolrHttpRequestRetryHandler(Integer.getInteger("solr.httpclient.retries", 3)));

        } else {
            retBuilder = retBuilder.setRetryHandler(NO_RETRY);
        }

        final String basicAuthUser = config.get(HttpClientUtil.PROP_BASIC_AUTH_USER);
        final String basicAuthPass = config.get(HttpClientUtil.PROP_BASIC_AUTH_PASS);

        if (basicAuthUser != null && basicAuthPass != null) {
            CredentialsProvider credsProvider = new BasicCredentialsProvider();
            credsProvider.setCredentials(AuthScope.ANY,
                    new UsernamePasswordCredentials(basicAuthUser, basicAuthPass));
            retBuilder.setDefaultCredentialsProvider(credsProvider);
        }

        if (config.getBool(HttpClientUtil.PROP_ALLOW_COMPRESSION, false)) {
            retBuilder.addInterceptorFirst(new UseCompressionRequestInterceptor());
            retBuilder.addInterceptorFirst(new UseCompressionResponseInterceptor());
        } else {
            retBuilder.disableContentCompression();
        }

        return retBuilder;
    }

    public static void close(HttpClient httpClient) {

        org.apache.solr.common.util.IOUtils.closeQuietly((CloseableHttpClient) httpClient);

        assert ObjectReleaseTracker.release(httpClient);
    }

    public static void addRequestInterceptor(HttpRequestInterceptor interceptor) {
        interceptors.add(interceptor);
    }

    public static void removeRequestInterceptor(HttpRequestInterceptor interceptor) {
        interceptors.remove(interceptor);
    }

    public static void clearRequestInterceptors() {
        interceptors.clear();
    }

    private static class UseCompressionRequestInterceptor implements HttpRequestInterceptor {

        @Override
        public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
            if (!request.containsHeader("Accept-Encoding")) {
                request.addHeader("Accept-Encoding", "gzip, deflate");
            }
        }
    }

    private static class UseCompressionResponseInterceptor implements HttpResponseInterceptor {

        @Override
        public void process(final HttpResponse response, final HttpContext context)
                throws HttpException, IOException {

            HttpEntity entity = response.getEntity();
            Header ceheader = entity.getContentEncoding();
            if (ceheader != null) {
                HeaderElement[] codecs = ceheader.getElements();
                for (int i = 0; i < codecs.length; i++) {
                    if (codecs[i].getName().equalsIgnoreCase("gzip")) {
                        response.setEntity(new GzipDecompressingEntity(response.getEntity()));
                        return;
                    }
                    if (codecs[i].getName().equalsIgnoreCase("deflate")) {
                        response.setEntity(new DeflateDecompressingEntity(response.getEntity()));
                        return;
                    }
                }
            }
        }
    }

    private static class GzipDecompressingEntity extends HttpEntityWrapper {
        public GzipDecompressingEntity(final HttpEntity entity) {
            super(entity);
        }

        @Override
        public InputStream getContent() throws IOException, IllegalStateException {
            return new GZIPInputStream(wrappedEntity.getContent());
        }

        @Override
        public long getContentLength() {
            return -1;
        }
    }

    private static class DeflateDecompressingEntity extends GzipDecompressingEntity {
        public DeflateDecompressingEntity(final HttpEntity entity) {
            super(entity);
        }

        @Override
        public InputStream getContent() throws IOException, IllegalStateException {
            return new InflaterInputStream(wrappedEntity.getContent());
        }
    }

    public static void setHttpClientRequestContextBuilder(SolrHttpClientContextBuilder httpClientContextBuilder) {
        httpClientRequestContextBuilder = httpClientContextBuilder;
    }

    /**
     * Create a HttpClientContext object and {@link HttpClientContext#setUserToken(Object)}
     * to an internal singleton. It allows to reuse underneath {@link HttpClient} 
     * in connection pools if client authentication is enabled.
     */
    public static HttpClientContext createNewHttpClientRequestContext() {
        HttpClientContext context = httpClientRequestContextBuilder.createContext(HttpSolrClient.cacheKey);

        return context;
    }

    public static Builder createDefaultRequestConfigBuilder() {
        String cpolicy = cookiePolicy;
        Builder builder = RequestConfig.custom();

        builder.setSocketTimeout(DEFAULT_SO_TIMEOUT).setConnectTimeout(DEFAULT_CONNECT_TIMEOUT)
                .setRedirectsEnabled(false).setDecompressionEnabled(false); // we do our own compression / decompression
        if (cpolicy != null) {
            builder.setCookieSpec(cpolicy);
        }
        return builder;
    }

    public static void setCookiePolicy(String policyName) {
        cookiePolicy = policyName;
    }

    /**
     * @lucene.internal
     */
    static boolean toBooleanDefaultIfNull(Boolean bool, boolean valueIfNull) {
        if (bool == null) {
            return valueIfNull;
        }
        return bool.booleanValue() ? true : false;
    }

    /**
     * @lucene.internal
     */
    static Boolean toBooleanObject(String str) {
        if ("true".equalsIgnoreCase(str)) {
            return Boolean.TRUE;
        } else if ("false".equalsIgnoreCase(str)) {
            return Boolean.FALSE;
        }
        // no match
        return null;
    }
}