sgammon/GUST

View on GitHub
java/gust/backend/model/ProtoModelCodec.java

Summary

Maintainability
A
0 mins
Test Coverage
/*
 * Copyright © 2020, The Gust Framework Authors. All rights reserved.
 *
 * The Gust/Elide framework and tools, and all associated source or object computer code, except where otherwise noted,
 * are licensed under the Zero Prosperity license, which is enclosed in this repository, in the file LICENSE.txt. Use of
 * this code in object or source form requires and implies consent and agreement to that license in principle and
 * practice. Source or object code not listing this header, or unless specified otherwise, remain the property of
 * Elide LLC and its suppliers, if any. The intellectual and technical concepts contained herein are proprietary to
 * Elide LLC and its suppliers and may be covered by U.S. and Foreign Patents, or patents in process, and are protected
 * by trade secret and copyright law. Dissemination of this information, or reproduction of this material, in any form,
 * is strictly forbidden except in adherence with assigned license requirements.
 */
package gust.backend.model;

import com.google.protobuf.Message;
import com.google.protobuf.TypeRegistry;
import com.google.protobuf.util.JsonFormat;
import gust.backend.runtime.Logging;
import org.slf4j.Logger;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Optional;


/**
 * Defines a {@link ModelCodec} which uses Protobuf serialization to export and import protos to and from from raw
 * byte-strings. These formats are built into Protobuf and are considered extremely reliable, even across languages.
 *
 * <p>Two formats of Protobuf serialization are supported:
 * <ul>
 *   <li><b>Binary:</b> Most efficient format. Best for production use. Completely illegible to humans.</li>
 *   <li><b>ProtoJSON:</b> Protocol Buffers-defined JSON translation protocol.</li>
 * </ul></p>
 *
 * @see ModelCodec Generic model codec interface.
 */
@Immutable
@ThreadSafe
public final class ProtoModelCodec<Model extends Message> implements ModelCodec<Model, EncodedModel, EncodedModel> {
  /** Default wire format mode. */
  private static final EncodingMode DEFAULT_FORMAT = EncodingMode.BINARY;

  /** Log pipe to use. */
  private static final Logger logging = Logging.logger(ProtoModelCodec.class);

  /** Protobuf wire format to use. */
  private final EncodingMode wireMode;

  /** Builder from which to spawn models. */
  private final Model instance;

  /** JSON printer utility, initialized when operating with `wireMode=JSON`. */
  private final @Nullable JsonFormat.Printer jsonPrinter;

  /** JSON parser utility, initialized when operating with `wireMode=JSON`. */
  private final @Nullable JsonFormat.Parser jsonParser;

  /** Serializer object. */
  private final @Nonnull ModelSerializer<Model, EncodedModel> serializer;

  /** De-serializer object. */
  private final @Nonnull ModelDeserializer<EncodedModel, Model> deserializer;

  /**
   * Private constructor. Use static factory methods.
   *
   * @see #forModel(Message) To spawn a proto-codec for a given model.
   * @param instance Model instance (empty) to use for type information.
   * @param mode Mode to apply to this codec instance.
   * @param registry Optional type registry of other types to use with {@link JsonFormat}.
   */
  private ProtoModelCodec(@Nonnull Model instance, @Nonnull EncodingMode mode, @Nullable TypeRegistry registry) {
    this.wireMode = mode;
    this.instance = instance;
    this.serializer = new ProtoMessageSerializer();
    this.deserializer = new ProtoMessageDeserializer();

    if (logging.isTraceEnabled())
      logging.trace(String.format("Initializing `ProtoModelCodec` with format %s.", mode.name()));

    if (mode == EncodingMode.JSON) {
      TypeRegistry resolvedRegisry = registry != null ? registry : TypeRegistry.newBuilder()
        .add(instance.getDescriptorForType())
        .build();

      this.jsonParser = JsonFormat.parser()
        .usingTypeRegistry(resolvedRegisry);

      this.jsonPrinter = JsonFormat.printer()
        .usingTypeRegistry(resolvedRegisry)
        .sortingMapKeys()
        .omittingInsignificantWhitespace();

    } else {
      this.jsonParser = null;
      this.jsonPrinter = null;
    }
  }

  // -- Factories -- //

  /**
   * Acquire a Protobuf model codec for the provided model instance. The codec will operate in the default
   * {@link EncodingMode} unless specified otherwise via the other method variants on this object.
   *
   * @param <M> Model instance type.
   * @param instance Model instance to return a codec for.
   * @return Model codec which serializes and de-serializes to/from Protobuf wire formats.
   */
  @SuppressWarnings({"WeakerAccess", "unused"})
  public @Nonnull static <M extends Message> ProtoModelCodec<M> forModel(@Nonnull M instance) {
    return forModel(instance, DEFAULT_FORMAT);
  }

  /**
   * Acquire a Protobuf model codec for the provided model instance. The codec will operate in the default
   * {@link EncodingMode} unless specified otherwise via the other method variants on this object.
   *
   * @param <M> Model instance type.
   * @param instance Model instance to return a codec for.
   * @param mode Wire format mode to operate in (one of {@code JSON} or {@code BINARY}).
   * @return Model codec which serializes and de-serializes to/from Protobuf wire formats.
   */
  public @Nonnull static <M extends Message> ProtoModelCodec<M> forModel(@Nonnull M instance,
                                                                         @Nonnull EncodingMode mode) {
    return forModel(instance, mode, Optional.empty());
  }

  /**
   * Acquire a Protobuf model codec for the provided model instance. The codec will operate in the default
   * {@link EncodingMode} unless specified otherwise via the other method variants on this object.
   *
   * @param <M> Model instance type.
   * @param instance Model instance to return a codec for.
   * @param mode Wire format mode to operate in (one of {@code JSON} or {@code BINARY}).
   * @return Model codec which serializes and de-serializes to/from Protobuf wire formats.
   */
  @SuppressWarnings("WeakerAccess")
  public @Nonnull static <M extends Message> ProtoModelCodec<M> forModel(@Nonnull M instance,
                                                                         @Nonnull EncodingMode mode,
                                                                         @Nonnull Optional<TypeRegistry> registry) {
    return new ProtoModelCodec<>(
      instance,
      mode,
      registry.orElse(null));
  }

  /** Serializes model instances into raw bytes, according to Protobuf wire protocol semantics. */
  private final class ProtoMessageSerializer implements ModelSerializer<Model, EncodedModel> {
    /**
     * Serialize a model instance from the provided object type to the specified output type, throwing exceptions
     * verbosely if we are unable to correctly, verifiably, and properly export the record.
     *
     * @param input Input record object to serialize.
     * @return Serialized record data, of the specified output type.
     * @throws ModelDeflateException If the model fails to export or serialize for any reason.
     */
    @Override
    public @Nonnull EncodedModel deflate(@Nonnull Message input) throws ModelDeflateException, IOException {
      if (logging.isDebugEnabled())
        logging.debug(String.format(
          "Deflating record of type '%s' with format %s.",
          input.getDescriptorForType().getFullName(),
          wireMode.name()));

      if (wireMode == EncodingMode.BINARY) {
        return EncodedModel.wrap(
          input.getDescriptorForType().getFullName(),
          wireMode,
          input.toByteArray());
      } else {
        return EncodedModel.wrap(
          input.getDescriptorForType().getFullName(),
          wireMode,
          Objects.requireNonNull(jsonPrinter).print(input).getBytes(StandardCharsets.UTF_8));
      }
    }
  }

  /** De-serializes model instances from raw bytes, according to Protobuf wire protocol semantics. */
  private final class ProtoMessageDeserializer implements ModelDeserializer<EncodedModel, Model> {
    /**
     * De-serialize a model instance from the provided input type, throwing exceptions verbosely if we are unable to
     * correctly, verifiably, and properly load the record.
     *
     * @param data Input data or object from which to load the model instance.
     * @return De-serialized and inflated model instance. Always a {@link Message}.
     * @throws ModelInflateException If the model fails to load for any reason.
     */
    @Override
    public @Nonnull Model inflate(@Nonnull EncodedModel data) throws ModelInflateException, IOException {
      if (logging.isDebugEnabled())
        logging.debug(String.format(
          "Inflating record of type '%s' with format %s.",
          data.getType(),
          wireMode.name()));

      if (wireMode == EncodingMode.BINARY) {
        //noinspection unchecked
        return (Model)instance.newBuilderForType().mergeFrom(data.getRawBytes().toByteArray()).build();
      } else {
        Message.Builder builder = instance.newBuilderForType();
        Objects.requireNonNull(jsonParser).merge(
          data.getRawBytes().toStringUtf8(),
          builder);

        //noinspection unchecked
        return (Model)builder.build();  // need to install proto JSON
      }
    }
  }

  // -- API: Codec -- //
  /** @inheritDoc */
  @Override
  public @Nonnull Model instance() {
    return instance;
  }

  /**
   * Acquire an instance of the {@link ModelSerializer} attached to this adapter. The instance is not guaranteed to be
   * created fresh for this invocation.
   *
   * @return Serializer instance.
   * @see #deserializer() For the inverse of this method.
   * @see #deserialize(Object) To call into de-serialization directly.
   */
  @Override
  public @Nonnull ModelSerializer<Model, EncodedModel> serializer() {
    return this.serializer;
  }

  /**
   * Acquire an instance of the {@link ModelDeserializer} attached to this adapter. The instance is not guaranteed to be
   * created fresh for this invocation.
   *
   * @return Deserializer instance.
   * @see #serializer() For the inverse of this method.
   * @see #serialize(Message) To call into serialization directly.
   */
  @Override
  public @Nonnull ModelDeserializer<EncodedModel, Model> deserializer() {
    return this.deserializer;
  }
}