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

com.launchdarkly.eventsource.EventSource Maven / Gradle / Ivy

There is a newer version: 4.1.1
Show newest version
package com.launchdarkly.eventsource;

import okhttp3.*;
import okio.BufferedSource;
import okio.Okio;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

import static com.launchdarkly.eventsource.ReadyState.*;
import static java.lang.String.format;

/**
 * Client for Server-Sent Events
 * aka EventSource
 */
public class EventSource implements ConnectionHandler, Closeable {
  private final Logger logger;

  /**
   * The default value for {@link Builder#reconnectTimeMs(long)}: 1000 (1 second).
   */
  public static final long DEFAULT_RECONNECT_TIME_MS = 1000;
  /**
   * The default value for {@link Builder#maxReconnectTimeMs(long)}: 30000 (30 seconds).
   */
  public static final long DEFAULT_MAX_RECONNECT_TIME_MS = 30000;
  /**
   * The default value for {@link Builder#connectTimeoutMs(int)}: 10000 (10 seconds).
   */
  public static final int DEFAULT_CONNECT_TIMEOUT_MS = 10000;
  /**
   * The default value for {@link Builder#writeTimeoutMs(int)}: 5000 (5 seconds).
   */
  public static final int DEFAULT_WRITE_TIMEOUT_MS = 5000;
  /**
   * The default value for {@link Builder#readTimeoutMs(int)}: 300000 (5 minutes).
   */
  public static final int DEFAULT_READ_TIMEOUT_MS = 1000 * 60 * 5;
  /**
   * The default value for {@link Builder#backoffResetThresholdMs(long)}: 60000 (60 seconds).
   */
  public static final int DEFAULT_BACKOFF_RESET_THRESHOLD_MS = 1000 * 60;

  private final String name;
  private volatile HttpUrl url;
  private final Headers headers;
  private final String method;
  @Nullable private final RequestBody body;
  private final RequestTransformer requestTransformer;
  private final ExecutorService eventExecutor;
  private final ExecutorService streamExecutor;
  private long reconnectTimeMs;
  private long maxReconnectTimeMs;
  private final long backoffResetThresholdMs;
  private volatile String lastEventId;
  private final EventHandler handler;
  private final ConnectionErrorHandler connectionErrorHandler;
  private final AtomicReference readyState;
  private final OkHttpClient client;
  private volatile Call call;
  private final Random jitter = new Random();
  private Response response;
  private BufferedSource bufferedSource;

  EventSource(Builder builder) {
    this.name = builder.name;
    this.logger = LoggerFactory.getLogger(EventSource.class.getCanonicalName() + "." + name);
    this.url = builder.url;
    this.headers = addDefaultHeaders(builder.headers);
    this.method = builder.method;
    this.body = builder.body;
    this.requestTransformer = builder.requestTransformer;
    this.reconnectTimeMs = builder.reconnectTimeMs;
    this.maxReconnectTimeMs = builder.maxReconnectTimeMs;
    this.backoffResetThresholdMs = builder.backoffResetThresholdMs;
    ThreadFactory eventsThreadFactory = createThreadFactory("okhttp-eventsource-events");
    this.eventExecutor = Executors.newSingleThreadExecutor(eventsThreadFactory);
    ThreadFactory streamThreadFactory = createThreadFactory("okhttp-eventsource-stream");
    this.streamExecutor = Executors.newSingleThreadExecutor(streamThreadFactory);
    this.handler = new AsyncEventHandler(this.eventExecutor, builder.handler);
    this.connectionErrorHandler = builder.connectionErrorHandler;
    this.readyState = new AtomicReference<>(RAW);
    this.client = builder.clientBuilder.build();
  }

  private ThreadFactory createThreadFactory(final String type) {
    final ThreadFactory backingThreadFactory =
        Executors.defaultThreadFactory();
    final AtomicLong count = new AtomicLong(0);
    return new ThreadFactory() {
      @Override
      public Thread newThread(Runnable runnable) {
        Thread thread = backingThreadFactory.newThread(runnable);
        thread.setName(format(Locale.ROOT, "%s-[%s]-%d", type, name, count.getAndIncrement()));
        thread.setDaemon(true);
        return thread;
      }
    };
  }

  /**
   * Attempts to connect to the remote event source if not already connected. This method returns
   * immediately; the connection happens on a worker thread.
   */
  public void start() {
    if (!readyState.compareAndSet(RAW, CONNECTING)) {
      logger.info("Start method called on this already-started EventSource object. Doing nothing");
      return;
    }
    logger.debug("readyState change: " + RAW + " -> " + CONNECTING);
    logger.info("Starting EventSource client using URI: " + url);
    streamExecutor.execute(new Runnable() {
      public void run() {
        connect();
      }
    });
  }

  /**
   * Returns an enum indicating the current status of the connection.
   * @return a {@link ReadyState} value
   */
  public ReadyState getState() {
    return readyState.get();
  }

  @Override
  public void close() {
    ReadyState currentState = readyState.getAndSet(SHUTDOWN);
    logger.debug("readyState change: " + currentState + " -> " + SHUTDOWN);
    if (currentState == SHUTDOWN) {
      return;
    }
    if (currentState == ReadyState.OPEN) {
      try {
        handler.onClosed();
      } catch (Exception e) {
        handler.onError(e);
      }
    }

    if (call != null) {
      // The call.cancel() must precede the bufferedSource.close().
      // Otherwise, an IllegalArgumentException "Unbalanced enter/exit" error is thrown by okhttp.
      // https://github.com/google/ExoPlayer/issues/1348
      call.cancel();
      logger.debug("call cancelled");
    }

    eventExecutor.shutdownNow();
    streamExecutor.shutdownNow();

    if (client != null) {
      if (client.connectionPool() != null) {
        client.connectionPool().evictAll();
      }
      if (client.dispatcher() != null) {
        client.dispatcher().cancelAll();
        if (client.dispatcher().executorService() != null) {
          client.dispatcher().executorService().shutdownNow();
        }
      }
    }
  }
  
  Request buildRequest() {
    Request.Builder builder = new Request.Builder()
        .headers(headers)
        .url(url)
        .method(method, body);

    if (lastEventId != null && !lastEventId.isEmpty()) {
      builder.addHeader("Last-Event-ID", lastEventId);
    }
    
    Request request = builder.build();
    return requestTransformer == null ? request : requestTransformer.transformRequest(request);
  }

  private void connect() {
    response = null;
    bufferedSource = null;

    int reconnectAttempts = 0;
    ConnectionErrorHandler.Action errorHandlerAction = null;
    
    try {
      while (!Thread.currentThread().isInterrupted() && readyState.get() != SHUTDOWN) {
        long connectedTime = -1;

        ReadyState currentState = readyState.getAndSet(CONNECTING);
        logger.debug("readyState change: " + currentState + " -> " + CONNECTING);
        try {        	  
          call = client.newCall(buildRequest());
          response = call.execute();
          if (response.isSuccessful()) {
            connectedTime = System.currentTimeMillis();
            currentState = readyState.getAndSet(OPEN);
            if (currentState != CONNECTING) {
              logger.warn("Unexpected readyState change: " + currentState + " -> " + OPEN);
            } else {
              logger.debug("readyState change: " + currentState + " -> " + OPEN);
            }
            logger.info("Connected to Event Source stream.");
            try {
              handler.onOpen();
            } catch (Exception e) {
              handler.onError(e);
            }
            if (bufferedSource != null) {
              bufferedSource.close();
            }
            bufferedSource = Okio.buffer(response.body().source());
            EventParser parser = new EventParser(url.uri(), handler, EventSource.this);
            for (String line; !Thread.currentThread().isInterrupted() && (line = bufferedSource.readUtf8LineStrict()) != null; ) {
              parser.line(line);
            }
          } else {
            logger.debug("Unsuccessful Response: " + response);
            errorHandlerAction = dispatchError(new UnsuccessfulResponseException(response.code()));
          }
        } catch (EOFException eofe) {
          logger.warn("Connection unexpectedly closed.");
        } catch (IOException ioe) {
          if (readyState.get() != SHUTDOWN) {
        	  	logger.debug("Connection problem.", ioe);
        	  	errorHandlerAction = dispatchError(ioe);
          } else {
        	  	errorHandlerAction = ConnectionErrorHandler.Action.SHUTDOWN;
          }
        } finally {
          ReadyState nextState = CLOSED;
          if (errorHandlerAction == ConnectionErrorHandler.Action.SHUTDOWN) {
            logger.info("Connection has been explicitly shut down by error handler");
            nextState = SHUTDOWN;
          }
          currentState = readyState.getAndSet(nextState);
          logger.debug("readyState change: " + currentState + " -> " + nextState);

          if (response != null && response.body() != null) {
            response.close();
            logger.debug("response closed");
          }

          if (bufferedSource != null) {
            try {
              bufferedSource.close();
              logger.debug("buffered source closed");
            } catch (IOException e) {
              logger.warn("Exception when closing bufferedSource", e);
            }
          }

          if (currentState == ReadyState.OPEN) {
            try {
              handler.onClosed();
            } catch (Exception e) {
              handler.onError(e);
            }
          }
          // Reset the backoff if we had a successful connection that stayed good for at least
          // backoffResetThresholdMs milliseconds.
          if (connectedTime >= 0 && (System.currentTimeMillis() - connectedTime) >= backoffResetThresholdMs) {
            reconnectAttempts = 0;
          }
          maybeWaitWithBackoff(++reconnectAttempts);
        }
      }
    } catch (RejectedExecutionException ignored) {
      call = null;
      response = null;
      bufferedSource = null;
      logger.debug("Rejected execution exception ignored: ", ignored);
      // During shutdown, we tried to send a message to the event handler
      // Do not reconnect; the executor has been shut down
    }
  }

  private ConnectionErrorHandler.Action dispatchError(Throwable t) {
    ConnectionErrorHandler.Action action = connectionErrorHandler.onConnectionError(t);
    if (action != ConnectionErrorHandler.Action.SHUTDOWN) {
      handler.onError(t);
    }
    return action;
  }
  
  private void maybeWaitWithBackoff(int reconnectAttempts) {
    if (reconnectTimeMs > 0 && reconnectAttempts > 0) {
      try {
        long sleepTimeMs = backoffWithJitter(reconnectAttempts);
        logger.info("Waiting " + sleepTimeMs + " milliseconds before reconnecting...");
        Thread.sleep(sleepTimeMs);
      } catch (InterruptedException ignored) {
      }
    }
  }

  long backoffWithJitter(int reconnectAttempts) {
    long jitterVal = Math.min(maxReconnectTimeMs, reconnectTimeMs * pow2(reconnectAttempts));
    return jitterVal / 2 + nextLong(jitter, jitterVal) / 2;
  }

  // Returns 2**k, or Integer.MAX_VALUE if 2**k would overflow
  private int pow2(int k) {
    return (k < Integer.SIZE - 1) ? (1 << k) : Integer.MAX_VALUE;
  }

  // Adapted from http://stackoverflow.com/questions/2546078/java-random-long-number-in-0-x-n-range
  // Since ThreadLocalRandom.current().nextLong(n) requires Android 5
  private long nextLong(Random rand, long bound) {
    if (bound <= 0) {
      throw new IllegalArgumentException("bound must be positive");
    }

    long r = rand.nextLong() & Long.MAX_VALUE;
    long m = bound - 1L;
    if ((bound & m) == 0) { // i.e., bound is a power of 2
      r = (bound * r) >> (Long.SIZE - 1);
    } else {
      for (long u = r; u - (r = u % bound) + m < 0L; u = rand.nextLong() & Long.MAX_VALUE) ;
    }
    return r;
  }

  private static Headers addDefaultHeaders(Headers custom) {
    Headers.Builder builder = new Headers.Builder();

    builder.add("Accept", "text/event-stream").add("Cache-Control", "no-cache");

    for (Map.Entry> header : custom.toMultimap().entrySet()) {
      for (String value : header.getValue()) {
        builder.add(header.getKey(), value);
      }
    }

    return builder.build();
  }

  /**
   * Sets the minimum delay between connection attempts. The actual delay may be slightly less or
   * greater, since there is a random jitter. When there is a connection failure, the delay will
   * start at this value and will increase exponentially up to the {@link #setMaxReconnectTimeMs(long)}
   * value with each subsequent failure, unless it is reset as described in
   * {@link Builder#backoffResetThresholdMs(long)}.
   * @param reconnectionTimeMs the minimum delay in milliseconds
   * @see #setMaxReconnectTimeMs(long)
   * @see Builder#reconnectTimeMs(long)
   * @see #DEFAULT_RECONNECT_TIME_MS
   */
  public void setReconnectionTimeMs(long reconnectionTimeMs) {
    this.reconnectTimeMs = reconnectionTimeMs;
  }

  /**
   * Sets the maximum delay between connection attempts. See {@link #setReconnectionTimeMs(long)}.
   * The default value is 30000 (30 seconds).
   * @param maxReconnectTimeMs the maximum delay in milliseconds
   * @see #setReconnectionTimeMs(long)
   * @see Builder#maxReconnectTimeMs(long)
   * @see #DEFAULT_MAX_RECONNECT_TIME_MS
   */
  public void setMaxReconnectTimeMs(long maxReconnectTimeMs) {
    this.maxReconnectTimeMs = maxReconnectTimeMs;
  }

  /**
   * Returns the current maximum reconnect delay as set by {@link #setReconnectionTimeMs(long)}.
   * @return the maximum delay in milliseconds
   */
  public long getMaxReconnectTimeMs() {
    return this.maxReconnectTimeMs;
  }

  /**
   * Sets the ID value of the last event received. This will be sent to the remote server on the
   * next connection attempt.
   * @param lastEventId the last event identifier
   */
  public void setLastEventId(String lastEventId) {
    this.lastEventId = lastEventId;
  }

  /**
   * Returns the current stream endpoint as an OkHttp HttpUrl.
   * @return the endpoint URL
   * @since 1.9.0
   * @see #getUri()
   * @see #setHttpUrl(HttpUrl)
   */
  public HttpUrl getHttpUrl() {
    return this.url;
  }
  
  /**
   * Returns the current stream endpoint as a java.net.URI.
   * @return the endpoint URI
   * @see #getHttpUrl()
   * @see #setUri(URI)
   */
  public URI getUri() {
    return this.url.uri();
  }

  /**
   * Changes the stream endpoint. This change will not take effect until the next time the
   * EventSource attempts to make a connection.
   * 
   * @param url the new endpoint, as an OkHttp HttpUrl
   * @throws IllegalArgumentException if the parameter is null or if the scheme is not HTTP or HTTPS
   * @see #getHttpUrl()
   * @see #setUri(URI)
   * 
   * @since 1.9.0
   */
  public void setHttpUrl(HttpUrl url) {
    if (url == null) {
      throw badUrlException();
    }
    this.url = url;
  }
  
  /**
   * Changes the stream endpoint. This change will not take effect until the next time the
   * EventSource attempts to make a connection.
   * 
   * @param uri the new endpoint, as a java.net.URI
   * @throws IllegalArgumentException if the parameter is null or if the scheme is not HTTP or HTTPS
   * @see #getUri()
   * @see #setHttpUrl(HttpUrl)
   */
  public void setUri(URI uri) {
    setHttpUrl(uri == null ? null : HttpUrl.get(uri));
  }

  private static IllegalArgumentException badUrlException() {
    return new IllegalArgumentException("URI/URL must not be null and must be HTTP or HTTPS");
  }
  
  /**
   * Interface for an object that can modify the network request that the EventSource will make.
   * Use this in conjunction with {@link Builder#requestTransformer(RequestTransformer)} if you need to set request
   * properties other than the ones that are already supported by the builder (or if, for
   * whatever reason, you need to determine the request properties dynamically rather than
   * setting them to fixed values initially). For example:
   * 
   * public class RequestTagger implements EventSource.RequestTransformer {
   *   public Request transformRequest(Request input) {
   *     return input.newBuilder().tag("hello").build();
   *   }
   * }
   * 
   * EventSource es = new EventSource.Builder(handler, uri).requestTransformer(new RequestTagger()).build();
   * 
   * 
   * @since 1.9.0
   */
  public static interface RequestTransformer {
    /**
     * Returns a request that is either the same as the input request or based on it. When
     * this method is called, EventSource has already set all of its standard properties on
     * the request.
     * 
     * @param input the original request
     * @return the request that will be used
     */
    public Request transformRequest(Request input);
  }
  
  /**
   * Builder for {@link EventSource}.
   */
  public static final class Builder {
    private String name = "";
    private long reconnectTimeMs = DEFAULT_RECONNECT_TIME_MS;
    private long maxReconnectTimeMs = DEFAULT_MAX_RECONNECT_TIME_MS;
    private long backoffResetThresholdMs = DEFAULT_BACKOFF_RESET_THRESHOLD_MS;
    private final HttpUrl url;
    private final EventHandler handler;
    private ConnectionErrorHandler connectionErrorHandler = ConnectionErrorHandler.DEFAULT;
    private Headers headers = Headers.of();
    private Proxy proxy;
    private Authenticator proxyAuthenticator = null;
    private String method = "GET";
    private RequestTransformer requestTransformer = null;
    @Nullable private RequestBody body = null;
    private OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
            .connectionPool(new ConnectionPool(1, 1, TimeUnit.SECONDS))
            .connectTimeout(DEFAULT_CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
            .readTimeout(DEFAULT_READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)
            .writeTimeout(DEFAULT_WRITE_TIMEOUT_MS, TimeUnit.MILLISECONDS)
            .retryOnConnectionFailure(true);

    /**
     * Creates a new builder.
     * 
     * @param handler the event handler
     * @param uri the endpoint as a java.net.URI
     * @throws IllegalArgumentException if either argument is null, or if the endpoint is not HTTP or HTTPS
     */
    public Builder(EventHandler handler, URI uri) {
      this(handler, uri == null ? null : HttpUrl.get(uri));
    }

    /**
     * Creates a new builder.
     * 
     * @param handler the event handler
     * @param url the endpoint as an OkHttp HttpUrl
     * @throws IllegalArgumentException if either argument is null, or if the endpoint is not HTTP or HTTPS
     * 
     * @since 1.9.0
     */
    public Builder(EventHandler handler, HttpUrl url) {
      if (handler == null) {
        throw new IllegalArgumentException("handler must not be null");
      }
      if (url == null) {
        throw badUrlException();
      }
      this.url = url;
      this.handler = handler;
    }
    
    /**
     * Set the HTTP method used for this EventSource client to use for requests to establish the EventSource.
     *
     * Defaults to "GET".
     *
     * @param method the HTTP method name
     * @return the builder
     */
    public Builder method(String method) {
      if (method != null && method.length() > 0) {
        this.method = method.toUpperCase();
      }
      return this;
    }

    /**
     * Sets the request body to be used for this EventSource client to use for requests to establish the EventSource.
     * @param body the body to use in HTTP requests
     * @return the builder
     */
    public Builder body(@Nullable RequestBody body) {
      this.body = body;
      return this;
    }

    /**
     * Specifies an object that will be used to customize outgoing requests. See {@link RequestTransformer} for details.
     * 
     * @param requestTransformer the transformer object
     * @return the builder
     * 
     * @since 1.9.0
     */
    public Builder requestTransformer(@Nullable RequestTransformer requestTransformer) {
      this.requestTransformer = requestTransformer;
      return this;
    }
    
    /**
     * Set the name for this EventSource client to be used when naming the logger and threadpools. This is mainly useful when
     * multiple EventSource clients exist within the same process.
     *
     * @param name the name (without any whitespaces)
     * @return the builder
     */
    public Builder name(String name) {
      if (name != null) {
        this.name = name;
      }
      return this;
    }

    /**
     * Sets the minimum delay between connection attempts. The actual delay may be slightly less or
     * greater, since there is a random jitter. When there is a connection failure, the delay will
     * start at this value and will increase exponentially up to the {@link #setMaxReconnectTimeMs(long)}
     * value with each subsequent failure, unless it is reset as described in
     * {@link Builder#backoffResetThresholdMs(long)}.
     * @param reconnectTimeMs the minimum delay in milliseconds
     * @return the builder
     * @see EventSource#DEFAULT_RECONNECT_TIME_MS
     * @see EventSource#setReconnectionTimeMs(long)
     */
    public Builder reconnectTimeMs(long reconnectTimeMs) {
      this.reconnectTimeMs = reconnectTimeMs;
      return this;
    }

    /**
     * Sets the maximum delay between connection attempts. See {@link #setReconnectionTimeMs(long)}.
     * The default value is 30000 (30 seconds).
     * @param maxReconnectTimeMs the maximum delay in milliseconds
     * @return the builder
     * @see EventSource#DEFAULT_MAX_RECONNECT_TIME_MS
     * @see EventSource#setMaxReconnectTimeMs(long)
     */
    public Builder maxReconnectTimeMs(long maxReconnectTimeMs) {
      this.maxReconnectTimeMs = maxReconnectTimeMs;
      return this;
    }

    /**
     * Sets the minimum amount of time that a connection must stay open before the EventSource resets its
     * backoff delay. If a connection fails before the threshold has elapsed, the delay before reconnecting
     * will be greater than the last delay; if it fails after the threshold, the delay will start over at
     * the initial minimum value. This prevents long delays from occurring on connections that are only
     * rarely restarted.
     *   
     * @param backoffResetThresholdMs the minimum time in milliseconds that a connection must stay open to
     *   avoid resetting the delay 
     * @return the builder
     * @see EventSource#DEFAULT_BACKOFF_RESET_THRESHOLD_MS
     * 
     * @since 1.9.0
     */
    public Builder backoffResetThresholdMs(long backoffResetThresholdMs) {
      this.backoffResetThresholdMs = backoffResetThresholdMs;
      return this;
    }

    /**
     * Set the headers to be sent when establishing the EventSource connection.
     *
     * @param headers headers to be sent with the EventSource request
     * @return the builder
     */
    public Builder headers(Headers headers) {
      this.headers = headers;
      return this;
    }

    /**
     * Set a custom HTTP client that will be used to make the EventSource connection.
     * If you're setting this along with other connection-related items (ie timeouts, proxy),
     * you should do this first to avoid overwriting values.
     *
     * @param client the HTTP client
     * @return the builder
     */
    public Builder client(OkHttpClient client) {
      this.clientBuilder = client.newBuilder();
      return this;
    }

    /**
     * Set the HTTP proxy address to be used to make the EventSource connection
     *
     * @param proxyHost the proxy hostname
     * @param proxyPort the proxy port
     * @return the builder
     */
    public Builder proxy(String proxyHost, int proxyPort) {
      proxy = new Proxy(Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
      return this;
    }

    /**
     * Set the {@link Proxy} to be used to make the EventSource connection.
     *
     * @param proxy the proxy
     * @return the builder
     */
    public Builder proxy(Proxy proxy) {
      this.proxy = proxy;
      return this;
    }

    /**
     * Sets the Proxy Authentication mechanism if needed. Defaults to no auth.
     *
     * @param proxyAuthenticator the authentication mechanism
     * @return the builder
     */
    public Builder proxyAuthenticator(Authenticator proxyAuthenticator) {
      this.proxyAuthenticator = proxyAuthenticator;
      return this;
    }

    /**
     * Sets the connection timeout.
     *
     * @param connectTimeoutMs the connection timeout in milliseconds
     * @return the builder
     * @see EventSource#DEFAULT_CONNECT_TIMEOUT_MS
     */
    public Builder connectTimeoutMs(int connectTimeoutMs) {
      this.clientBuilder.connectTimeout(connectTimeoutMs, TimeUnit.MILLISECONDS);
      return this;
    }

    /**
     * Sets the write timeout in milliseconds.
     *
     * @param writeTimeoutMs the write timeout in milliseconds
     * @return the builder
     * @see EventSource#DEFAULT_WRITE_TIMEOUT_MS
     */
    public Builder writeTimeoutMs(int writeTimeoutMs) {
      this.clientBuilder.writeTimeout(writeTimeoutMs, TimeUnit.MILLISECONDS);
      return this;
    }

    /**
     * Sets the read timeout in milliseconds. If a read timeout happens, the {@code EventSource}
     * will restart the connection.
     *
     * @param readTimeoutMs the read timeout in milliseconds
     * @return the builder
     * @see EventSource#DEFAULT_WRITE_TIMEOUT_MS
     */
    public Builder readTimeoutMs(int readTimeoutMs) {
      this.clientBuilder.readTimeout(readTimeoutMs, TimeUnit.MILLISECONDS);
      return this;
    }

    /**
     * Sets the {@link ConnectionErrorHandler} that should process connection errors.
     *
     * @param handler the error handler
     * @return the builder
     */
    public Builder connectionErrorHandler(ConnectionErrorHandler handler) {
      if (handler != null) {
        this.connectionErrorHandler = handler;
      }
      return this;
    }

    /**
     * Constructs an {@link EventSource} using the builder's current properties.
     * @return the new EventSource instance
     */
    public EventSource build() {
      if (proxy != null) {
        clientBuilder.proxy(proxy);
      }

      try {
        clientBuilder.sslSocketFactory(new ModernTLSSocketFactory(), defaultTrustManager());
      } catch (GeneralSecurityException e) {
        // TLS is not available, so don't set up the socket factory, swallow the exception
      }

      if (proxyAuthenticator != null) {
        clientBuilder.proxyAuthenticator(proxyAuthenticator);
      }

      return new EventSource(this);
    }

    protected OkHttpClient.Builder getClientBuilder() {
      return clientBuilder;
    }

    private static X509TrustManager defaultTrustManager() throws GeneralSecurityException {
      TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
              TrustManagerFactory.getDefaultAlgorithm());
      trustManagerFactory.init((KeyStore) null);
      TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
      if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
        throw new IllegalStateException("Unexpected default trust managers:"
                + Arrays.toString(trustManagers));
      }
      return (X509TrustManager) trustManagers[0];
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy