ratpack.sse.internal.ServerSentEventDecoder Maven / Gradle / Ivy
/*
* Copyright 2015 the original author or authors.
*
* 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 ratpack.sse.internal;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.util.ByteProcessor;
import ratpack.func.Action;
import ratpack.sse.Event;
import static io.netty.util.CharsetUtil.UTF_8;
/**
* Decodes {@link io.netty.buffer.ByteBuf}s into {@link ratpack.sse.Event}s.
*
* ByteBufs are parsed as detailed by the W3C Server-Sent Events Specification. Inspiration was also taken from
* RxNetty's decoder
*
* A single ByteBuf may result in 0 or more events being dispatched to the supplied event action.
*/
public class ServerSentEventDecoder {
private static final char LINE_FEED = '\n';
private static final char CARRIAGE_RETURN = '\r';
private static final char[] EVENT_ID_FIELD_NAME = "event".toCharArray();
private static final char[] DATA_FIELD_NAME = "data".toCharArray();
private static final char[] ID_FIELD_NAME = "id".toCharArray();
private static final ByteProcessor SCAN_EOL_PROCESSOR = value -> !isLineDelimiter((char) value);
private static final ByteProcessor SCAN_COLON_PROCESSOR = value -> (char) value != ':';
private static final ByteProcessor SKIP_COLON_AND_WHITE_SPACE_PROCESSOR = value -> {
char valueChar = (char) value;
return valueChar == ':' || valueChar == ' ';
};
public static final ServerSentEventDecoder INSTANCE = new ServerSentEventDecoder();
private ServerSentEventDecoder() {}
private enum State {
SkipColonAndWhiteSpaces, // Skip colon and all whitespaces after reading field name.
SkipTillEOL, // Skip to the end of the current line.
ReadFieldName, // Read till a colon to get the name of the field.
ReadFieldValue, // Read value till the line delimiter.
ReadLine, // Read till the next line delimeter.
DispatchEvent // Dispatch a new server sent event with the current values.
}
private enum FieldName {
Event,
Data,
Id
}
public void decode(ByteBuf in, ByteBufAllocator bufferAllocator, Action> eventAction) throws Exception {
ByteBuf currentLineBuffer = null;
String eventData = null;
String eventType = null;
String eventId = null;
FieldName currentFieldType = null;
State state = State.ReadLine;
try {
while (in.isReadable()) {
final int readerIndexAtStart = in.readerIndex();
switch (state) {
case ReadLine:
final int endOfLineStartIndex = scanAndFindEndOfLine(in);
if (-1 == endOfLineStartIndex) { // End of line not found, set readIndex to the end
in.readerIndex(in.capacity());
} else {
final int bytesAvailableInThisLine = endOfLineStartIndex - readerIndexAtStart;
if (bytesAvailableInThisLine == 0) {
state = State.DispatchEvent;
} else {
currentLineBuffer = bufferAllocator.buffer(bytesAvailableInThisLine, bytesAvailableInThisLine);
in.readBytes(currentLineBuffer, bytesAvailableInThisLine);
state = State.ReadFieldName;
}
}
break;
case DispatchEvent:
eventAction.execute(new DefaultEvent<>().id(eventId).event(eventType).data(eventData));
eventId = null;
eventType = null;
eventData = null;
state = State.SkipTillEOL;
break;
case ReadFieldName:
int indexOfColon = scanAndFindColon(currentLineBuffer);
if (-1 == indexOfColon) { // No colon found, use the whole line as the field name, and the empty string as the field value.
indexOfColon = currentLineBuffer.capacity();
}
if (1 == indexOfColon) { // Line starts with a colon, ignore
state = State.ReadLine;
currentLineBuffer.release();
} else {
ByteBuf fieldNameBuffer = bufferAllocator.buffer(indexOfColon, indexOfColon);
currentLineBuffer.readBytes(fieldNameBuffer, indexOfColon);
state = State.SkipColonAndWhiteSpaces; // We have read the field name, next we should skip colon & WS.
try {
currentFieldType = readCurrentFieldTypeFromBuffer(fieldNameBuffer);
} finally {
if (null == currentFieldType) {
state = State.SkipTillEOL; // Ignore this event completely.
}
fieldNameBuffer.release();
}
}
break;
case SkipColonAndWhiteSpaces:
skipColonAndWhiteSpaces(currentLineBuffer);
state = State.ReadFieldValue;
break;
case SkipTillEOL:
skipTilEOL(in);
state = State.ReadLine;
break;
case ReadFieldValue:
final int bytesAvailableInThisValue = currentLineBuffer.readableBytes();
ByteBuf currentFieldValue = bufferAllocator.buffer(bytesAvailableInThisValue, bytesAvailableInThisValue);
currentLineBuffer.readBytes(currentFieldValue, bytesAvailableInThisValue);
state = State.SkipTillEOL;
switch (currentFieldType) {
case Data:
if (null == eventData) {
eventData = currentFieldValue.toString(UTF_8);
} else {
eventData = eventData + LINE_FEED + currentFieldValue.toString(UTF_8);
}
break;
case Id:
eventId = currentFieldValue.toString(UTF_8);
break;
case Event:
eventType = currentFieldValue.toString(UTF_8);
break;
}
currentFieldValue.release();
currentLineBuffer.release();
break;
}
}
} finally {
in.release();
}
}
private static int scanAndFindColon(ByteBuf byteBuf) {
return byteBuf.forEachByte(SCAN_COLON_PROCESSOR);
}
private static int scanAndFindEndOfLine(ByteBuf byteBuf) {
return byteBuf.forEachByte(SCAN_EOL_PROCESSOR);
}
private static boolean skipColonAndWhiteSpaces(ByteBuf byteBuf) {
return skipTillMatching(byteBuf, SKIP_COLON_AND_WHITE_SPACE_PROCESSOR);
}
private static boolean skipTilEOL(ByteBuf byteBuf) {
if (skipLineFeed(byteBuf)) {
return skipCarriageReturn(byteBuf);
}
return skipCarriageReturn(byteBuf) && skipLineFeed(byteBuf);
}
private static boolean skipLineFeed(ByteBuf byteBuf) {
return skipTillFirstMatching(byteBuf, (byte) LINE_FEED);
}
private static boolean skipCarriageReturn(ByteBuf byteBuf) {
return skipTillFirstMatching(byteBuf, (byte) CARRIAGE_RETURN);
}
private static boolean skipTillMatching(ByteBuf byteBuf, ByteProcessor processor) {
final int lastIndexProcessed = byteBuf.forEachByte(processor);
if (-1 == lastIndexProcessed) {
byteBuf.readerIndex(byteBuf.readerIndex() + byteBuf.readableBytes()); // If all the remaining bytes are to be ignored, discard the buffer.
} else {
byteBuf.readerIndex(lastIndexProcessed);
}
return -1 != lastIndexProcessed;
}
private static boolean skipTillFirstMatching(ByteBuf byteBuf, byte thing) {
int i = byteBuf.indexOf(byteBuf.readerIndex(), byteBuf.capacity(), thing);
if (-1 == i) {
return false;
} else {
byteBuf.readByte();
return true;
}
}
/**
* This code tries to eliminate the need of creating a string from the ByteBuf as the field names are very
* constrained. The algorithm is as follows:
*
* -- Scan the bytes in the buffer.
* -- If the first byte matches the expected field names then use the matching field name char array to verify
* the rest of the field name.
* -- If the first byte does not match, reject the field name.
* -- After the first byte, exact match the rest of the field name with the expected field name, byte by byte.
* -- If the name does not exactly match the expected value, then reject the field name.
*/
private static FieldName readCurrentFieldTypeFromBuffer(final ByteBuf fieldNameBuffer) {
FieldName toReturn = FieldName.Data;
int readableBytes = fieldNameBuffer.readableBytes();
final int readerIndexAtStart = fieldNameBuffer.readerIndex();
char[] fieldNameToVerify = DATA_FIELD_NAME;
boolean verified = false;
int actualFieldNameIndexToCheck = 0; // Starts with 1 as the first char is validated by equality.
for (int i = readerIndexAtStart; i < readableBytes; i++) {
final char charAtI = (char) fieldNameBuffer.getByte(i);
if (i == readerIndexAtStart) {
switch (charAtI) { // See which among the known field names this buffer belongs.
case 'e':
fieldNameToVerify = EVENT_ID_FIELD_NAME;
toReturn = FieldName.Event;
break;
case 'd':
fieldNameToVerify = DATA_FIELD_NAME;
toReturn = FieldName.Data;
break;
case 'i':
fieldNameToVerify = ID_FIELD_NAME;
toReturn = FieldName.Id;
break;
default:
return null;
}
} else {
if (++actualFieldNameIndexToCheck >= fieldNameToVerify.length || charAtI != fieldNameToVerify[actualFieldNameIndexToCheck]) {
// If the character does not match or the buffer is bigger than the expected name, then discard.
verified = false;
break;
} else {
// Verified till now. If all characters are matching then this stays as verified, else changed to false.
verified = true;
}
}
}
if (verified) {
return toReturn;
} else {
return null;
}
}
private static boolean isLineDelimiter(char c) {
return c == CARRIAGE_RETURN || c == LINE_FEED;
}
}