/*
* COMSAT
* Copyright (c) 2013-2014, Parallel Universe Software Co. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v1.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 3.0
* as published by the Free Software Foundation.
*/
package co.paralleluniverse.comsat.webactors;
import co.paralleluniverse.actors.ActorRef;
import co.paralleluniverse.strands.channels.Channels;
import co.paralleluniverse.strands.channels.SendPort;
import com.google.common.base.Function;
import java.nio.charset.Charset;
/**
* Utility classes for SSE (<a href="http://dev.w3.org/html5/eventsource/">Server-Sent Events</a>).
* To start an SSE stream in response to an {@link HttpRequest}, do the following:
*
* ```java
* request.getFrom().send(new HttpResponse(self(), SSE.startSSE(request)));
* ```
* This will result in a {@link HttpStreamOpened} message being sent to the web actor from a newly
* created actor that represents the SSE connection. To send SSE events, simply send {@link WebDataMessage}s
* to that actor:
*
* ```java
* // send events
* sseActor.send(new WebDataMessage(self(), SSE.event("this is an SSE event!")));
* ```
*
* You might want to consider wrapping the actor sending {@link HttpStreamOpened} with a
* {@link co.paralleluniverse.strands.channels.Channels#mapSend(co.paralleluniverse.strands.channels.SendPort, com.google.common.base.Function) mapping channel}
* to transform a specialized message class into {@link WebDataMessage} using the methods in this class.
*
* For a good tutorial on SSE, please see: <a href="http://www.html5rocks.com/en/tutorials/eventsource/basics/">Stream Updates with Server-Sent Events</a>,
* by Eric Bidelman
*/
public final class SSE {
/*
*see http://www.html5rocks.com/en/tutorials/eventsource/basics/
*/
/**
* This method returns a new {@link HttpResponse HttpResponse} with
* its {@link HttpResponse.Builder#setContentType(String) content type}
* and {@link HttpResponse.Builder#setCharacterEncoding(java.nio.charset.Charset) character encoding} set
* in compliance with to the SSE spec, and an empty body.
*
* @param request the {@link HttpRequest} in response to which we wish to start an SSE stream.
* @return an {@link HttpResponse.Builder HttpResponse.Builder} (which can have other metadata, such as headers or cookies added to).
*/
public static HttpResponse.Builder startSSE(ActorRef<? super WebMessage> from, HttpRequest request) {
return new HttpResponse.Builder(from, request)
.setContentType("text/event-stream")
.setCharacterEncoding(Charset.forName("UTF-8"))
.startActor();
}
/**
* This method returns a new {@link HttpResponse HttpResponse} with
* its {@link HttpResponse.Builder#setContentType(String) content type}
* and {@link HttpResponse.Builder#setCharacterEncoding(java.nio.charset.Charset) character encoding} set
* in compliance with to the SSE spec, and a body encoding a {@link #reconnectTimeout(long) reconnection timeout} indication.
*
* @param request the {@link HttpRequest} in response to which we wish to start an SSE stream.
* @param reconnectTimeout the amount of time, in milliseconds, the client should wait before attempting to reconnect
* after the connection has closed (will be encoded in the message body as {@code retry: ...})
* @return an {@link HttpResponse.Builder HttpResponse.Builder} (which can have other metadata, such as headers or cookies added to).
*/
public static HttpResponse.Builder startSSE(ActorRef<? super WebMessage> from, HttpRequest request, long reconnectTimeout) {
return new HttpResponse.Builder(from, request, retryString(reconnectTimeout) + '\n')
.setContentType("text/event-stream")
.setCharacterEncoding(Charset.forName("UTF-8"))
.startActor();
}
/**
* Wrappes the whole string body
*/
public static SendPort<WebDataMessage> wrapAsSSE(SendPort<WebDataMessage> actor) {
return Channels.mapSend(actor, new Function<WebDataMessage, WebDataMessage>() {
@Override
public WebDataMessage apply(WebDataMessage f) {
return new WebDataMessage(f.getFrom(), SSE.event(f.getStringBody()));
}
});
}
/**
* Returns the SSE last-event-id value from the request (the {@code Last-Event-ID} header).
*
* @param request the request
* @return the SSE last-event-id value from the request, or {@code -1} if not specified.
*/
public static long getLastEventId(HttpRequest request) {
String str = request.getHeader("Last-Event-ID");
if (str == null)
return -1;
return Long.parseLong(str);
}
/**
* Encodes a given payload as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param id the SSE event id (will be encoded in the message as {@code id: ...})
* @param eventType the name of the type of the event (will be encoded in the message as {@code event: ...})
* @param payload the message payload (will be encoded in the message as {@code data: ...})
* @return the payload encoded as an SSE event
*/
public static String event(long id, String eventType, String payload) {
return idString(id) + eventString(eventType) + dataString(payload) + '\n';
}
/**
* Encodes a given payload as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param eventType the name of the type of the event (will be encoded in the message as {@code event: ...})
* @param payload the message payload (will be encoded in the message as {@code data: ...})
* @return the payload encoded as an SSE event
*/
public static String event(String eventType, String payload) {
return dataString(payload) + '\n';
}
/**
* Encodes a given payload and id as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param id the SSE event id (will be encoded in the message as {@code id: ...})
* @param payload the message payload (will be encoded in the message as {@code data: ...})
* @return the id and payload encoded as an SSE event
*/
public static String event(long id, String payload) {
return idString(id) + dataString(payload) + '\n';
}
/**
* Encodes a given payload as an SSE event message. The returned value can be used as the body of a {@link WebDataMessage}.
*
* @param payload the message payload the message payload (will be encoded in the message as {@code data: ...})
* @return the payload encoded as an SSE event
*/
public static String event(String payload) {
return dataString(payload) + '\n';
}
/**
* Encodes an indication to the client to attempt a reconnect if the connection is closed within the given time.
* This string may be concatenated ahead of a string encoding an SSE event, like so: {@code reconnectTimeout(t) + event(x)}).
*
* @param reconnectTimeout the amount of time, in milliseconds, the client should wait before attempting to reconnect
* after the connection has closed (will be encoded in the message as {@code retry: ...})
* @return a string encoding the reconnection timeout indication
*/
public static String reconnectTimeout(long reconnectTimeout) {
return retryString(reconnectTimeout);
}
private static String idString(long id) {
return "id: " + id + '\n';
}
private static String eventString(String eventName) {
return "event: " + eventName + '\n';
}
private static String retryString(long reconnectTimeout) {
return "retry: " + reconnectTimeout + '\n';
}
private static String dataString(String payload) {
String message = payload.trim();
if (message.charAt(message.length() - 1) == '\n')
message = message.substring(0, message.length() - 1);
message = message.replaceAll("\n", "\ndata: ");
message = "data: " + message + '\n';
return message;
}
private SSE() {
}
}