/*
* Copyright 2015 Netflix, Inc.
*
* 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 io.reactivex.netty.protocol.http.sse;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
/**
* An object representing a server-sent-event following the <a href="http://www.w3.org/TR/eventsource/">SSE specifications</a>
*
* A server sent event is composed of the following:
*
* <ul>
<li>Event id: This is the last event id seen on the stream this event was received. This can be null, if no id is received.</li>
<li>Event type: The last seen event type seen on the stream this event was received. This can be null, if no type is received.</li>
<li>Data: This is the actual event data.</li>
</ul>
*
* <h2>Type</h2>
*
* A {@link ServerSentEvent} is of the type {@link Type#Data} unless it is explicitly passed on creation.
*
* <h2>Memory management</h2>
*
* This is an implementation of {@link ByteBufHolder} so it is required to be explicitly released by calling
* {@link #release()} when this instance is no longer required.
*/
public class ServerSentEvent implements ByteBufHolder {
private static final Logger logger = LoggerFactory.getLogger(ServerSentEvent.class);
private static Charset sseEncodingCharset;
static {
try {
sseEncodingCharset = Charset.forName("UTF-8");
} catch (Exception e) {
logger.error("UTF-8 charset not available. Since SSE only contains UTF-8 data, we can not read SSE data.");
sseEncodingCharset = null;
}
}
public enum Type {
Data,
Id,
EventType
}
private final Type type;
/*This is required to make sure we allocate ByteBuf only inside an eventloop, else the ByteBuf pool will grow in the
* owner thread*/
private final String dataAsString;
private final ByteBuf data;
private final ByteBuf eventId;
private final ByteBuf eventType;
public ServerSentEvent(Type type, ByteBuf data) {
this(type, null, null, data);
}
public ServerSentEvent(ByteBuf data) {
this(Type.Data, data);
}
public ServerSentEvent(ByteBuf eventId, ByteBuf eventType, ByteBuf data) {
this(Type.Data, eventId, eventType, data);
}
protected ServerSentEvent(Type type, ByteBuf eventId, ByteBuf eventType, ByteBuf data) {
dataAsString = null;
this.data = data;
this.type = type;
this.eventId = eventId;
this.eventType = eventType;
}
private ServerSentEvent(String data) {
dataAsString = data;
this.data = null;
type = Type.Data;
eventId = null;
eventType = null;
}
/**
* The type of this event. For events which contain an event Id or event type along with data, the type is still
* {@link Type#Data}. The type will be {@link Type#Id} or {@link Type#EventType} only if the event just contains the
* event type or event id and no data.
*
* @return Type of this event.
*/
public Type getType() {
return type;
}
public boolean hasEventId() {
return null != eventId;
}
public boolean hasEventType() {
return null != eventType;
}
public ByteBuf getEventId() {
return eventId;
}
public String getEventIdAsString() {
return eventId.toString(getSseCharset());
}
public ByteBuf getEventType() {
return eventType;
}
public String getEventTypeAsString() {
return eventType.toString(getSseCharset());
}
public boolean hasDataAsString() {
return null != dataAsString;
}
public String contentAsString() {
return null != dataAsString ? dataAsString : data.toString(getSseCharset());
}
@Override
public ByteBuf content() {
return null != data ? data : Unpooled.buffer().writeBytes(dataAsString.getBytes(getSseCharset()));
}
@Override
public ByteBufHolder copy() {
if (hasDataAsString()) {
return new ServerSentEvent(dataAsString);
} else {
return new ServerSentEvent(type, null != eventId? eventId.copy() : null,
null != eventType? eventType.copy() : null, data.copy());
}
}
@Override
public ByteBufHolder duplicate() {
if (hasDataAsString()) {
return new ServerSentEvent(dataAsString);
} else {
return new ServerSentEvent(type, null != eventId ? eventId.duplicate() : null,
null != eventType ? eventType.duplicate() : null, data.duplicate());
}
}
@Override
public ByteBufHolder retainedDuplicate() {
return duplicate().retain();
}
@Override
public ByteBufHolder replace(ByteBuf content) {
return new ServerSentEvent(content);
}
@Override
public int refCnt() {
return hasDataAsString() ? 1 : data.refCnt(); // Ref count is consistent across data, eventId and eventType
}
@Override
public ByteBufHolder retain() {
if(hasEventId()) {
eventId.retain();
}
if(hasEventType()) {
eventType.retain();
}
if (!hasDataAsString()) {
data.retain();
}
return this;
}
@Override
public ByteBufHolder retain(int increment) {
if(hasEventId()) {
eventId.retain(increment);
}
if(hasEventType()) {
eventType.retain(increment);
}
if (!hasDataAsString()) {
data.retain(increment);
}
return this;
}
@Override
public ByteBufHolder touch() {
return touch(null);
}
@Override
public ByteBufHolder touch(Object hint) {
if (!hasDataAsString()) {
data.touch(hint);
}
return this;
}
@Override
public boolean release() {
return data.release(1);
}
@Override
public boolean release(int decrement) {
if(hasEventId()) {
eventId.release(decrement);
}
if(hasEventType()) {
eventType.release(decrement);
}
return data.release(decrement);
}
/**
* Creates a {@link ServerSentEvent} instance with an event id.
*
* @param eventId Id for the event.
* @param data Data for the event.
*
* @return The {@link ServerSentEvent} instance.
*/
public static ServerSentEvent withEventId(ByteBuf eventId, ByteBuf data) {
return new ServerSentEvent(eventId, null, data);
}
/**
* Creates a {@link ServerSentEvent} instance with an event type.
*
* @param eventType Type for the event.
* @param data Data for the event.
*
* @return The {@link ServerSentEvent} instance.
*/
public static ServerSentEvent withEventType(ByteBuf eventType, ByteBuf data) {
return new ServerSentEvent(null, eventType, data);
}
/**
* Creates a {@link ServerSentEvent} instance with an event id and type.
*
* @param eventType Type for the event.
* @param eventId Id for the event.
* @param data Data for the event.
*
* @return The {@link ServerSentEvent} instance.
*/
public static ServerSentEvent withEventIdAndType(ByteBuf eventId, ByteBuf eventType, ByteBuf data) {
return new ServerSentEvent(eventId, eventType, data);
}
/**
* Creates a {@link ServerSentEvent} instance with data.
*
* @param data Data for the event.
*
* @return The {@link ServerSentEvent} instance.
*/
public static ServerSentEvent withData(ByteBuf data) {
return new ServerSentEvent(data);
}
/**
* Creates a {@link ServerSentEvent} instance with data.
*
* @param data Data for the event.
*
* @return The {@link ServerSentEvent} instance.
*/
public static ServerSentEvent withData(String data) {
return new ServerSentEvent(data);
}
protected Charset getSseCharset() {
return null == sseEncodingCharset ? Charset.forName("UTF-8") : sseEncodingCharset;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
if (hasEventId()) {
sb.append("id: ");
sb.append(getEventIdAsString());
sb.append('\n');
}
if (hasEventType()) {
sb.append("event: ");
sb.append(getEventTypeAsString());
sb.append('\n');
}
sb.append("data: ");
sb.append(contentAsString());
sb.append('\n');
return sb.toString();
}
}