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

ratpack.sse.internal.ServerSentEventDecoder Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc-1
Show newest version
/*
 * Copyright 2016 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.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.ByteProcessor;
import ratpack.func.Action;
import ratpack.sse.ServerSentEvent;
import ratpack.sse.ServerSentEventBuilder;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ServerSentEventDecoder implements AutoCloseable {

  private static final ByteBuf NEWLINE_BYTEBUF = Unpooled.unreleasableBuffer(Unpooled.wrappedBuffer(new byte[]{'\n'}));
  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 SKIP_TILL_LINE_DELIMITER_PROCESSOR = value -> !isLineDelimiter((char) value);

  private static final ByteProcessor SKIP_LINE_DELIMITERS_AND_SPACES_PROCESSOR = value -> isLineDelimiter((char) value) || (char) value == ' ';

  private static final ByteProcessor SKIP_COLON_AND_WHITE_SPACE_PROCESSOR = value -> {
    char valueChar = (char) value;
    return valueChar == ':' || valueChar == ' ';
  };

  private static final ByteProcessor SCAN_COLON_PROCESSOR = value -> (char) value != ':';

  private static final ByteProcessor SCAN_EOL_PROCESSOR = value -> !isLineDelimiter((char) value);

  private enum State {
    ReadFieldName,
    SkipColonAndWhiteSpaces,
    ReadFieldValue,
    DiscardUntilEOL,
    DiscardEOL,
    Closed
  }

  private enum Type {
    Data,
    Id,
    EventType
  }

  private List eventId = new ArrayList<>(1);
  private List eventType = new ArrayList<>(1);
  private List eventData = new ArrayList<>(1);

  private ByteBuf buffer; // Can be field value of name, according to the current state.

  private Type currentFieldType;
  private State state = State.ReadFieldName;

  private final ByteBufAllocator allocator;
  private final Action emitter;

  public ServerSentEventDecoder(ByteBufAllocator allocator, Action emitter) {
    this.allocator = allocator;
    this.emitter = emitter;
  }

  public void decode(ByteBuf in) throws Exception {
    if (state == State.Closed) {
      in.release();
      return;
    }

    try {
      doDecode(in);
    } catch (Exception e) {
      close();
      throw e;
    } finally {
      in.release();
    }
  }

  private void doDecode(ByteBuf in) throws Exception {
    while (in.isReadable()) {
      final int readerIndexAtStart = in.readerIndex();

      switch (state) {
        case SkipColonAndWhiteSpaces:
          if (skipColonAndWhiteSpaces(in)) {
            state = State.ReadFieldValue;
          }
          break;
        case DiscardUntilEOL:
          if (skipTillEOL(in)) {
            state = State.DiscardEOL;
          }
          break;
        case DiscardEOL:
          byte b = in.readByte();
          assert b == '\n';
          state = State.ReadFieldName;
          break;
        case ReadFieldName:
          byte peek = peek(in);
          if (peek == '\n') {
            in.readByte();
            emit();
            break;
          }

          int endOfNameIndex = scanAndFindColon(in);
          if (endOfNameIndex == -1) {
            endOfNameIndex = scanAndFindEndOfLine(in);
          }

          if (endOfNameIndex == -1) {
            // Accumulate data into the field name buffer.
            if (buffer == null) {
              buffer = allocator.buffer();
            }
            // accumulate into incomplete data buffer to be used when the full data arrives.
            buffer.writeBytes(in);
            break;
          }

          int fieldNameLengthInTheCurrentBuffer = endOfNameIndex - readerIndexAtStart;

          ByteBuf fieldNameBuffer;
          if (buffer == null) {
            // Consume the data from the input buffer.
            fieldNameBuffer = allocator.buffer(fieldNameLengthInTheCurrentBuffer, fieldNameLengthInTheCurrentBuffer);
            in.readBytes(fieldNameBuffer, fieldNameLengthInTheCurrentBuffer);
          } else {
            // Read the remaining data into the temporary buffer
            in.readBytes(buffer, fieldNameLengthInTheCurrentBuffer);
            fieldNameBuffer = buffer;
            buffer = null;
          }

          state = State.SkipColonAndWhiteSpaces; // We have read the field name, next we should skip colon & WS.
          try {
            currentFieldType = readCurrentFieldTypeFromBuffer(fieldNameBuffer);
          } finally {
            if (currentFieldType == null) {
              state = State.DiscardUntilEOL; // Ignore this event completely.
            }
            fieldNameBuffer.release();
          }
          break;
        case ReadFieldValue:

          final int endOfLineStartIndex = scanAndFindEndOfLine(in);
          if (endOfLineStartIndex == -1) { // End of line not found, accumulate data into a temporary buffer.
            if (buffer == null) {
              buffer = allocator.buffer(in.readableBytes());
            }
            // accumulate into incomplete data buffer to be used when the full data arrives.
            buffer.writeBytes(in);
          } else { // Read the data till end of line into the value buffer.
            final int bytesAvailableInThisIteration = endOfLineStartIndex - readerIndexAtStart;
            if (buffer == null) {
              buffer = allocator.buffer(bytesAvailableInThisIteration, bytesAvailableInThisIteration);
            }
            buffer.writeBytes(in, bytesAvailableInThisIteration);

            List field;
            switch (currentFieldType) {
              case Data:
                field = eventData;
                break;
              case Id:
                field = eventId;
                break;
              default: // type
                field = eventType;
                break;
            }

            field.add(buffer);
            buffer = null;
            state = State.DiscardUntilEOL;
          }
          break;
      }
    }
  }

  private void emit() throws Exception {
    boolean any = false;
    ServerSentEventBuilder event = ServerSentEvent.builder();
    String id = str(eventId);
    if (id != null) {
      any = true;
      event.id(id);
    }
    String type = str(eventType);
    if (type != null) {
      any = true;
      event.event(type);
    }
    String data = str(eventData);
    if (data != null) {
      any = true;
      event.data(data);
    }

    if (any) {
      emitter.execute(event.build());
    }

    state = State.ReadFieldName;
  }

  private String str(List bufs) {
    if (bufs.isEmpty()) {
      return null;
    } else {
      try {
        String str;
        if (bufs.size() == 1) {
          ByteBuf buf = bufs.get(0);
          str = buf.toString(StandardCharsets.UTF_8);
          buf.release();
        } else {
          CompositeByteBuf composite = allocator.compositeBuffer(bufs.size() * 2 - 1);
          try {
            Iterator iterator = bufs.iterator();
            composite.addComponent(true, iterator.next());
            while (iterator.hasNext()) {
              composite.addComponent(true, NEWLINE_BYTEBUF);
              composite.addComponent(true, iterator.next());
            }
            str = composite.toString(StandardCharsets.UTF_8);
          } finally {
            composite.release();
          }
        }
        return str;
      } finally {
        bufs.clear();
      }
    }
  }

  private static Type readCurrentFieldTypeFromBuffer(final ByteBuf fieldNameBuffer) {
    /*
     * 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.
     * -- Ignore an leading whitespaces
     * -- 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.
     */
    Type toReturn = Type.Data;
    skipLineDelimiters(fieldNameBuffer);
    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 < readerIndexAtStart + 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 = Type.EventType;
            break;
          case 'd':
            fieldNameToVerify = DATA_FIELD_NAME;
            toReturn = Type.Data;
            break;
          case 'i':
            fieldNameToVerify = ID_FIELD_NAME;
            toReturn = Type.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;
    }
  }

  @Override
  public void close() {
    if (eventId != null) {
      eventId.forEach(ByteBuf::release);
      eventId = null;
    }
    if (eventType != null) {
      eventType.forEach(ByteBuf::release);
      eventType = null;
    }
    if (eventData != null) {
      eventData.forEach(ByteBuf::release);
      eventData = null;
    }

    if (buffer != null) {
      buffer.release();
      buffer = null;
    }

    state = State.Closed;
  }

  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 skipLineDelimiters(ByteBuf byteBuf) {
    return skipTillMatching(byteBuf, SKIP_LINE_DELIMITERS_AND_SPACES_PROCESSOR);
  }

  private static boolean skipColonAndWhiteSpaces(ByteBuf byteBuf) {
    return skipTillMatching(byteBuf, SKIP_COLON_AND_WHITE_SPACE_PROCESSOR);
  }

  private static boolean skipTillEOL(ByteBuf in) {
    return skipTillMatching(in, SKIP_TILL_LINE_DELIMITER_PROCESSOR);
  }

  private static boolean skipTillMatching(ByteBuf byteBuf, ByteProcessor processor) {
    final int lastIndexProcessed = byteBuf.forEachByte(processor);
    if (lastIndexProcessed == -1) {
      byteBuf.readerIndex(byteBuf.readerIndex() + byteBuf.readableBytes()); // If all the remaining bytes are to be ignored, discard the buffer.
    } else {
      byteBuf.readerIndex(lastIndexProcessed);
    }

    return lastIndexProcessed != -1;
  }

  private static boolean isLineDelimiter(char c) {
    return c == '\n';
  }

  private static byte peek(ByteBuf buffer) {
    return buffer.getByte(buffer.readerIndex());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy