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 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.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

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

import static com.launchdarkly.eventsource.Helpers.pow2;
import static com.launchdarkly.eventsource.ReadyState.CLOSED;
import static com.launchdarkly.eventsource.ReadyState.CONNECTING;
import static com.launchdarkly.eventsource.ReadyState.OPEN;
import static com.launchdarkly.eventsource.ReadyState.RAW;
import static com.launchdarkly.eventsource.ReadyState.SHUTDOWN;
import static java.lang.String.format;

import okhttp3.Authenticator;
import okhttp3.Call;
import okhttp3.ConnectionPool;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * A client for the Server-Sent
 * Events (SSE) protocol.
 * 

* Instances are always configured and constructed with {@link Builder}. The client is created in * an inactive state and will not connect until you call {@link #start()}. *

* Note that although {@code EventSource} is named after the JavaScript API that is described in * the SSE specification, its behavior is not identical to standard web browser implementations of * EventSource, specifically in terms of failure/reconnection behavior: it will automatically * retry (with a backoff delay) for some error conditions where a browser will not retry. It also * supports request configuration options (such as request headers and method) that the browser * EventSource does not support. However, its interpretation of the stream data is fully conformant * with the SSE specification, unless you use the opt-in mode {@link Builder#streamEventData(boolean)} * which allows for greater efficiency in some use cases but has some behavioral constraints. */ public class EventSource implements Closeable { final Logger logger; // visible for tests /** * The default value for {@link Builder#reconnectTime(Duration)}: 1 second. */ public static final Duration DEFAULT_RECONNECT_TIME = Duration.ofSeconds(1); /** * The default value for {@link Builder#maxReconnectTime(Duration)}: 30 seconds. */ public static final Duration DEFAULT_MAX_RECONNECT_TIME = Duration.ofSeconds(30); /** * The default value for {@link Builder#connectTimeout(Duration)}: 10 seconds. */ public static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10); /** * The default value for {@link Builder#writeTimeout(Duration)}: 5 seconds. */ public static final Duration DEFAULT_WRITE_TIMEOUT = Duration.ofSeconds(5); /** * The default value for {@link Builder#readTimeout(Duration)}: 5 minutes. */ public static final Duration DEFAULT_READ_TIMEOUT = Duration.ofMinutes(5); /** * The default value for {@link Builder#backoffResetThreshold(Duration)}: 60 seconds. */ public static final Duration DEFAULT_BACKOFF_RESET_THRESHOLD = Duration.ofSeconds(60); /** * The default value for {@link Builder#readBufferSize(int)}. */ public static final int DEFAULT_READ_BUFFER_SIZE = 1000; private static final Headers defaultHeaders = new Headers.Builder().add("Accept", "text/event-stream").add("Cache-Control", "no-cache").build(); private final String name; private volatile HttpUrl url; private final Headers headers; private final String method; private final RequestBody body; private final RequestTransformer requestTransformer; private final ExecutorService eventExecutor; private final ExecutorService streamExecutor; final int readBufferSize; // visible for tests volatile Duration reconnectTime; // visible for tests final Duration maxReconnectTime; // visible for tests final Duration backoffResetThreshold; // visible for tests private volatile String lastEventId; final AsyncEventHandler handler; // visible for tests private final ConnectionErrorHandler connectionErrorHandler; final boolean streamEventData; // visible for tests final Set expectFields; // visible for tests private final AtomicReference readyState; private final OkHttpClient client; private volatile Call call; private final Random jitter = new Random(); EventSource(Builder builder) { this.name = builder.name == null ? "" : builder.name; if (builder.logger == null) { String loggerName = (builder.loggerBaseName == null ? EventSource.class.getCanonicalName() : builder.loggerBaseName) + (name.isEmpty() ? "" : ("." + name)); this.logger = new SLF4JLogger(loggerName); } else { this.logger = builder.logger; } this.url = builder.url; this.headers = addDefaultHeaders(builder.headers); this.method = builder.method; this.body = builder.body; this.requestTransformer = builder.requestTransformer; this.lastEventId = builder.lastEventId; this.reconnectTime = builder.reconnectTime; this.maxReconnectTime = builder.maxReconnectTime; this.backoffResetThreshold = builder.backoffResetThreshold; this.streamEventData = builder.streamEventData; this.expectFields = builder.expectFields; ThreadFactory eventsThreadFactory = createThreadFactory("okhttp-eventsource-events", builder.threadPriority); this.eventExecutor = Executors.newSingleThreadExecutor(eventsThreadFactory); ThreadFactory streamThreadFactory = createThreadFactory("okhttp-eventsource-stream", builder.threadPriority); this.streamExecutor = Executors.newSingleThreadExecutor(streamThreadFactory); Semaphore eventThreadSemaphore; if (builder.maxEventTasksInFlight > 0) { eventThreadSemaphore = new Semaphore(builder.maxEventTasksInFlight); } else { eventThreadSemaphore = null; } this.handler = new AsyncEventHandler(this.eventExecutor, builder.handler, logger, eventThreadSemaphore); this.connectionErrorHandler = builder.connectionErrorHandler == null ? ConnectionErrorHandler.DEFAULT : builder.connectionErrorHandler; this.readBufferSize = builder.readBufferSize; this.readyState = new AtomicReference<>(RAW); this.client = builder.clientBuilder.build(); } private ThreadFactory createThreadFactory(final String type, final Integer threadPriority) { final ThreadFactory backingThreadFactory = Executors.defaultThreadFactory(); final AtomicLong count = new AtomicLong(0); return runnable -> { Thread thread = backingThreadFactory.newThread(runnable); thread.setName(format(Locale.ROOT, "%s-[%s]-%d", type, name, count.getAndIncrement())); thread.setDaemon(true); if (threadPriority != null) { thread.setPriority(threadPriority); } 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(this::run); } /** * Drops the current stream connection (if any) and attempts to reconnect. *

* This method returns immediately after dropping the current connection; the reconnection happens on * a worker thread. *

* If a connection attempt is already in progress but has not yet connected, or if {@link #close()} has * previously been called, this method has no effect. If {@link #start()} has never been called, it is * the same as calling {@link #start()}. */ public void restart() { ReadyState previousState = readyState.getAndUpdate(t -> t == ReadyState.OPEN ? ReadyState.CLOSED : t); if (previousState == OPEN) { closeCurrentStream(previousState); } else if (previousState == RAW) { start(); } // if already connecting or already shutdown or in the process of closing, do nothing } /** * Returns an enum indicating the current status of the connection. * @return a {@link ReadyState} value */ public ReadyState getState() { return readyState.get(); } /** * Drops the current stream connection (if any) and permanently shuts down the EventSource. */ @Override public void close() { ReadyState currentState = readyState.getAndSet(SHUTDOWN); logger.debug("readyState change: {} -> {}", currentState, SHUTDOWN); if (currentState == SHUTDOWN) { return; } closeCurrentStream(currentState); eventExecutor.shutdown(); streamExecutor.shutdown(); // COVERAGE: these null guards are here for safety but in practice the values are never null and there // is no way to cause them to be null in unit tests if (client.connectionPool() != null) { client.connectionPool().evictAll(); } if (client.dispatcher() != null) { client.dispatcher().cancelAll(); if (client.dispatcher().executorService() != null) { client.dispatcher().executorService().shutdownNow(); } } } /** * Block until all underlying threads have terminated and resources have been released. * * @param timeout maximum time to wait for everything to shut down * @return {@code true} if all thread pools terminated within the specified timeout, {@code false} otherwise. * @throws InterruptedException if this thread is interrupted while blocking */ public boolean awaitClosed(final Duration timeout) throws InterruptedException { final long deadline = System.currentTimeMillis() + timeout.toMillis(); if (!eventExecutor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS)) { return false; } long shutdownTimeoutMills = Math.max(0, deadline - System.currentTimeMillis()); if (!streamExecutor.awaitTermination(shutdownTimeoutMills, TimeUnit.MILLISECONDS)) { return false; // COVERAGE: this condition can't be reproduced in unit tests } if (client.dispatcher().executorService() != null) { shutdownTimeoutMills = Math.max(0, deadline - System.currentTimeMillis()); if (!client.dispatcher().executorService().awaitTermination(shutdownTimeoutMills, TimeUnit.MILLISECONDS)) { return false; // COVERAGE: this condition can't be reproduced in unit tests } } return true; } private void closeCurrentStream(ReadyState previousState) { if (previousState == ReadyState.OPEN) { handler.onClosed(); } 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", null); } } 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 run() { AtomicLong connectedTime = new AtomicLong(); int reconnectAttempts = 0; try { while (!Thread.currentThread().isInterrupted() && readyState.get() != SHUTDOWN) { if (reconnectAttempts == 0) { reconnectAttempts++; } else { reconnectAttempts = maybeReconnectDelay(reconnectAttempts, connectedTime.get()); } newConnectionAttempt(connectedTime); } } catch (RejectedExecutionException ignored) { // COVERAGE: there is no way to simulate this condition in unit tests call = 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 int maybeReconnectDelay(int reconnectAttempts, long connectedTime) { if (reconnectTime.isZero() || reconnectTime.isNegative()) { return reconnectAttempts; } int counter = reconnectAttempts; // Reset the backoff if we had a successful connection that stayed good for at least // backoffResetThresholdMs milliseconds. if (connectedTime > 0 && (System.currentTimeMillis() - connectedTime) >= backoffResetThreshold.toMillis()) { counter = 1; } try { Duration sleepTime = backoffWithJitter(counter); logger.info("Waiting " + sleepTime.toMillis() + " milliseconds before reconnecting..."); Thread.sleep(sleepTime.toMillis()); } catch (InterruptedException ignored) { // COVERAGE: no way to cause this in unit tests } return ++counter; } private void newConnectionAttempt(AtomicLong connectedTime) { ConnectionErrorHandler.Action errorHandlerAction = ConnectionErrorHandler.Action.PROCEED; ReadyState stateBeforeConnecting = readyState.getAndSet(CONNECTING); logger.debug("readyState change: {} -> {}", stateBeforeConnecting, CONNECTING); connectedTime.set(0); call = client.newCall(buildRequest()); try { try (Response response = call.execute()) { if (response.isSuccessful()) { connectedTime.set(System.currentTimeMillis()); handleSuccessfulResponse(response); // If handleSuccessfulResponse returned without throwing an exception, it means the server // ended the stream. We don't call the handler's onError() method in this case; but we will // call the ConnectionErrorHandler with an EOFException, in case it wants to do something // special in this scenario (like choose not to retry the connection). However, first we // should check the state in case we've been deliberately closed from elsewhere. ReadyState state = readyState.get(); if (state != SHUTDOWN && state != CLOSED) { logger.warn("Connection unexpectedly closed"); errorHandlerAction = connectionErrorHandler.onConnectionError(new EOFException()); } } else { logger.debug("Unsuccessful response: {}", response); errorHandlerAction = dispatchError(new UnsuccessfulResponseException(response.code())); } } } catch (IOException e) { ReadyState state = readyState.get(); if (state != SHUTDOWN && state != CLOSED) { logger.debug("Connection problem: {}", e); errorHandlerAction = dispatchError(e); } } finally { if (errorHandlerAction == ConnectionErrorHandler.Action.SHUTDOWN) { logger.info("Connection has been explicitly shut down by error handler"); close(); } else { boolean wasOpen = readyState.compareAndSet(OPEN, CLOSED); boolean wasConnecting = readyState.compareAndSet(CONNECTING, CLOSED); if (wasOpen) { logger.debug("readyState change: {} -> {}", OPEN, CLOSED); handler.onClosed(); } else if (wasConnecting) { logger.debug("readyState change: {} -> {}", CONNECTING, CLOSED); } } } } // Read the response body as an SSE stream and dispatch each received event to the EventHandler. // This function exits in one of two ways: // 1. A normal return - this means the response simply ended. // 2. Throwing an IOException - there was an unexpected connection failure. private void handleSuccessfulResponse(Response response) throws IOException { ConnectionHandler connectionHandler = new ConnectionHandler() { @Override public void setReconnectionTime(Duration reconnectionTime) { EventSource.this.setReconnectionTime(reconnectionTime); } @Override public void setLastEventId(String lastEventId) { EventSource.this.setLastEventId(lastEventId); } }; ReadyState previousState = readyState.getAndSet(OPEN); if (previousState != CONNECTING) { // COVERAGE: there is no way to simulate this condition in unit tests logger.warn("Unexpected readyState change: " + previousState + " -> " + OPEN); } else { logger.debug("readyState change: {} -> {}", previousState, OPEN); } logger.info("Connected to EventSource stream."); handler.onOpen(); EventParser parser = new EventParser( response.body().byteStream(), url.uri(), handler, connectionHandler, readBufferSize, streamEventData, expectFields, logger ); // COVERAGE: the isInterrupted() condition is not encountered in unit tests and it's unclear if it can ever happen while (!Thread.currentThread().isInterrupted() && !parser.isEof()) { parser.processStream(); } } private ConnectionErrorHandler.Action dispatchError(Throwable t) { ConnectionErrorHandler.Action action = connectionErrorHandler.onConnectionError(t); if (action != ConnectionErrorHandler.Action.SHUTDOWN) { handler.onError(t); } return action; } Duration backoffWithJitter(int reconnectAttempts) { long maxTimeLong = Math.min(maxReconnectTime.toMillis(), reconnectTime.toMillis() * pow2(reconnectAttempts)); // 2^31 milliseconds is much longer than any reconnect time we would reasonably want to use, so we can pin this to int int maxTimeInt = maxTimeLong > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)maxTimeLong; return Duration.ofMillis(maxTimeInt / 2 + jitter.nextInt(maxTimeInt) / 2); } private static Headers addDefaultHeaders(Headers custom) { Headers.Builder builder = new Headers.Builder(); for (String name : defaultHeaders.names()) { if (!custom.names().contains(name)) { // skip the default if they set any custom values for this key for (String value: defaultHeaders.values(name)) { builder.add(name, value); } } } for (String name : custom.names()) { for (String value : custom.values(name)) { builder.add(name, value); } } return builder.build(); } // setReconnectionTime and setLastEventId are used only by our internal ConnectionHandler, in response // to stream events. From an application's point of view, these properties can only be set at // configuration time via the builder. private void setReconnectionTime(Duration reconnectionTime) { this.reconnectTime = reconnectionTime; } private void setLastEventId(String lastEventId) { this.lastEventId = lastEventId; } /** * Returns the ID value, if any, of the last known event. *

* This can be set initially with {@link Builder#lastEventId(String)}, and is updated whenever an event * is received that has an ID. Whether event IDs are supported depends on the server; it may ignore this * value. * * @return the last known event ID, or null * @see Builder#lastEventId(String) * @since 2.0.0 */ public String getLastEventId() { return lastEventId; } /** * Returns the current stream endpoint as an OkHttp HttpUrl. * * @return the endpoint URL * @since 1.9.0 * @see #getUri() */ public HttpUrl getHttpUrl() { return this.url; } /** * Returns the current stream endpoint as a java.net.URI. * * @return the endpoint URI * @see #getHttpUrl() */ public URI getUri() { return this.url.uri(); } /** * Interface for an object that can modify the network request that the EventSource will make. * Use this in conjunction with {@link EventSource.Builder#requestTransformer(EventSource.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 Duration reconnectTime = DEFAULT_RECONNECT_TIME; private Duration maxReconnectTime = DEFAULT_MAX_RECONNECT_TIME; private Duration backoffResetThreshold = DEFAULT_BACKOFF_RESET_THRESHOLD; private String lastEventId; private final HttpUrl url; private final EventHandler handler; private ConnectionErrorHandler connectionErrorHandler = ConnectionErrorHandler.DEFAULT; private Integer threadPriority = null; private Headers headers = Headers.of(); private Proxy proxy; private Authenticator proxyAuthenticator = null; private String method = "GET"; private RequestTransformer requestTransformer = null; private RequestBody body = null; private OkHttpClient.Builder clientBuilder; private int readBufferSize = DEFAULT_READ_BUFFER_SIZE; private Logger logger = null; private String loggerBaseName = null; private int maxEventTasksInFlight = 0; private boolean streamEventData; private Set expectFields = null; /** * 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 new IllegalArgumentException("URI/URL must not be null"); } this.url = url; this.handler = handler; this.clientBuilder = createInitialClientBuilder(); } private static OkHttpClient.Builder createInitialClientBuilder() { OkHttpClient.Builder b = new OkHttpClient.Builder() .connectionPool(new ConnectionPool(1, 1, TimeUnit.SECONDS)) .connectTimeout(DEFAULT_CONNECT_TIMEOUT) .readTimeout(DEFAULT_READ_TIMEOUT) .writeTimeout(DEFAULT_WRITE_TIMEOUT) .retryOnConnectionFailure(true); try { b.sslSocketFactory(new ModernTLSSocketFactory(), defaultTrustManager()); } catch (GeneralSecurityException e) { // TLS is not available, so don't set up the socket factory, swallow the exception // COVERAGE: There is no way to cause this to happen in unit tests } return b; } /** * 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; if null or empty, "GET" is used as the default * @return the builder */ public Builder method(String method) { this.method = (method != null && method.length() > 0) ? method.toUpperCase() : "GET"; 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(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(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. *

* The name only affects logging when using the default SLF4J integration; if you have specified a custom * {@link #logger(Logger)}, the name will not be included in log messages unless your logger implementation adds it. * * @param name the name (without any whitespaces) * @return the builder */ public Builder name(String name) { this.name = name; return this; } /** * Sets the ID value of the last event received. *

* This will be sent to the remote server on the initial connection request, allowing the server to * skip past previously sent events if it supports this behavior. Once the connection is established, * this value will be updated whenever an event is received that has an ID. Whether event IDs are * supported depends on the server; it may ignore this value. * * @param lastEventId the last event identifier * @return the builder * @since 2.0.0 */ public Builder lastEventId(String lastEventId) { this.lastEventId = lastEventId; 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 #maxReconnectTime(Duration)} * value with each subsequent failure, unless it is reset as described in * {@link Builder#backoffResetThreshold(Duration)}. * * @param reconnectTime the minimum delay; null to use the default * @return the builder * @see EventSource#DEFAULT_RECONNECT_TIME */ public Builder reconnectTime(Duration reconnectTime) { this.reconnectTime = reconnectTime == null ? DEFAULT_RECONNECT_TIME : reconnectTime; return this; } /** * Sets the maximum delay between connection attempts. See {@link #reconnectTime(Duration)}. * The default value is 30 seconds. * * @param maxReconnectTime the maximum delay; null to use the default * @return the builder * @see EventSource#DEFAULT_MAX_RECONNECT_TIME */ public Builder maxReconnectTime(Duration maxReconnectTime) { this.maxReconnectTime = maxReconnectTime == null ? DEFAULT_MAX_RECONNECT_TIME : maxReconnectTime; 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 backoffResetThreshold the minimum time that a connection must stay open to avoid resetting * the delay; null to use the default * @return the builder * @see EventSource#DEFAULT_BACKOFF_RESET_THRESHOLD */ public Builder backoffResetThreshold(Duration backoffResetThreshold) { this.backoffResetThreshold = backoffResetThreshold == null ? DEFAULT_BACKOFF_RESET_THRESHOLD : backoffResetThreshold; 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 connectTimeout the connection timeout; null to use the default * @return the builder * @see EventSource#DEFAULT_CONNECT_TIMEOUT */ public Builder connectTimeout(Duration connectTimeout) { this.clientBuilder.connectTimeout(connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : connectTimeout); return this; } /** * Sets the write timeout. * * @param writeTimeout the write timeout; null to use the default * @return the builder * @see EventSource#DEFAULT_WRITE_TIMEOUT */ public Builder writeTimeout(Duration writeTimeout) { this.clientBuilder.writeTimeout(writeTimeout == null ? DEFAULT_WRITE_TIMEOUT : writeTimeout); return this; } /** * Sets the read timeout. If a read timeout happens, the {@code EventSource} * will restart the connection. * * @param readTimeout the read timeout; null to use the default * @return the builder * @see EventSource#DEFAULT_READ_TIMEOUT */ public Builder readTimeout(Duration readTimeout) { this.clientBuilder.readTimeout(readTimeout == null ? DEFAULT_READ_TIMEOUT : readTimeout); return this; } /** * Sets the {@link ConnectionErrorHandler} that should process connection errors. * * @param handler the error handler * @return the builder */ public Builder connectionErrorHandler(ConnectionErrorHandler handler) { this.connectionErrorHandler = handler; return this; } /** * Specifies the priority for threads created by {@code EventSource}. *

* If this is left unset, or set to {@code null}, threads will inherit the default priority * provided by {@code Executors.defaultThreadFactory()}. * * @param threadPriority the thread priority, or null to ue the default * @return the builder * @since 2.2.0 */ public Builder threadPriority(Integer threadPriority) { this.threadPriority = threadPriority; return this; } /** * Specifies any type of configuration actions you want to perform on the OkHttpClient builder. *

* {@link ClientConfigurer} is an interface with a single method, {@link ClientConfigurer#configure(okhttp3.OkHttpClient.Builder)}, * that will be called with the {@link okhttp3.OkHttpClient.Builder} instance being used by EventSource. * In Java 8, this can be a lambda. *

* It is not guaranteed to be called in any particular order relative to other configuration * actions specified by this Builder, so if you are using more than one method, do not attempt * to overwrite the same setting in two ways. *


     *     // Java 8 example (lambda)
     *     eventSourceBuilder.clientBuilderActions(b -> {
     *         b.sslSocketFactory(mySocketFactory, myTrustManager);
     *     });
     * 
     *     // Java 7 example (anonymous class)
     *     eventSourceBuilder.clientBuilderActions(new EventSource.Builder.ClientConfigurer() {
     *         public void configure(OkHttpClient.Builder v) {
     *             b.sslSocketFactory(mySocketFactory, myTrustManager);
     *         }
     *     });
     * 
* @param configurer a ClientConfigurer (or lambda) that will act on the HTTP client builder * @return the builder * @since 1.10.0 */ public Builder clientBuilderActions(ClientConfigurer configurer) { configurer.configure(clientBuilder); return this; } /** * Specifies the fixed size of the buffer that EventSource uses to parse incoming data. *

* EventSource allocates a single buffer to hold data from the stream as it scans for * line breaks. If no lines of data from the stream exceed this size, it will keep reusing * the same space; if a line is longer than this size, it creates a temporary * {@code ByteArrayOutputStream} to accumulate data for that line, which is less efficient. * Therefore, if an application expects to see many lines in the stream that are longer * than {@link EventSource#DEFAULT_READ_BUFFER_SIZE}, it can specify a larger buffer size * to avoid unnecessary heap allocations. * * @param readBufferSize the buffer size * @return the builder * @throws IllegalArgumentException if the size is less than or equal to zero * @see EventSource#DEFAULT_READ_BUFFER_SIZE * @since 2.4.0 */ public Builder readBufferSize(int readBufferSize) { if (readBufferSize <= 0) { throw new IllegalArgumentException("readBufferSize must be greater than zero"); } this.readBufferSize = readBufferSize; return this; } /** * Specifies a custom logger to receive EventSource logging. *

* If you do not provide a logger, the default is to send log output to SLF4J. * * @param logger a {@link Logger} implementation, or null to use the default (SLF4J) * @return the builder * @since 2.3.0 */ public Builder logger(Logger logger) { this.logger = logger; return this; } /** * Specifies the base logger name to use for SLF4J logging. *

* The default is {@code com.launchdarkly.eventsource.EventSource}, plus any name suffix specified * by {@link #name(String)}. If you instead use {@link #logger(Logger)} to specify some other log * destination rather than SLF4J, this name is unused. * * @param loggerBaseName the SLF4J logger name, or null to use the default * @return the builder * @since 2.3.0 */ public Builder loggerBaseName(String loggerBaseName) { this.loggerBaseName = loggerBaseName; return this; } /** * Specifies the maximum number of tasks that can be "in-flight" for the thread executing {@link EventHandler}. * A semaphore will be used to artificially constrain the number of tasks sitting in the queue fronting the * event handler thread. When this limit is reached the stream thread will block until the backpressure passes. *

* For backward compatibility reasons the default is "unbounded". * * @param maxEventTasksInFlight the maximum number of tasks/messages that can be in-flight for the {@code EventHandler} * @return the builder * @since 2.5.0 */ public Builder maxEventTasksInFlight(int maxEventTasksInFlight) { this.maxEventTasksInFlight = maxEventTasksInFlight; return this; } /** * Specifies whether EventSource should send a {@link MessageEvent} to the handler as soon as it receives the * beginning of the event data, allowing the handler to read the data incrementally with * {@link MessageEvent#getDataReader()}. *

* The default for this property is {@code false}, meaning that EventSource will always read the entire event into * memory before dispatching it to the handler. *

* If you set it to {@code true}, it will instead call the handler as soon as it sees a {@code data} field-- * setting {@link MessageEvent#getDataReader()} to a {@link java.io.Reader} that reads directly from the data as * it arrives. The EventSource will perform any necessary parsing under the covers, so that for instance if there * are multiple {@code data:} lines in the event, the {@link java.io.Reader} will emit a newline character between * each and will not see the "data:" field names. The {@link java.io.Reader} will report "end of stream" as soon * as the event is terminated normally by a blank line. *

* This mode is designed for applications that expect very large data items to be delivered over SSE. Use it * with caution, since there are several limitations: *

    *
  • EventSource cannot continue processing further events on the stream until the handler's * {@link EventHandler#onMessage(String, MessageEvent)} method has returned.
  • *
  • The {@link MessageEvent} is constructed as soon as a {@code data:} field appears, so it will only include * fields that appeared before {@code data:}. In other words, if the SSE server happens to send {@code data:} * first and {@code event:} second, {@link MessageEvent#getEventName()} will not contain the value of * {@code event:} but will be {@link MessageEvent#DEFAULT_EVENT_NAME} instead; similarly, an {@code id:} field will * be ignored if it appears after {@code data:} in this mode. Therefore, you should only use this mode if the * server's behavior is predictable in this regard.
  • *
  • The SSE protocol specifies that an event should be processed only if it is terminated by a blank line, but * in this mode the handler will receive the event as soon as a {@code data:} field appears-- so, if the stream * happens to cut off abnormally without a trailing blank line, technically you will be receiving an incomplete * event that should have been ignored.
  • *
* * @param streamEventData true if events should be dispatched immediately with asynchronous data rather than * read fully before dispatch * @return the builder * @see #expectFields(String...) * @since 2.6.0 */ public Builder streamEventData(boolean streamEventData) { this.streamEventData = streamEventData; return this; } /** * Specifies that the application expects the server to send certain fields in every event. *

* This setting makes no difference unless you have enabled {@link #streamEventData(boolean)} mode. In that case, * it causes EventSource to only use the streaming data mode for an event if the specified fields have * already been received; otherwise, it will buffer the whole event (as if {@link #streamEventData(boolean)} had * not been enabled), to ensure that those fields are not lost if they appear after the {@code data:} field. *

* For instance, if you had called {@code expectFields("event")}, then EventSource would be able to use streaming * data mode for the following SSE response-- *


     *     event: hello
     *     data: here is some very long streaming data
     * 
*

* --but it would buffer the full event if the server used the opposite order: *


     *     data: here is some very long streaming data
     *     event: hello
     * 
*

* Such behavior is not automatic because in some applications, there might never be an {@code event:} field, * and EventSource has no way to anticipate this. * * @param fieldNames a list of SSE field names (case-sensitive; any names other than "event" and "id" are ignored) * @return the builder * @see #streamEventData(boolean) * @since 2.6.0 */ public Builder expectFields(String... fieldNames) { if (fieldNames == null || fieldNames.length == 0) { expectFields = null; } else { expectFields = new HashSet<>(); for (String f: fieldNames) { if (f != null) { expectFields.add(f); } } } 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); } 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)) { // COVERAGE: There is no way to cause this to happen in unit tests throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)); } return (X509TrustManager) trustManagers[0]; } /** * An interface for use with {@link EventSource.Builder#clientBuilderActions(ClientConfigurer)}. * @since 1.10.0 */ public static interface ClientConfigurer { /** * This method is called with the OkHttp {@link okhttp3.OkHttpClient.Builder} that will be used for * the EventSource, allowing you to call any configuration methods you want. * @param builder the client builder */ public void configure(OkHttpClient.Builder builder); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy