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

groovyx.net.http.OkHttpBuilder Maven / Gradle / Ivy

There is a newer version: 1.0.4
Show newest version
/**
 * Copyright (C) 2017 HttpBuilder-NG Project
 *
 * Licensed 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 groovyx.net.http;

import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import okhttp3.*;
import okio.BufferedSink;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.Function;

import static groovyx.net.http.FromServer.Header.keyValue;
import static groovyx.net.http.HttpBuilder.ResponseHandlerFunction.HANDLER_FUNCTION;
import static groovyx.net.http.HttpConfig.AuthType.DIGEST;

/**
 * `HttpBuilder` implementation based on the http://square.github.io/okhttp/[OkHttp] client library.
 *
 * Generally, this class should not be used directly, the preferred method of instantiation is via one of the two static `configure()` methods of this
 * class or using one of the `configure` methods of `HttpBuilder` with a factory function for this builder.
 */
public class OkHttpBuilder extends HttpBuilder {

    private static final Function okFactory = OkHttpBuilder::new;
    private final ChainedHttpConfig config;
    private final HttpObjectConfig.Client clientConfig;
    private final Executor executor;
    private final OkHttpClient client;

    protected OkHttpBuilder(final HttpObjectConfig config) {
        super(config);

        this.config = new HttpConfigs.ThreadSafeHttpConfig(config.getChainedConfig());
        this.clientConfig = config.getClient();
        this.executor = config.getExecution().getExecutor();

        final OkHttpClient.Builder builder = new OkHttpClient.Builder();

        final SSLContext sslContext = config.getExecution().getSslContext();
        if (sslContext != null) {
            builder.sslSocketFactory(sslContext.getSocketFactory()/*, (X509TrustManager) TRUST_MANAGERS[0]*/);
            builder.hostnameVerifier(config.getExecution().getHostnameVerifier());
        }

        // DIGEST support - defining this here only allows DIGEST config on the HttpBuilder configuration, not for individual methods.
        final HttpConfig.Auth auth = config.getRequest().getAuth();
        if (auth != null && auth.getAuthType() == DIGEST) {
            Map authCache = new ConcurrentHashMap<>();

            builder.addInterceptor(new AuthenticationCacheInterceptor(authCache));
            builder.authenticator(new CachingAuthenticatorDecorator(
                new DigestAuthenticator(new com.burgstaller.okhttp.digest.Credentials(auth.getUser(), auth.getPassword())), authCache)
            );
        }

        final Consumer clientCustomizer = clientConfig.getClientCustomizer();
        if (clientCustomizer != null) {
            clientCustomizer.accept(builder);
        }

        this.client = builder.build();
    }

    /**
     * Retrieves the internal client implementation as an {@link OkHttpClient} instance.
     *
     * @return the reference to the internal client implementation as an {@link OkHttpClient}
     */
    public Object getClientImplementation() {
        return client;
    }

    /**
     * Creates an `HttpBuilder` using the `OkHttpBuilder` factory instance configured with the provided configuration closure.
     *
     * The configuration closure delegates to the {@link HttpObjectConfig} interface, which is an extension of the {@link HttpConfig} interface -
     * configuration properties from either may be applied to the global client configuration here. See the documentation for those interfaces for
     * configuration property details.
     *
     * [source,groovy]
     * ----
     * def http = HttpBuilder.configure {
     * request.uri = 'http://localhost:10101'
     * }
     * ----
     *
     * @param closure the configuration closure (delegated to {@link HttpObjectConfig})
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(@DelegatesTo(HttpObjectConfig.class) final Closure closure) {
        return configure(okFactory, closure);
    }

    /**
     * Creates an `HttpBuilder` using the `OkHttpBuilder` factory instance configured with the provided configuration function.
     *
     * The configuration {@link Consumer} function accepts an instance of the {@link HttpObjectConfig} interface, which is an extension of the {@link HttpConfig}
     * interface - configuration properties from either may be applied to the global client configuration here. See the documentation for those interfaces for
     * configuration property details.
     *
     * This configuration method is generally meant for use with standard Java.
     *
     * [source,java]
     * ----
     * HttpBuilder.configure(new Consumer() {
     * public void accept(HttpObjectConfig config) {
     * config.getRequest().setUri(format("http://localhost:%d", serverRule.getPort()));
     * }
     * });
     * ----
     *
     * Or, using lambda expressions:
     *
     * [source,java]
     * ----
     * HttpBuilder.configure(config -> {
     * config.getRequest().setUri(format("http://localhost:%d", serverRule.getPort()));
     * });
     * ----
     *
     * @param configuration the configuration function (accepting {@link HttpObjectConfig})
     * @return the configured `HttpBuilder`
     */
    public static HttpBuilder configure(final Consumer configuration) {
        return configure(okFactory, configuration);
    }

    @Override
    protected ChainedHttpConfig getObjectConfig() {
        return config;
    }

    @Override
    public Executor getExecutor() {
        return executor;
    }

    @Override
    protected Object doGet(final ChainedHttpConfig chainedConfig) {
        return execute((url) -> new Request.Builder().get().url(url), chainedConfig);
    }

    @Override
    protected Object doHead(final ChainedHttpConfig chainedConfig) {
        return execute((url) -> new Request.Builder().head().url(url), chainedConfig);
    }

    @Override
    protected Object doPost(final ChainedHttpConfig chainedConfig) {
        return execute((url) -> new Request.Builder().post(resolveRequestBody(chainedConfig)).url(url),
            chainedConfig);
    }

    @Override
    protected Object doPut(final ChainedHttpConfig chainedConfig) {
        return execute((url) -> new Request.Builder().put(resolveRequestBody(chainedConfig)).url(url),
            chainedConfig);
    }

    @Override
    protected Object doPatch(final ChainedHttpConfig chainedConfig) {
        return execute((url) -> new Request.Builder().patch(resolveRequestBody(chainedConfig)).url(url),
            chainedConfig);
    }

    @Override
    protected Object doDelete(final ChainedHttpConfig chainedConfig) {
        return execute((url) -> new Request.Builder().delete().url(url), chainedConfig);
    }

    @Override
    public void close() throws IOException {
        // does nothing
    }

    private RequestBody resolveRequestBody(ChainedHttpConfig chainedConfig) {
        final ChainedHttpConfig.ChainedRequest cr = chainedConfig.getChainedRequest();
        RequestBody body = RequestBody.create(MediaType.parse(cr.actualContentType()), "");
        if (cr.actualBody() != null) {
            final OkHttpToServer toServer = new OkHttpToServer(chainedConfig);
            chainedConfig.findEncoder().accept(chainedConfig, toServer);

            body = toServer;
        }
        return body;
    }

    private void applyHeaders(final Request.Builder requestBuilder, final ChainedHttpConfig.ChainedRequest cr) throws URISyntaxException {
        for (Map.Entry entry : cr.actualHeaders(new LinkedHashMap<>()).entrySet()) {
            requestBuilder.addHeader(entry.getKey(), entry.getValue());
        }

        final String contentType = cr.actualContentType();
        if (contentType != null) {
            requestBuilder.addHeader("Content-Type", contentType);
        }

        for (Map.Entry e : cookiesToAdd(clientConfig, cr).entrySet()) {
            requestBuilder.addHeader(e.getKey(), e.getValue());
        }
    }

    private static void applyAuth(final Request.Builder requestBuilder, final ChainedHttpConfig chainedConfig) {
        final HttpConfig.Auth auth = chainedConfig.getChainedRequest().actualAuth();
        if (auth != null) {
            switch (auth.getAuthType()) {
                case BASIC:
                    requestBuilder.addHeader("Authorization", Credentials.basic(auth.getUser(), auth.getPassword()));
                    break;
                case DIGEST:
                    // supported in constructor with an interceptor
            }
        }
    }

    private Object execute(final Function makeBuilder, final ChainedHttpConfig chainedConfig) {
        try {
            final ChainedHttpConfig.ChainedRequest cr = chainedConfig.getChainedRequest();
            final URI uri = cr.getUri().toURI();
            final HttpUrl httpUrl = HttpUrl.get(uri);
            final Request.Builder requestBuilder = makeBuilder.apply(httpUrl);

            applyHeaders(requestBuilder, cr);
            applyAuth(requestBuilder, chainedConfig);

            try (Response response = client.newCall(requestBuilder.build()).execute()) {
                return HANDLER_FUNCTION.apply(chainedConfig,
                    new OkHttpFromServer(chainedConfig.getChainedRequest().getUri().toURI(), response));
            } catch (IOException ioe) {
                throw ioe; //re-throw, close has happened
            }
        } catch (Exception e) {
            return handleException(chainedConfig.getChainedResponse(), e);
        }
    }

    private class OkHttpFromServer implements FromServer {

        private final URI uri;
        private final Response response;
        private List> headers;

        private OkHttpFromServer(final URI uri, final Response response) {
            this.uri = uri;
            this.response = response;
            this.headers = populateHeaders();
            addCookieStore(uri, headers);
        }

        private List> populateHeaders() {
            final Headers headers = response.headers();
            List> ret = new ArrayList<>();
            for (String name : headers.names()) {
                List values = headers.values(name);
                for (String value : values) {
                    ret.add(keyValue(name, value));
                }
            }

            return ret;
        }

        @Override
        public InputStream getInputStream() {
            return response.body().byteStream();
        }

        @Override
        public int getStatusCode() {
            return response.code();
        }

        @Override
        public String getMessage() {
            return response.message();
        }

        @Override
        public List> getHeaders() {
            return headers;
        }

        @Override
        public boolean getHasBody() {
            try {
                return !response.body().source().exhausted();
            } catch (IOException e) {
                return false;
            }
        }

        @Override
        public URI getUri() {
            return uri;
        }

        @Override
        public void finish() {
            response.close();
        }
    }

    private static class OkHttpToServer extends RequestBody implements ToServer {

        private ChainedHttpConfig config;
        private InputStream inputStream;

        private OkHttpToServer(final ChainedHttpConfig config) {
            this.config = config;
        }

        @Override
        public void toServer(final InputStream inputStream) {
            this.inputStream = inputStream;
        }

        @Override
        public MediaType contentType() {
            return MediaType.parse(config.findContentType());
        }

        @Override
        public void writeTo(final BufferedSink sink) throws IOException {
            try {
                int b = inputStream.read();
                while (b != -1) {
                    sink.writeByte(b);
                    b = inputStream.read();
                }
            } finally {
                inputStream.close();
            }
        }
    }
}