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

on.avro.axon-avro-serializer.0.0.5.source-code.AvroSerializer.kt Maven / Gradle / Ivy

package io.holixon.axon.avro.serializer

import io.holixon.avro.adapter.api.AvroAdapterApi.schemaResolver
import io.holixon.avro.adapter.api.AvroSchemaIncompatibilityResolver
import io.holixon.avro.adapter.api.AvroSchemaReadOnlyRegistry
import io.holixon.avro.adapter.api.AvroSingleObjectEncoded
import io.holixon.avro.adapter.api.converter.GenericDataRecordToSpecificRecordConverter
import io.holixon.avro.adapter.api.decoder.SingleObjectToSpecificRecordDecoder
import io.holixon.avro.adapter.api.ext.ByteArrayExt.toHexString
import io.holixon.avro.adapter.common.AvroAdapterDefault
import io.holixon.avro.adapter.common.DefaultSchemaStore
import io.holixon.avro.adapter.common.converter.DefaultGenericDataRecordToSpecificRecordChangingSchemaConverter
import io.holixon.avro.adapter.common.converter.SchemaResolutionSupport
import io.holixon.avro.adapter.common.decoder.DefaultSingleObjectToSpecificRecordDecoder
import io.holixon.axon.avro.serializer.converter.AvroSingleObjectEncodedToGenericDataRecordTypeConverter
import io.holixon.axon.avro.serializer.converter.GenericDataRecordToAvroSingleObjectEncodedConverter
import io.holixon.axon.avro.serializer.ext.TypeExt.isUnknown
import io.holixon.axon.avro.serializer.fn.SchemaTypeSerializer
import io.holixon.axon.avro.serializer.revision.SchemaBasedRevisionResolver
import org.apache.avro.generic.GenericData
import org.apache.avro.specific.SpecificRecordBase
import org.apache.avro.util.ClassUtils
import org.axonframework.messaging.MetaData
import org.axonframework.serialization.*
import org.axonframework.serialization.json.JacksonSerializer
import org.axonframework.serialization.upcasting.event.IntermediateEventRepresentation
import org.slf4j.Logger
import org.slf4j.LoggerFactory

/**
 * Serializer implementation that uses Avro Single Object format for serialization.
 */
class AvroSerializer private constructor(
  /**
   * Gets the revision from class. Only works for SpecificRecordBase, GenericData.Record does not have schema on class.
   */
  private val revisionResolver: RevisionResolver,

  /**
   * Serializer for GenericData.Record.
   */
  private val genericDataRecordSerializer: SchemaTypeSerializer,

  /**
   * Serializer for SpecificRecordBase.
   */
  private val specificRecordSerializer: SchemaTypeSerializer,

  /**
   * Serializer for [MetaData].
   */
  @Deprecated(message = "metaData should also be based on schema, this is just a temp. hack")
  private val metaDataSerializer: Serializer,

  /**
   * Axon converters, typically this is a [ChainingConverter] that contains SPI converters for base types and specific [GenericData.Record] converters for [IntermediateEventRepresentation] during upcasting.
   */
  private val converter: Converter,

  /**
   * Pass a logger instance to trace processing.
   */
  private val logger: Logger,

  /**
   * Converts a generic record to a specific record.
   */
  private val genericDataRecordToSpecificRecordConverter: GenericDataRecordToSpecificRecordConverter,

  /**
   * Decode singleOnject bytes to specific record.
   */
  private val specificRecordDecoder: SingleObjectToSpecificRecordDecoder
) : Serializer {

  companion object {

    /**
     * Creates a [Builder] instance to configure the serializer.
     */
    @JvmStatic
    fun builder() = Builder()

    /**
     * Instantiate a default [AvroSerializer].
     *
     * The [RevisionResolver] is defaulted to a [SchemaBasedRevisionResolver], the [Converter] to a [ChainingConverter],
     * which is then by initialized by registering the converters for SpecificRecord, GenericData.Record and ByteArray.
     *
     * @return a [AvroSerializer]
     */
    @JvmStatic
    fun defaultSerializer(): AvroSerializer = builder().build()

    /**
     * Secondary constructor.
     */
    operator fun invoke(builder: Builder): AvroSerializer {
      val schemaResolver = builder.schemaReadOnlyRegistry.schemaResolver()
      val schemaResolutionSupport = SchemaResolutionSupport(
        schemaResolver,
        builder.decoderSpecificRecordClassResolver,
        builder.schemaIncompatibilityResolver
      )

      val converter = if (builder.converter is ChainingConverter) {
        // use GenericData.Record as intermediate representation.
        // register ByteArray to GenericData.Record converter
        // register GenericData.Record to ByteArray converter
        (builder.converter as ChainingConverter).apply {
          this.registerConverter(AvroSingleObjectEncodedToGenericDataRecordTypeConverter(schemaResolver))
          this.registerConverter(GenericDataRecordToAvroSingleObjectEncodedConverter())
        }
      } else {
        builder.converter
      }

      return AvroSerializer(
        revisionResolver = builder.revisionResolver,
        genericDataRecordSerializer = SchemaTypeSerializer.genericRecordDataSerializer(converter),
        specificRecordSerializer = SchemaTypeSerializer.specificRecordDataSerializer(converter),
        metaDataSerializer = JacksonSerializer.defaultSerializer(),
        converter = converter,
        logger = LoggerFactory.getLogger(AvroSerializer::class.java),
        genericDataRecordToSpecificRecordConverter = DefaultGenericDataRecordToSpecificRecordChangingSchemaConverter(schemaResolutionSupport),
        specificRecordDecoder = DefaultSingleObjectToSpecificRecordDecoder(
          schemaStore = DefaultSchemaStore(schemaResolver),
          schemaResolutionSupport = schemaResolutionSupport
        )
      )
    }
  }

  override fun classForType(type: SerializedType): Class<*> = if (SimpleSerializedType.emptyType() == type) {
    Void::class.java
  } else {
    try {
      // if the class can not be found, it is unknown

      // FIXME: here we do just class for name ... while in the converter we have a specific schema compatibility check ... is that correct?
      ClassUtils.forName(type.name)
    } catch (e: ClassNotFoundException) {
      UnknownSerializedType::class.java
    }
  }

  override fun typeForClass(type: Class<*>?): SerializedType {

    if (type == null || Void.TYPE == type || Void::class.java == type) {
      return SimpleSerializedType.emptyType()
    }
    // FIXME; checked with "is SpecificRecord" ... not against type
//    else if (type ==  SpecificRecordBase::class.java) {
//      val schema = type.schema.find(schemaReadOnlyRegistry = schemaReadOnlyRegistry)
//      return SchemaSerializedType(schema)
//    } else if (type is GenericData.Record) {
//      val schema = type.schema.find(schemaReadOnlyRegistry = schemaReadOnlyRegistry)
//      return SchemaSerializedType(schema)
//    }
    else {
      return SimpleSerializedType(type.name, revisionResolver.revisionOf(type))
    }
  }

  override fun getConverter(): Converter = converter

  /*
   * Support GenericDataRecord as intermediate format (see registered converters for them)
   */
  override fun  canSerializeTo(expectedRepresentation: Class): Boolean =
    GenericData.Record::class.java == expectedRepresentation ||
      converter.canConvert(AvroSingleObjectEncoded::class.java, expectedRepresentation)

  override fun  serialize(data: Any, expectedRepresentation: Class): SerializedObject = when (data) {
    is MetaData -> metaDataSerializer.serialize(data, expectedRepresentation)
    is GenericData.Record -> genericDataRecordSerializer.serialize(data, expectedRepresentation)
    is SpecificRecordBase -> specificRecordSerializer.serialize(data, expectedRepresentation)
    else -> throw IllegalArgumentException("cannot serialize $data to $expectedRepresentation")
  }


  override fun  deserialize(serializedObject: SerializedObject): T? {
    if (SerializedType.emptyType() == serializedObject.type) {
      return null
    }

    val type: Class<*> = classForType(serializedObject.type)
    if (type == MetaData::class.java) {
      return metaDataSerializer.deserialize(serializedObject)
    } else if (type.isUnknown()) {
      @Suppress("UNCHECKED_CAST")
      return UnknownSerializedType(this, serializedObject) as T
    }

    return try {
      @Suppress("UNCHECKED_CAST")
      when (serializedObject.contentType) {
        GenericData.Record::class.java -> {
          // we run into this branch if the byte array was converted and manipulated on the level of the intermediate representation
          // in this case the format is GenericData.Record
          (genericDataRecordToSpecificRecordConverter.convert(serializedObject.data as GenericData.Record) as T)
            .apply {
              logger.debug("deserialized: ${(serializedObject.data as GenericData.Record)} to $this")
            }
        }
        else -> {
          val bytesSerialized = converter.convert(serializedObject, AvroSingleObjectEncoded::class.java)
          (specificRecordDecoder.decode(bytesSerialized.data) as T)
            .apply {
              logger.debug("deserialized: ${(bytesSerialized.data as ByteArray).toHexString()} to $this")
            }
        }
      }
    } catch (e: Exception) {
      throw SerializationException("Error while deserializing object", e)
    }
  }

  /**
   * Builder for the resolver.
   */
  class Builder {
    /**
     * The Revision resolver to determine (schema) revision of message.
     * @default [SchemaBasedRevisionResolver]
     */
    var revisionResolver: RevisionResolver = SchemaBasedRevisionResolver()
      private set

    /**
     * The axon converter.
     * @default [ChainingConverter] using classloader SPI.
     */
    var converter: Converter = ChainingConverter()
      private set

    /**
     * Access to schemas.
     * @default [io.holixon.avro.adapter.common.registry.InMemoryAvroSchemaReadOnlyRegistry].
     */
    var schemaReadOnlyRegistry: AvroSchemaReadOnlyRegistry = AvroAdapterDefault.inMemorySchemaRegistry()
      private set

    /**
     * Resolves class for schema.
     * @default [AvroAdapterDefault.reflectionBasedDecoderSpecificRecordClassResolver] - using [ClassUtils.forName].
     */
    var decoderSpecificRecordClassResolver: AvroAdapterDefault.DecoderSpecificRecordClassResolver =
      AvroAdapterDefault.reflectionBasedDecoderSpecificRecordClassResolver
      private set

    /**
     * How to determine if the writer schema encoded in the stored bytes is compatible to the reader schema found on the class path.
     * @default [AvroAdapterDefault.defaultSchemaCompatibilityResolver]
     */
    var schemaIncompatibilityResolver: AvroSchemaIncompatibilityResolver = AvroAdapterDefault.defaultSchemaCompatibilityResolver
      private set

    fun revisionResolver(revisionResolver: RevisionResolver) = apply {
      this.revisionResolver = revisionResolver
    }

    /**
     * Sets the converter.
     */
    fun converter(converter: Converter) = apply {
      this.converter = converter
    }

    /**
     * Sets the (read-only) schema registry.
     */
    fun schemaRegistry(schemaReadOnlyRegistry: AvroSchemaReadOnlyRegistry) = apply {
      this.schemaReadOnlyRegistry = schemaReadOnlyRegistry
    }

    /**
     * Sets class resolver for specific record.
     */
    fun decoderSpecificRecordClassResolver(decoderSpecificRecordClassResolver: AvroAdapterDefault.DecoderSpecificRecordClassResolver) =
      apply {
        this.decoderSpecificRecordClassResolver = decoderSpecificRecordClassResolver
      }

    /**
     * Sets resolver for schema incompatibilities.
     */
    fun schemaIncompatibilityResolver(schemaIncompatibilityResolver: AvroSchemaIncompatibilityResolver) = apply {
      this.schemaIncompatibilityResolver = schemaIncompatibilityResolver
    }

    /**
     * Build the serializer.
     */
    fun build() = AvroSerializer(this)
  }

}