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

org.davidmoten.rx2.io.Client Maven / Gradle / Ivy

package org.davidmoten.rx2.io;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

import org.davidmoten.rx2.io.internal.FlowableFromInputStream;
import org.davidmoten.rx2.io.internal.FlowableSingleFlatMapPublisher;
import org.davidmoten.rx2.io.internal.HttpMethod;
import org.davidmoten.rx2.io.internal.Util;

import com.github.davidmoten.guavamini.Preconditions;
import com.github.davidmoten.guavamini.annotations.VisibleForTesting;

import io.reactivex.Flowable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.functions.BiConsumer;
import io.reactivex.functions.Consumer;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;

public final class Client {

    private Client() {
        // prevent instantiation
    }

    /**
     * Returns a Builder for an over-network {@link Flowable}. The HTTP verb used
     * with the {@code url} will be {@code GET}.
     * 
     * @param url
     *            location of the stream to connect to using HTTP GET
     * @return Builder for an over-network {@code Flowable}
     */
    public static Builder get(String url) {
        Preconditions.checkNotNull(url);
        return new Builder(url, HttpMethod.GET);
    }

    /**
     * Returns a Builder for an over-network {@link Flowable}. The HTTP verb used
     * with the {@code url} will be {@code POST}.
     * 
     * @param url
     *            location of the stream to connect to using HTTP POST
     * @return Builder for an over-network {@code Flowable}
     */
    public static Builder post(String url) {
        Preconditions.checkNotNull(url);
        return new Builder(url, HttpMethod.POST);
    }

    public static final class Builder {

        private final String url;
        private final HttpMethod method;
        private int connectTimeoutMs = 30000;
        private int readTimeoutMs = 0;
        private Map requestHeaders = new HashMap<>();
        private SSLSocketFactory sslSocketFactory;
        private List> transforms = new ArrayList<>();
        private Proxy proxy;
        private Scheduler requestScheduler = Schedulers.trampoline();

        Builder(String url, HttpMethod method) {
            this.url = url;
            this.method = method;
        }

        /**
         * Sets the read timeout in ms for the HTTP connection. Default is zero which is
         * advisable for a long-running occasionally quiet stream. When a read timeout
         * occurs the {@link Flowable} will emit an error.
         * 
         * @param timeoutMs
         *            read timeout for the HTTP connection.
         * @return this
         */
        public Builder readTimeoutMs(int timeoutMs) {
            Preconditions.checkArgument(timeoutMs >= 0);
            this.readTimeoutMs = timeoutMs;
            return this;
        }

        /**
         * Sets the connect timeout in ms for the HTTP connection. Default is 30s which
         * is When a read timeout occurs the {@link Flowable} will emit an error.
         * 
         * @param timeoutMs
         *            connect timeout for the HTTP connection.
         * @return this
         */
        public Builder connectTimeoutMs(int timeoutMs) {
            Preconditions.checkArgument(timeoutMs >= 0);
            this.connectTimeoutMs = timeoutMs;
            return this;
        }

        /**
         * Sets the proxy details for the HTTP connection.
         * 
         * @param host
         *            proxy host
         * @param port
         *            proxy port
         * @return this
         */
        public Builder proxy(String host, int port) {
            Preconditions.checkNotNull(host);
            Preconditions.checkArgument(port > 0);
            return proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)));
        }

        /**
         * Sets the proxy for the HTTP connection.
         * 
         * @param proxy
         *            the proxy details
         * @return this
         */
        public Builder proxy(Proxy proxy) {
            Preconditions.checkNotNull(proxy);
            this.proxy = proxy;
            return this;
        }

        /**
         * Sets the actions that should be applied to {@link HttpURLConnection} before
         * calling {@code HttpURLConnection.getInputStream()}. This applies to all HTTP
         * calls being the subscription, request and cancel calls.
         * 
         * @param transform
         *            action to apply
         * @return this
         */
        public Builder transform(Consumer transform) {
            Preconditions.checkNotNull(transform);
            this.transforms.add(transform);
            return this;
        }

        /**
         * Sets the Basic Authentication details for all HTTP connections (subscription,
         * request and cancellation).
         * 
         * @param username
         *            authentication username
         * @param password
         *            authentication password
         * @return this
         */
        public Builder basicAuth(String username, String password) {
            Preconditions.checkNotNull(username);
            Preconditions.checkNotNull(password);
            String s = Base64.getEncoder()
                    .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
            return requestHeader("Authorization", "Basic " + s);
        }

        /**
         * Sets a request header for the HTTP connection.
         * 
         * @param key
         *            request header key
         * @param value
         *            request header value
         * @return this
         */
        public Builder requestHeader(String key, String value) {
            Preconditions.checkNotNull(key);
            Preconditions.checkNotNull(value);
            requestHeaders.put(key, value);
            return this;
        }

        /**
         * Sets the {@link SSLSocketFactory} to be used for HTTPS connections.
         * 
         * @param sslSocketFactory
         *            ssl socket factory
         * @return this
         */
        public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory) {
            Preconditions.checkNotNull(sslSocketFactory);
            this.sslSocketFactory = sslSocketFactory;
            return this;
        }

        /**
         * Sets the {@link SSLContext} to be used for HTTP connections. The socket
         * factory will be obtained from the SSLContext.
         * 
         * @param sslContext
         *            ssl context
         * @return this
         */
        public Builder sslContext(SSLContext sslContext) {
            Preconditions.checkNotNull(sslContext);
            return sslSocketFactory(sslContext.getSocketFactory());
        }

        public Builder requestScheduler(Scheduler scheduler) {
            this.requestScheduler = scheduler;
            return this;
        }

        /**
         * Sets the deserializer to be used on the arriving {@link ByteBuffer}s.
         * 
         * @param serializer
         *            the deserializer to be used
         * @return this
         */
        public  Flowable deserializer(Deserializer serializer) {
            Preconditions.checkNotNull(serializer);
            return build().map(serializer::deserialize);
        }

        /**
         * Returns the {@link Flowable} where deserialization is performed by
         * {@link Serializer#javaIo()}.
         * 
         * @param 
         *            the Flowable result type
         * @return the built Flowable
         */
        public  Flowable deserialized() {
            return deserializer(Serializer.javaIo());
        }

        /**
         * Returns the built {@code Flowable} based on all the builder
         * options specified.
         * 
         * @return the built Flowable.
         */
        public Flowable build() {
            return toFlowable(url, new Options(method, connectTimeoutMs, readTimeoutMs,
                    requestHeaders, sslSocketFactory, transforms, proxy, requestScheduler));
        }
    }

    public static Flowable read(Single inSource,
            BiConsumer requester) {
        return new FlowableSingleFlatMapPublisher<>(inSource,
                in -> new FlowableFromInputStream(in, requester));
    }

    private static Flowable toFlowable(String url, Options options) {
        URL u;
        try {
            u = new URL(url);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        BiConsumer requester = new Requester(url, options);

        return Flowable.using( //
                () -> {
                    final HttpURLConnection con = open(u, options);
                    prepareConnection(con, options);
                    return con.getInputStream();
                }, //
                in -> read(Single.just(in), requester), //
                in -> Util.close(in));
    }

    private static HttpURLConnection open(URL url, Options options) throws IOException {
        if (options.proxy == null) {
            return (HttpURLConnection) url.openConnection();
        } else {
            return (HttpURLConnection) url.openConnection(options.proxy);
        }
    }

    private static void prepareConnection(HttpURLConnection con, Options options)
            throws ProtocolException {
        con.setRequestMethod(options.method.method());
        con.setUseCaches(false);
        con.setConnectTimeout(options.connectTimeoutMs);
        con.setReadTimeout(options.readTimeoutMs);
        options.requestHeaders.entrySet().stream()
                .forEach(entry -> con.setRequestProperty(entry.getKey(), entry.getValue()));
        if (options.sslSocketFactory != null && con instanceof HttpsURLConnection) {
            ((HttpsURLConnection) con).setSSLSocketFactory(options.sslSocketFactory);
        }
        transform(con, options.transforms);
    }

    private static void transform(final HttpURLConnection con,
            List> transforms) {
        transforms.stream().forEach(transform -> {
            try {
                transform.accept(con);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }

    @VisibleForTesting
    static final class Options {
        final HttpMethod method;
        final int connectTimeoutMs;
        int readTimeoutMs;
        final Map requestHeaders;
        final SSLSocketFactory sslSocketFactory;
        final List> transforms;
        final Proxy proxy;
        final Scheduler requestScheduler;

        Options(HttpMethod method, int connectTimeoutMs, int readTimeoutMs,
                Map requestHeaders, SSLSocketFactory sslSocketFactory,
                List> transforms, Proxy proxy,
                Scheduler requestScheduler) {
            this.method = method;
            this.connectTimeoutMs = connectTimeoutMs;
            this.readTimeoutMs = readTimeoutMs;
            this.requestHeaders = requestHeaders;
            this.sslSocketFactory = sslSocketFactory;
            this.transforms = transforms;
            this.proxy = proxy;
            this.requestScheduler = requestScheduler;
        }
    }

    static final class Requester implements BiConsumer {

        private final String url;
        private final Options options;

        Requester(String url, Options options) {
            this.url = url;
            this.options = options;
        }

        @Override
        public void accept(Long id, Long request) throws Exception {
            options.requestScheduler.scheduleDirect(() -> {
                try {
                    HttpURLConnection con = (HttpURLConnection) new URL(
                            url + "?id=" + id + "&r=" + request) //
                                    .openConnection();
                    prepareConnection(con, options);
                    int code = con.getResponseCode();
                    if (code != 200) {
                        throw new IOException(
                                "response code from request call was not 200: " + code);
                    }
                } catch (Throwable e) {
                    RxJavaPlugins.onError(e);
                }
            });
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy