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

io.vertx.ext.stomp.lite.frame.Frame Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright (c) 2011-2015 The original author or authors
 *  ------------------------------------------------------
 *  All rights reserved. This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License v1.0
 *  and Apache License v2.0 which accompanies this distribution.
 *
 *       The Eclipse Public License is available at
 *       http://www.eclipse.org/legal/epl-v10.html
 *
 *       The Apache License v2.0 is available at
 *       http://www.opensource.org/licenses/apache2.0.php
 *
 *  You may elect to redistribute this code under either of these licenses.
 */

package io.vertx.ext.stomp.lite.frame;

import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Represents a STOMP frame. STOMP frames are structured as follows. It starts by a {@code command}, followed by a
 * set of headers. Then the frame may have a body and is finished by a {@code 0} byte. This class represents this
 * structure and provide access to the different parts.
 * 
 * This class is NOT thread-safe.
 *
 * @author Clement Escoffier
 */
public class Frame {

  public static final String UTF_8 = "utf-8";

  // General headers
  public static final String CONTENT_LENGTH = "content-length";
  public static final String CONTENT_TYPE = "content-type";

  // Connection headers
  public static final String HOST = "host";

  public static final String VERSION = "version";
  public static final String ACCEPT_VERSION = "accept-version";

  public static final String SESSION = "session";
  public static final String SERVER = "server";

  public static final String LOGIN = "login";
  public static final String PASSCODE = "passcode";
  public static final String HEARTBEAT = "heart-beat";

  // Message headers
  public static final String DESTINATION = "destination";
  public static final String RECEIPT = "receipt";
  public static final String RECEIPT_ID = "receipt-id";
  public static final String ACK = "ack";
  public static final String ID = "id";
  public static final String SUBSCRIPTION = "subscription";
  public static final String MESSAGE_ID = "message-id";
  public static final String TRANSACTION = "transaction";
  public static final String MESSAGE = "message";

  /**
   * Header used when a frame using an unknown command is received. The created {@link Frame} object uses
   * the {@link Command#UNKNOWN} command and gives the original command in this header.
   */
  public static final String STOMP_FRAME_COMMAND = "frame-command";

  /**
   * Regex used to extract the body encoding that may be specified in the {@code content-type} header. By default,
   * UTF-8 is used.
   */
  private final static Pattern CHARSET_PATTERN = Pattern.compile(".+;charset=([a-zA-Z0-9\\-]+);?.*");

  /**
   * The list of command defined by the STOMP specification.
   * It also contains a {@code PING} command used for heartbeat. It should not be used in frames (as it's not a valid
   * command).
   */
  public enum Command {
    // Connection
    CONNECT,
    CONNECTED,
    STOMP,

    // Client
    SEND,
    SUBSCRIBE,
    UNSUBSCRIBE,
    ACK,
    NACK,
    BEGIN,
    COMMIT,
    ABORT,
    DISCONNECT,

    // Server
    MESSAGE,
    RECEIPT,
    ERROR,

    // This is not a real STOMP frame, it just notice a ping frame from the client.
    PING,

    UNKNOWN
  }

  /**
   * Represents the heartbeat configuration. Heartbeat determine when a party involved in the exchange (either the
   * client or the server) can detect the inactivity of the other party and close the connection. Configuration is
   * made in the {@code heartbeat} header.
   * 
   * This class is thread-safe.
   */
  public static class Heartbeat {
    final int x;
    final int y;

    public Heartbeat(int x, int y) {
      this.x = x;
      this.y = y;
    }

    /**
     * Creates an instance of {@link Frame.Heartbeat} from the {@code heartbeat header} of a frame
     * . If the header is {@code null}, the (0,0) configuration is returned.
     *
     * @param header the header
     * @return the heartbeat configuration
     */
    public static Heartbeat parse(String header) {
      if (header == null) {
        return new Heartbeat(0, 0);
      } else {
        String[] token = header.split(FrameParser.COMMA);
        return new Heartbeat(Integer.parseInt(token[0]), Integer.parseInt(token[1]));
      }
    }

    /**
     * Creates an instance of {@link Frame.Heartbeat} from the JSON configuration provides in the
     * client / server options. The JSON is structure as follows: {@code {"x": 1000, "y": 1000}}. The {@code x} and
     * {@code y} time are given in milliseconds.
     *
     * @param json the json object configuring the heartbeat.
     * @return the heartbeat configuration
     */
    public static Heartbeat create(JsonObject json) {
      return new Heartbeat(
          json.getInteger("x", 0),
          json.getInteger("y", 0));
    }

    /**
     * @return the heartbeat configuration to be used in the {@code heartbeat} header.
     */
    @Override
    public String toString() {
      return x + "," + y;
    }

    /**
     * Computes the period at which the client must send a Heartbeat to the server.
     * If no heartbeat or other data is received within this time the client must be considered inactive.
     * The value is computed from the two parties heartbeat configuration.
     *
     * @param client the client configuration
     * @param server the server configuration
     * @return the clientHeartbeat period
     */
    public static long computeClientHeartbeatPeriod(Heartbeat client, Heartbeat server) {
      if (client.x == 0 || server.y == 0) {
        return 0;
      }
      return Math.max(client.x, server.y);
    }

    /**
     * Computes the period at which the server must send a Heartbeat to the client.
     * If no heartbeat or other data is received within this time the server must be considered inactive.
     * The value is computed from the two parties heartbeat configuration.
     *
     * @param client the client configuration
     * @param server the server configuration
     * @return the serverHeartbeat period
     */
    public static long computeServerHeartbeatPeriod(Heartbeat client, Heartbeat server) {
      if (client.y == 0 || server.x == 0) {
        return 0;
      }
      return Math.max(client.y, server.x);
    }
  }

  /**
   * Only SEND, MESSAGE and ERROR frames accept bodies.
   */
  private final static List COMMANDS_ACCEPTING_BODY = Arrays.asList(
      Command.SEND, Command.MESSAGE, Command.ERROR, Command.UNKNOWN);

  private Command command;

  private final Map headers;

  private Buffer body;

  /**
   * Creates an un-configured frame. Should only be used by converters.
   */
  public Frame() {
    // Default constructor.
    headers = new Headers();
  }

  /**
   * Creates a new instance of {@link Frame} by copying the values from the {@code other} frame. The body of the
   * frame is copied.
   *
   * @param other the frame to copy.
   */
  public Frame(Frame other) {
    this();
    this.command = other.command;
    this.headers.putAll(headers);
    if (other.body != null) {
      this.body = other.body.copy();
    }
    validate();
  }

  /**
   * Creates a new instance of {@link Frame} from its JSON representation.
   *
   * @param json the json form of the frame
   */
  public Frame(JsonObject json) {
    this();
    FrameConverter.fromJson(json, this);
    validate();
  }

  /**
   * Creates a new instance of {@link Frame}.
   *
   * @param command the command, must not be {@code null}
   * @param headers the headers, must not be {@code null}
   * @param body    the body
   */
  public Frame(Command command, Map headers, Buffer body) {
    Objects.requireNonNull(command, "The frame command must be set");
    Objects.requireNonNull(headers, "The headers must be set to empty if none");
    this.command = command;
    this.headers = headers;
    this.body = body;
    validate();
  }

  /**
   * Adds a header to the frame.
   *
   * @param key   the header name
   * @param value the header value
   * @return the current {@link Frame}
   */
  public Frame addHeader(String key, String value) {
    headers.putIfAbsent(key, value);
    return this;
  }

  /**
   * Gets the value of the {@code ack} header.
   *
   * @return the {@code ack} header value, {@code null} if not set
   */
  public String getAck() {
    return headers.get(ACK);
  }

  /**
   * Gets the frame headers. Modifications to the returned {@link Map} modifies the headers of the frame.
   *
   * @return the headers
   */
  public Map getHeaders() {
    return headers;
  }

  /**
   * Sets the headers of the frames.
   *
   * @param headers the header, may be {@code null}. In the {@code null} case, an empty map is used to store the
   *                frame headers.
   * @return the current {@link Frame}
   */
  public Frame setHeaders(Map headers) {
    if (headers == null) {
      this.headers.clear();
    } else {
      this.headers.clear();
      this.headers.putAll(headers);
    }
    return this;
  }

  /**
   * Sets the frame command.
   *
   * @param command the command, must not be {@code null}
   * @return the current {@link Frame}
   */
  public Frame setCommand(Command command) {
    Objects.requireNonNull(command, "The frame command must not be null.");
    this.command = command;
    return this;
  }

  /**
   * Sets the body of the frame.
   *
   * @param body the body
   * @return the current {@link Frame}
   */
  public Frame setBody(Buffer body) {
    this.body = body;
    return this;
  }

  /**
   * @return the JSON representation of the current frame.
   */
  public JsonObject toJson() {
    JsonObject json = new JsonObject();
    FrameConverter.toJson(this, json);
    return json;
  }

  /**
   * Checks the validity of the frame. Frames must have a valid command, and not all frames can have a body.
   */
  public void validate() {
    // A frame must have a valid command
    if (command == null) {
      throw new FrameException("The frame does not have a command");
    }

    // Spec says: Only the SEND, MESSAGE and ERROR frames MAY have a body, All other frames MUST NOT
    // have a body
    if (!COMMANDS_ACCEPTING_BODY.contains(command) && !hasEmptyBody()) {
      throw new FrameException("The frame " + command.name() + " cannot have a body");
    }

  }

  /**
   * @return whether or not the frame has a body.
   */
  public boolean hasEmptyBody() {
    return body == null || body.length() == 0;
  }

  public Command getCommand() {
    return command;
  }

  /**
   * Gets the value of the header with the given name.
   *
   * @param name the header name
   * @return the value, {@code null} if not set
   */
  public String getHeader(String name) {
    return headers.get(name);
  }

  public Buffer getBody() {
    return body;
  }

  /**
   * Gets the body of the frames as a String encoded in the given encoding.
   *
   * @param encoding the encoding
   * @return the body, {@code null} if none
   */
  public String getBodyAsString(String encoding) {
    if (body == null) {
      return null;
    }
    return body.toString(encoding);
  }

  /**
   * Gets the body of the frames as a String encoded in the frame encoding.
   *
   * @return the body, {@code null} if none
   */
  public String getBodyAsString() {
    return getBodyAsString(encoding());
  }

  /**
   * Read the frame encoding. If not set defaults to utf-8.
   *
   * @return the encoding
   */
  public String encoding() {
    String header = getHeader(CONTENT_TYPE);
    if (header == null) {
      return UTF_8;
    } else {
      final Matcher matcher = CHARSET_PATTERN.matcher(header);
      if (matcher.matches()) {
        return matcher.group(1);
      } else {
        return UTF_8;
      }
    }
  }

  /**
   * @return the body of the frame as a byte array, {@code null} if none.
   */
  public byte[] getBodyAsByteArray() {
    if (body == null) {
      return null;
    }
    return body.getBytes();
  }

  /**
   * Creates a buffer for the current frame. This buffer may contain an empty line if the {@code trailingLine} is set
   * to {@code true}
   *
   * @param trailingLine whether or not a trailing line should be added to the buffer
   * @return a {@link Buffer} containing the STOMP frame. It follows strictly the STOMP specification (including
   * header encoding).
   */
  public Buffer toBuffer(boolean trailingLine) {
    Buffer buffer = toBuffer();
    if (trailingLine) {
      buffer.appendString("\n");
    }
    return buffer;
  }

  /**
   * This method does not enforce the trailing line option. It should not be used directly, except for the PING frame.
   *
   * @return a {@link Buffer} containing the STOMP frame. It follows strictly the STOMP specification (including
   * header encoding).
   */
  public Buffer toBuffer() {
    Buffer buffer = Buffer.buffer(command.name() + "\n");
    for (Map.Entry entry : headers.entrySet()) {
      buffer.appendString(encode(entry.getKey()) + ":" + encode(entry.getValue()) + "\n");
    }
    buffer.appendString("\n");
    if (body != null) {
      buffer.appendBuffer(body);
    }
    buffer.appendString(FrameParser.NULL);
    return buffer;
  }

  private String encode(String header) {
    // By spec, frame headers need to be encoded. CONNECT and CONNECTED frames do not encode \r \n \c but still
    // require the encoding of \\.
    return HeaderCodec.encode(header, command == Command.CONNECT || command == Command.CONNECTED);
  }

  public String toString() {
    StringBuilder buffer = new StringBuilder(command.name() + "\n");
    for (Map.Entry entry : headers.entrySet()) {
      if (entry.getKey().equals(PASSCODE)) {
        buffer.append(entry.getKey()).append(":").append("********").append("\n");
      } else {
        buffer.append(entry.getKey()).append(":").append(entry.getValue()).append("\n");
      }
    }
    buffer.append("\n");
    if (body != null) {
      buffer.append(body);
    }
    buffer.append("^@");
    return buffer.toString();
  }

  // Getter and Setter on basic headers

  public Frame setDestination(String destination) {
    Objects.requireNonNull(destination);
    return addHeader(DESTINATION, destination);
  }

  public Frame setTransaction(String id) {
    Objects.requireNonNull(id);
    return addHeader(TRANSACTION, id);
  }

  public Frame setId(String id) {
    Objects.requireNonNull(id);
    return addHeader(ID, id);
  }

  public String getId() {
    return getHeader(ID);
  }

  public String getReceipt() {
    return getHeader(RECEIPT);
  }

  public String getTransaction() {
    return getHeader(TRANSACTION);
  }

  public String getDestination() {
    return getHeader(DESTINATION);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy