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

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

The newest version!
package com.launchdarkly.eventsource;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.util.Objects;

/**
 * Information about a message received from the stream.
 * 

* The properties of a message are defined by the SSE specification. Every message * has an event name set by the "event:" field ({@link #DEFAULT_EVENT_NAME} if not * specified), a data string set by "date:" fields, an optional last event ID string * (set by the "id:" field and defaulting to the same value until it is set again), * and an origin URI (always the same stream URI). */ public class MessageEvent implements StreamEvent { /** * The default value of {@link #getEventName()} for all SSE messages that did not have an {@code event} * field. This constant is defined in the SSE specification. * * @since 2.6.0 */ public static final String DEFAULT_EVENT_NAME = "message"; private static final int READER_BUFFER_SIZE = 2000; // used only when converting a Reader into a String private volatile String data; private volatile Reader dataReader; private final Object dataReaderLock; private final String eventName; private final String lastEventId; private final URI origin; /** * Simple constructor with event data only, using the default event name. *

* This constructor assumes that the event data has been fully read into memory as a String. * * @param data the event data; if null, will be changed to an empty string */ public MessageEvent(String data) { this(null, data, null, null); } /** * Constructs a new instance with the default event name. *

* This constructor assumes that the event data has been fully read into memory as a String. * * @param data the event data; if null, will be changed to an empty string * @param lastEventId the event ID, or null if none * @param origin the stream endpoint */ public MessageEvent(String data, String lastEventId, URI origin) { this(null, data, lastEventId, origin); } /** * Constructs a new instance. *

* This constructor assumes that the event data has been fully read into memory as a String. * * @param eventName the event name; if null, {@link #DEFAULT_EVENT_NAME} is used * @param data the event data; if null, will be changed to an empty string * @param lastEventId the event ID, or null if none * @param origin the stream endpoint * * @since 2.6.0 */ public MessageEvent(String eventName, String data, String lastEventId, URI origin) { this.eventName = eventName == null ? DEFAULT_EVENT_NAME : eventName; this.data = data == null ? "" : data; this.dataReader = null; this.dataReaderLock = new Object(); this.lastEventId = lastEventId; this.origin = origin; } /** * Constructs a new instance with lazy-loading behavior. *

* This constructor takes a {@link Reader} instead of a String for the event data. This is an * optimization that sometimes allows events to be processed without large buffers. The caller * must be careful about using this model because the behavior of the reader is not idempotent; * see {@link #getDataReader()}. * * @param eventName an object that will provide the event name if requested * @param dataReader a {@link Reader} for consuming the event data * @param lastEventId an object that will provide the last event ID if requested * @param origin the stream endpoint * * @see #getDataReader() * @since 2.6.0 */ public MessageEvent( String eventName, Reader dataReader, String lastEventId, URI origin ) { this.data = null; this.dataReader = dataReader; this.dataReaderLock = new Object(); this.eventName = eventName == null ? DEFAULT_EVENT_NAME : eventName; this.lastEventId = lastEventId; this.origin = origin; } /** * Constructs a new instance. * * @param eventName the event name * @param data the event data, if any */ public MessageEvent(String eventName, String data) { this(eventName, data, null, null); } /** * Returns the event name. This is the value of the {@code event} field in the SSE message, or, if * there was none, the constant {@link #DEFAULT_EVENT_NAME}. * * @return the event name * @since 2.6.0 */ public String getEventName() { return eventName; } /** * Returns the event data as a string. *

* The format of event data is described in the SSE specification. Every event has at least one * line with a {@code data} or {@code data:} prefix. After removing the prefix, multiple lines * are concatenated with a separator of {@code '\n'}. *

* If you have set the {@link EventSource.Builder#streamEventData(boolean)} option to {@code true} * to enable streaming delivery of event data to your handler without buffering the entire event, * you should use {@link #getDataReader()} instead of {@link #getData()}. Calling {@link #getData()} * in this mode would defeat the purpose by causing all of the data to be read at once. However, if * you do this, {@link #getData()} memoizes the result so that calling it repeatedly does not try * to read the stream again. *

* The method will never return {@code null}; every event has data, even if the data is empty * (zero length). * * @return the data string */ public String getData() { if (data != null) { return data; } synchronized (dataReaderLock) { if (data != null) { // debounce concurrent requests return data; // COVERAGE: this condition can't be reproduced in unit tests } char[] buffer = new char[READER_BUFFER_SIZE]; StringBuilder sb = new StringBuilder(READER_BUFFER_SIZE); try { int n; while ((n = dataReader.read(buffer, 0, buffer.length)) != -1) { sb.append(buffer, 0, n); } dataReader.close(); } catch (IOException e) { } data = sb.toString(); dataReader = new StringReader(data); return data; } } /** * Returns a single-use {@link Reader} for consuming the event data. *

* This is meant to be used if you have set the option {@link EventSource.Builder#streamEventData(boolean)} * to {@code true}, indicating that you want to consume the data with a streaming approach, * rather than buffering the full event in memory. In this mode, {@link #getDataReader()} * returns a {@link Reader} that consumes the event data while it is being received. The * format is the same as described in {@link #getData()} (for instance, if the data was * sent in multiple lines, the lines are separated by {@code '\n'}). Because this {@link Reader} * is connected directly to the HTTP response stream, it is only valid until you read the next * message. *

* See {@link EventSource.Builder#streamEventData(boolean)} for more details and important * limitations of the streaming mode. *

* If you have not set that option, then the returned {@link Reader} simply provides * the same already-buffered data that would be available from {@link #getData()}. *

* This {@link Reader} can be used only once; if you have already consumed the data, further * calls to {@link #getDataReader()} will return the same instance that will not read any more. *

* The method will never return {@code null}; every event has data, even if the data is empty * (zero length). *

* If the stream connection is closed before a complete SSE message has been received (that is, * before the usual blank line that would terminate a message), then instead of a normal EOF, * the Reader will throw a {@link StreamClosedWithIncompleteMessageException}. If this happens, * the application should generally discard the entire {@link MessageEvent} and not try to * process it further. * * @return a reader for the event data * @since 2.6.0 */ public Reader getDataReader() { synchronized (dataReaderLock) { if (dataReader == null) { dataReader = new StringReader(data); } return dataReader; } } /** * Returns the event ID, if any. * @return the event ID or null */ public String getLastEventId() { return lastEventId; } /** * Returns the endpoint of the stream that generated the event. * @return the stream URI */ public URI getOrigin() { return origin; } /** * Returns {@code true} if this event was dispatched with streaming data behavior rather than pre-read data. *

* This is only the case if you have set the {@link EventSource.Builder#streamEventData(boolean)} option * to {@code true}. * * @return true if the event has streaming data * @since 2.6.0 */ public boolean isStreamingData() { return data == null; } /** * Indicates that you are finished with this message. *

* This method is only relevant if you are using streaming data mode. It tells * EventSource that you will not be using the {@link #getDataReader()} stream any * more and that any remaining data for this event should be skipped. */ public void close() { synchronized (dataReaderLock) { if (dataReader != null) { try { dataReader.close(); } catch (IOException e) {} } } } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MessageEvent that = (MessageEvent) o; return Objects.equals(getEventName(), that.getEventName()) && Objects.equals(getData(), that.getData()) && Objects.equals(getLastEventId(), that.getLastEventId()) && Objects.equals(getOrigin(), that.getOrigin()); } @Override public int hashCode() { return Objects.hash(getEventName(), getData(), getLastEventId(), getOrigin()); } @Override public String toString() { synchronized (dataReaderLock) { StringBuilder sb = new StringBuilder("MessageEvent(eventName=") .append(eventName) .append(",data=") .append(data == null ? "" : data); if (lastEventId != null) { sb.append(",id=").append(lastEventId); } sb.append(",origin=").append(origin).append(')'); return sb.toString(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy