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

com.pagerduty.mapper.InheritanceMapping.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2015, PagerDuty
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted
 * provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
 * conditions and the following disclaimer in the documentation and/or other materials provided with
 * the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to
 * endorse or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.pagerduty.mapper

import java.lang.annotation.Annotation
import org.slf4j.LoggerFactory


/**
 * Maps single table inheritance (STI) using discriminator column.
 *
 * The target class must be annotated as @Superclass(subclasses). Only provided subclasses can
 * be used with the inheritance mapping. Each subclass must have @Discriminator annotation.
 * @Superclass annotation will be ignore for all the subclasses, so it is possible to have
 * multiple inheritance mappings that service different inheritance subtrees.
 */
private[mapper] class InheritanceMapping(
    val target: Class[_],
    val name: Option[String],
    val ttlSeconds: Option[Int],
    val registeredSerializers: Map[Class[_], Any],
    val customMappers: Map[Class[_ <: Annotation], Mapping => Mapping])
  extends UntypedEntityMapping
{
  import UntypedEntityMapping._
  import InheritanceMapping.log

  /**
   * ClassMapping for each subclass.
   */
  protected val mappingByClassName: Map[String, (String, ClassMapping)] = {
    def classMapping(subclass: Class[_]): (String, ClassMapping) = {
      val discriminatorAnnotation = subclass.getAnnotation(DiscriminatorAnnotationClass)
      if (discriminatorAnnotation == null) {
        throw new EntityMapperException(s"Class ${subclass.getName} " +
          s"must have @Discriminator annotation, because it is a part of ${target.getName} " +
          "inheritance mapping.")
      }
      val discriminator = discriminatorAnnotation.value
      val ttlOp = Option(target.getAnnotation(TtlAnnotationClass)).map(_.seconds)
      val classMapping = new ClassMapping(
        subclass, true, name, ttlOp, registeredSerializers, customMappers)
      (discriminator, classMapping)
    }

    target.getAnnotation(SuperclassAnnotationClass).subclasses.map { subclass =>
      subclass.getName -> classMapping(subclass)
    }.toMap
  }
  protected val mappingByDiscriminator: Map[String, ClassMapping] = {
    val classesByDiscriminator = mappingByClassName.toSeq
      .groupBy { case (_, (disciminator, _)) => disciminator }
      .mapValues(_.map { case (clazz, _) => clazz })

    classesByDiscriminator.find { case (discriminator, classes) => classes.size > 1 } match {
      case Some((discriminator, classes)) =>
        throw new EntityMapperException(s"Classes ${classes.mkString(", ")} " +
          s"have the same @Discriminator value '$discriminator'.")
      case None =>
        // ignore
    }

    mappingByClassName.map { case (_, (disciminator, mapping)) => disciminator -> mapping }
  }
  protected val allMappings: Seq[ClassMapping] = mappingByDiscriminator.values.toSeq

  protected def mappingFor(clazz: Class[_]): (String, ClassMapping) = {
    val className = if (clazz.getName.endsWith("$")) clazz.getSuperclass.getName else clazz.getName
    mappingByClassName.getOrElse(className, throw new EntityMapperException(
      s"Class ${clazz.getName} is not part of the ${target.getName} inheritance mapping."))
  }

  protected val discriminatorMapping: FieldMapping = {
    val discriminatorColumn = target.getAnnotation(SuperclassAnnotationClass).discriminatorColumn
    val stringSerializer = registeredSerializers(classOf[String])
    new FieldMapping(stringSerializer, prefixed(discriminatorColumn))
  }


  val isIdDefined: Boolean = allMappings.forall(_.isIdDefined)
  def getId(entity: Any): Any = mappingFor(entity.getClass)._2.getId(entity)
  def setId(entity: Any, id: Any): Unit = mappingFor(entity.getClass)._2.setId(entity, id)

  val serializersByColName: Seq[(String, Any)] = {
    val schema = allMappings.flatMap(_.serializersByColName).toSet
    val reserved = schema.find { case (colName, _) => colName == discriminatorMapping.name }
    if (reserved.isDefined) {
      throw new EntityMapperException(s"Column name '${reserved.get._1}' " +
        s"is reserved as discriminator column name in ${target.getName} inheritance mapping.")
    }
    validateSchema(target, schema)
    schema.toSeq ++ discriminatorMapping.serializersByColName
  }

  def write(
      targetId: Any, entity: Option[Any], mutation: MutationAdapter, ttlSeconds: Option[Int])
  : Unit = {
    if (entity.isDefined) {
      val (discriminator, mapping) = mappingFor(entity.get.getClass)
      // Remove all the columns from other mappings.
      allMappings.filterNot(_ == mapping).foreach {
        _.write(targetId, None, mutation, mapping.fieldNames, None)
      }
      // Write discriminator.
      discriminatorMapping.write(targetId, Some(discriminator), mutation, ttlSeconds)
      // Write mapping.
      mapping.write(targetId, entity, mutation, ttlSeconds)
    }
    else {
      discriminatorMapping.write(targetId, None, mutation, None)
      allMappings.foreach(_.write(targetId, None, mutation, None))
    }
  }

  def read(targetId: Any, result: ResultAdapter): MappedResult = {
    discriminatorMapping.read(targetId, result).flatMap { case discriminator: String =>
      mappingByDiscriminator.get(discriminator).map(_.readDefined(targetId, result)).getOrElse {
        log.error(s"Not found mapping for discriminator '$discriminator' " +
          s"for ${target.getName} inheritance mapping for entity " +
          s"'$targetId'.")
        Undefined
      }
    }
  }

  override def toString(): String = s"InheritanceMapping(${target.getName}, $name)"
}


private[mapper] object InheritanceMapping {
  val log = LoggerFactory.getLogger(classOf[InheritanceMapping])
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy