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

io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClientCodec Maven / Gradle / Ivy

package io.scalecube.services.gateway.transport.websocket;

import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MappingJsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import io.scalecube.services.api.ServiceMessage;
import io.scalecube.services.exceptions.MessageCodecException;
import io.scalecube.services.gateway.transport.GatewayClientCodec;
import io.scalecube.services.transport.api.DataCodec;
import io.scalecube.services.transport.api.ReferenceCountUtil;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map.Entry;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class WebsocketGatewayClientCodec implements GatewayClientCodec {

  private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketGatewayClientCodec.class);

  private static final MappingJsonFactory jsonFactory = new MappingJsonFactory(objectMapper());

  // special numeric fields
  private static final String STREAM_ID_FIELD = "sid";
  private static final String SIGNAL_FIELD = "sig";
  private static final String INACTIVITY_FIELD = "i";
  private static final String RATE_LIMIT_FIELD = "rlimit";
  // data field
  private static final String DATA_FIELD = "d";

  private final DataCodec dataCodec;
  private final boolean releaseDataOnEncode;

  /**
   * Constructor for codec which encode/decode client message to/from websocket gateway message
   * represented by json and transformed in {@link ByteBuf}.
   *
   * @param dataCodec data message codec.
   */
  public WebsocketGatewayClientCodec(DataCodec dataCodec) {
    this(dataCodec, true /*always release by default*/);
  }

  /**
   * Constructor for codec which encode/decode client message to/from websocket gateway message
   * represented by json and transformed in {@link ByteBuf}.
   *
   * @param dataCodec data message codec.
   * @param releaseDataOnEncode release data on encode flag.
   */
  public WebsocketGatewayClientCodec(DataCodec dataCodec, boolean releaseDataOnEncode) {
    this.dataCodec = dataCodec;
    this.releaseDataOnEncode = releaseDataOnEncode; // always release by default
  }

  @Override
  public DataCodec getDataCodec() {
    return dataCodec;
  }

  @Override
  public ByteBuf encode(ServiceMessage message) {
    ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();

    try (JsonGenerator generator =
        jsonFactory.createGenerator(
            (OutputStream) new ByteBufOutputStream(byteBuf), JsonEncoding.UTF8)) {
      generator.writeStartObject();

      // headers
      for (Entry header : message.headers().entrySet()) {
        String fieldName = header.getKey();
        String value = header.getValue();
        switch (fieldName) {
          case STREAM_ID_FIELD:
          case SIGNAL_FIELD:
          case INACTIVITY_FIELD:
          case RATE_LIMIT_FIELD:
            generator.writeNumberField(fieldName, Long.parseLong(value));
            break;
          default:
            generator.writeStringField(fieldName, value);
        }
      }

      // data
      Object data = message.data();
      if (data != null) {
        if (data instanceof ByteBuf) {
          ByteBuf dataBin = (ByteBuf) data;
          if (dataBin.isReadable()) {
            try {
              generator.writeFieldName(DATA_FIELD);
              generator.writeRaw(":");
              generator.flush();
              byteBuf.writeBytes(dataBin);
            } finally {
              if (releaseDataOnEncode) {
                ReferenceCountUtil.safestRelease(dataBin);
              }
            }
          }
        } else {
          generator.writeObjectField(DATA_FIELD, data);
        }
      }

      generator.writeEndObject();
    } catch (Throwable ex) {
      ReferenceCountUtil.safestRelease(byteBuf);
      Optional.ofNullable(message.data()).ifPresent(ReferenceCountUtil::safestRelease);
      LOGGER.error("Failed to encode message: {}", message, ex);
      throw new MessageCodecException("Failed to encode message", ex);
    }
    return byteBuf;
  }

  @Override
  public ServiceMessage decode(ByteBuf encodedMessage) {
    try (InputStream stream = new ByteBufInputStream(encodedMessage, true)) {
      JsonParser jp = jsonFactory.createParser(stream);
      ServiceMessage.Builder result = ServiceMessage.builder();

      JsonToken current = jp.nextToken();
      if (current != JsonToken.START_OBJECT) {
        throw new MessageCodecException("Root should be object", null);
      }
      long dataStart = 0;
      long dataEnd = 0;
      while ((jp.nextToken()) != JsonToken.END_OBJECT) {
        String fieldName = jp.getCurrentName();
        current = jp.nextToken();
        if (current == VALUE_NULL) {
          continue;
        }

        if (DATA_FIELD.equals(fieldName)) {
          dataStart = jp.getTokenLocation().getByteOffset();
          if (current.isScalarValue()) {
            if (!current.isNumeric() && !current.isBoolean()) {
              jp.getValueAsString();
            }
          } else if (current.isStructStart()) {
            jp.skipChildren();
          }
          dataEnd = jp.getCurrentLocation().getByteOffset();
        } else {
          // headers
          result.header(fieldName, jp.getValueAsString());
        }
      }
      // data
      if (dataEnd > dataStart) {
        result.data(encodedMessage.copy((int) dataStart, (int) (dataEnd - dataStart)));
      }
      return result.build();
    } catch (Throwable ex) {
      throw new MessageCodecException("Failed to decode message", ex);
    }
  }

  private static ObjectMapper objectMapper() {
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    mapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
    mapper.registerModule(new JavaTimeModule());
    return mapper;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy