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

com.github.gabadi.scalajooq.JooqMeta.scala Maven / Gradle / Ivy

The newest version!
package com.github.gabadi.scalajooq

import com.google.common.base.CaseFormat.{LOWER_CAMEL, UPPER_CAMEL, UPPER_UNDERSCORE}
import org.jooq.impl.TableImpl
import org.jooq.{DSLContext, Field, Record, RecordMapper, Table}

import scala.language.experimental.macros
import scala.reflect.macros.TypecheckException
import scala.reflect.macros.blackbox.Context

trait JooqMeta[T <: Table[R], R <: Record, E] extends RecordMapper[Record, E] {

  val table: T

  lazy val selectTable = joinedTable(table.asInstanceOf[Table[Record]])

  def joinedTable(current: Table[Record], leftJoin: Boolean = false): Table[Record]

  lazy val fields = selectTable.fields()

  lazy val aliasedFields = fields.map(f => withAlias(f))

  def withAlias[G](field: org.jooq.Field[G]): org.jooq.Field[G] = field.as(field.toString)

  override def map(record: Record): E = toEntity(record)

  def toEntity(record: Record): E = toEntityAliased(record, false)

  def toEntityAliased(record: Record, aliased: Boolean = true): E

  def toOptEntity[G >: R <: Record](record: Record): Option[E] = toOptEntityAliased(record, false)

  def toOptEntityAliased[G >: R <: Record](record: Record, aliased: Boolean = true): Option[E] = {
    val isNull = (if (aliased) aliasedFields else fields).forall(f => null == record.getValue(f))
    if (isNull) None else Some(toEntityAliased(record, aliased))
  }

  def toRecord(e: E, current: R = null.asInstanceOf[R])(implicit dsl: DSLContext): R

  def query(implicit dsl: DSLContext) = dsl.select(fields: _*).from(selectTable)

}

object JooqMeta {

  def metaOf[T <: TableImpl[R], R <: Record, E]: JooqMeta[T, R, E] =
  macro materializeRecordMapperImpl[T, R, E]


  def namespacedMetaOf[T <: TableImpl[R], R <: Record, E](namespace: String): JooqMeta[T, R, E] =
  macro materializeNamespacedRecordMapperImpl[T, R, E]

  def materializeRecordMapperImpl[T <: TableImpl[R] : c.WeakTypeTag, R <: Record : c.WeakTypeTag, E: c.WeakTypeTag](c: Context): c.Expr[JooqMeta[T, R, E]] = {
    createMetaImpl[T, R, E](c, "")
  }


  def materializeNamespacedRecordMapperImpl[T <: TableImpl[R] : c.WeakTypeTag, R <: Record : c.WeakTypeTag, E: c.WeakTypeTag](c: Context)(namespace: c.Expr[String]): c.Expr[JooqMeta[T, R, E]] = {
    createMetaImpl[T, R, E](c, c.eval(namespace))
  }


  def createMetaImpl[T <: TableImpl[R] : c.WeakTypeTag, R <: Record : c.WeakTypeTag, E: c.WeakTypeTag](c: Context, namespace: String): c.Expr[JooqMeta[T, R, E]] = {
    import c.universe._

    def canNotFindMapIdFieldBetween(table: c.universe.Type, entityMethod: String) = {
      c.abort(c.enclosingPosition,
        s"""
           |Mappings error between:
           |can not map $entityMethod with table $table
           """.stripMargin)
    }

    def implicitConversion(from: Type, to: Type) = {
      val r = c.inferImplicitValue(
        appliedType(typeOf[(_) => _].typeConstructor, from, to))
      if (r.equalsStructure(EmptyTree)) {
        c.abort(c.enclosingPosition,
          s"""
             |Mappings error between:
             |can not find an implicit conversion from $from to $to
           """.stripMargin)
      }
      r
    }

    def assertIsJooqMeta(mapper: c.universe.Tree) = {
      if (!mapper.tpe.typeSymbol.equals(symbolOf[JooqMeta[_, _, _]])) {
        c.abort(c.enclosingPosition,
          s"""
             |Mappings error between:
             |$mapper must be an instance of ${weakTypeOf[JooqMeta[_, _, _]]}
           """.stripMargin)
      }
    }

    def abortCanNotFindImplicitConversion(from: String, to: String) = {
      c.abort(c.enclosingPosition,
        s"""
           |Mappings error between:
           |Can not find an implicit conversion between $from and $to
           """.stripMargin)
    }

    def abortFieldCanNotBeMapped(field: String, tree: c.universe.Tree, message: String) = {
      c.abort(c.enclosingPosition,
        s"""
           |Mappings error:
           |Can not create the mapping for $field
            |Tried to: $tree
            |But failed with: $message
           """.stripMargin)
    }

    def abortFieldNotFoundInRecord(entity: String, record: String) = {
      c.abort(c.enclosingPosition,
        s"""
           |Mappings error:
           |$entity expects a $record column, but doesn't exists
             """.stripMargin)
    }


    def checkCaseClass(symbol: c.universe.ClassSymbol) = {
      if (!symbol.isClass || !symbol.isCaseClass)
        c.abort(c.enclosingPosition, "Can only map case classes.")
    }

    def caseClassFields(caseClass: c.universe.Type) = {
      checkCaseClass(caseClass.typeSymbol.asClass)
      val declarations = caseClass.decls
      val ctor = declarations.collectFirst {
        case m: MethodSymbol if m.isPrimaryConstructor => m
      }.get
      ctor.paramLists.head
    }

    def tableMemberToName(symbol: c.universe.Symbol) = {
      val name = UPPER_UNDERSCORE.to(LOWER_CAMEL, symbol.name.decodedName.toString)
      if (name.startsWith(namespace)) UPPER_CAMEL.to(LOWER_CAMEL, name.drop(namespace.length)) else name
    }

    def tableMembersMap(tableType: c.universe.Type) = tableType.members
      .filter { f =>
      val name = f.name.decodedName.toString
      name.toUpperCase.equals(name)
    }.map(m => tableMemberToName(m) -> m).toMap

    def recordMembersMap(recordType: c.universe.Type) = recordType.members
      .map(m => m.name.decodedName.toString -> m)
      .filter { case (n, m) =>
      n.startsWith("set") && m.asMethod.paramLists.head.size == 1
    }.toMap

    def tableInstanceMethod(tableType: c.universe.Type) = {
      val tableCompanion = tableType.typeSymbol.companion
      val companionMethod = TermName(LOWER_CAMEL.to(UPPER_UNDERSCORE, tableType.typeSymbol.name.decodedName.toString))
      q"$tableCompanion.$companionMethod"
    }

    val packag = q"com.github.gabadi.scalajooq"
    val jooqMeta = q"$packag.JooqMeta"
    def checkNotNullTree(tree: Tree, message: String) = q"$packag.Constraints.checkNotNull($tree, $message)"


    val tableType = weakTypeOf[T]
    val entityType = weakTypeOf[E]
    val recordType = weakTypeOf[R]

    val fields = caseClassFields(entityType)
    val table = tableInstanceMethod(tableType)

    val tableMembers = tableMembersMap(tableType)
    val recordMembers = recordMembersMap(recordType)

    val (toEntityParams, toRecordParams, mappedFields, joins) = fields.map { field =>
      val fieldName = field.name.decodedName.toString
      val fieldTermName = field.asTerm.name
      val columnName = LOWER_CAMEL.to(UPPER_UNDERSCORE, fieldName)
      val fieldIsOption = field.typeSignature <:< typeOf[Option[_]]
      val effectiveFieldType = if (fieldIsOption) field.typeSignature.typeArgs.head else field.typeSignature
      tableMembers.get(fieldName) match {
        // direct matching between record and entity
        case Some(recordMember) =>
          val recordSetter = recordMembers.get(s"set${namespace.capitalize}${fieldName.capitalize}").get

          val recordFieldType = recordSetter.asMethod.paramLists.head.head.typeSignature

          val e2rTypeConversion = implicitConversion(from = effectiveFieldType, to = recordFieldType)
          val r2eTypeConversion = implicitConversion(from = recordFieldType, to = effectiveFieldType)

          val getMaybeAliasedValue = q"if(aliased) r.getValue(withAlias(table.$recordMember)) else r.getValue(table.$recordMember)"

          if ((r2eTypeConversion equalsStructure EmptyTree) || (e2rTypeConversion equalsStructure EmptyTree)) {
            abortCanNotFindImplicitConversion(s"$entityType.$fieldName($effectiveFieldType)", s"$recordType.$columnName($recordFieldType)")
          } else {
            if (fieldIsOption) {
              (q"$fieldTermName = Option($getMaybeAliasedValue).map($r2eTypeConversion)",
                q"r.$recordSetter(e.$fieldTermName.map($e2rTypeConversion).orNull[$recordFieldType])",
                q"f = f ++ Array($table.$recordMember)",
                Nil)
            } else {
              val nullInRecordMessage = s"${recordMember.name.decodedName.toString} in record ${recordType.typeSymbol.name.decodedName.toString} is null in the database. This is inconsistent"
              val nullInEntityMessage = s"$fieldName in entity ${entityType.typeSymbol.name.decodedName.toString} must not be null"
              val entityFieldConverted = q"$e2rTypeConversion(e.$fieldTermName)"
              (q"$fieldTermName = $r2eTypeConversion(${checkNotNullTree(getMaybeAliasedValue, nullInRecordMessage)})",
                q"r.$recordSetter(${checkNotNullTree(entityFieldConverted, nullInEntityMessage)})",
                q"f = f ++ Array($table.$recordMember)",
                Nil)
            }
          }
        case None =>
          // there is no matching between record and entity
          val implicitMapper = {
            val expectedImplicitMapperType = appliedType(typeOf[RecordMapper[_, _]].typeConstructor, typeOf[Record], effectiveFieldType)
            val implicitMapper = c.inferImplicitValue(expectedImplicitMapperType)
            // exists an implicit JooqMeta
            if (!implicitMapper.equalsStructure(EmptyTree)) {
              assertIsJooqMeta(implicitMapper)
              implicitMapper
            } else {
              // try to resolve like an embedded entity
              val newNamespace = s"$namespace${if (namespace.isEmpty) fieldName else fieldName.capitalize}"
              val newNamespaceUpper = LOWER_CAMEL.to(UPPER_UNDERSCORE, newNamespace)
              val mayExistNamespace = tableType.members.exists(_.name.decodedName.toString.startsWith(newNamespaceUpper))

              if (!mayExistNamespace) {
                abortFieldNotFoundInRecord(s"$entityType.$fieldName", s"$recordType.$newNamespaceUpper")
              } else {
                val tree = q"""$jooqMeta.namespacedMetaOf[$tableType, $recordType, $effectiveFieldType]($newNamespace)"""
                try {
                  c.typecheck(tree = tree).tpe
                } catch {
                  case e: TypecheckException =>
                    abortFieldCanNotBeMapped(s"$entityType.$fieldName", tree, e.getMessage)
                }
                tree
              }
            }
          }

          val mapperRecordType = c.typecheck(implicitMapper).tpe.typeArgs(1)

          val toEntity = if (fieldIsOption) q"$implicitMapper.${TermName("toOptEntityAliased")}" else q"$implicitMapper.${TermName("toEntityAliased")}"

          // is an embedded entity
          if (mapperRecordType.equals(recordType)) {
            val toRecord = q"$implicitMapper.${TermName("toRecord")}"

            (q"$field = $toEntity(r, aliased)",
              if (fieldIsOption) {
                q"e.${TermName(fieldName)}.foreach(o => $toRecord(o, r))"
              } else {
                q"$toRecord(e.${TermName(fieldName)}, r)"
              },
              q"f = f ++ $implicitMapper.${TermName("fields")}",
              Nil)
          } else {
            // try to resolve like a joined entity
            val idSuffix = {
              val joinedTableType = implicitMapper.tpe.typeArgs.head
              val maybeMappedMethods = tableMembers.keySet.filter(f => f.startsWith(fieldName))
              if (maybeMappedMethods.isEmpty) {
                canNotFindMapIdFieldBetween(joinedTableType, s"$entityType.$fieldName")
              } else if (maybeMappedMethods.size == 1) {
                val suffix = maybeMappedMethods.head.replaceFirst(fieldName, "")
                s"${suffix.substring(0, 0).toLowerCase}${suffix.substring(1, suffix.length)}"
              } else {
                if (maybeMappedMethods.exists(_.equals(fieldName + "Id"))) {
                  "id"
                } else if (maybeMappedMethods.exists(_.equals(fieldName + "Oid"))) {
                  "oid"
                } else if (maybeMappedMethods.exists(_.equals(fieldName + "Code"))) {
                  "code"
                } else {
                  // improve message
                  canNotFindMapIdFieldBetween(joinedTableType, s"$entityType.$fieldName")
                }

              }
            }
            val tableFieldName = LOWER_CAMEL.to(UPPER_UNDERSCORE, s"$fieldName${idSuffix.capitalize}")
            val joinTableFieldName = LOWER_CAMEL.to(UPPER_UNDERSCORE, s"$idSuffix")
            val recordSetter = recordMembers.get(s"set${fieldName.capitalize}${idSuffix.capitalize}").get
            val fieldTypeIdMember = effectiveFieldType.member(TermName(idSuffix))
            val fieldTypeIdType = fieldTypeIdMember.asMethod.returnType
            val recordSetterType = recordSetter.typeSignature.paramLists.head.head.typeSignature
            val e2rTypeConversion = implicitConversion(from = fieldTypeIdType, to = recordSetterType)
            val nullInEntityMessage = s"$fieldName in entity ${entityType.typeSymbol.name.decodedName.toString} must not be null"
            val entityFieldConverted = q"$e2rTypeConversion(e.$fieldTermName.$fieldTypeIdMember)"

            val joinCondition = q"table.${TermName(tableFieldName)}.equal($implicitMapper.table.${TermName(joinTableFieldName)})"

            val join = if (fieldIsOption) {
              q"(current leftOuterJoin $implicitMapper.table).on($joinCondition)"
            } else {
              q"(if(leftJoin) (current leftOuterJoin $implicitMapper.table).on($joinCondition) else (current join $implicitMapper.table).on($joinCondition))"
            }
            val leftJoin = if (fieldIsOption) q"true" else q"leftJoin"

            (q"$field = $toEntity(r, aliased)",
              if (fieldIsOption) {
                q"e.${TermName(fieldName)}.foreach(o => r.$recordSetter($e2rTypeConversion(o.$fieldTypeIdMember)))"
              } else {
                q"r.$recordSetter(${checkNotNullTree(entityFieldConverted, nullInEntityMessage)})"
              },
              q"f = f ++ $implicitMapper.${TermName("fields")} ++ Array(table.${TermName(tableFieldName)})",
              q"current = $join" ::
                q"current = ($implicitMapper).joinedTable(current, $leftJoin)" :: Nil
              )
          }
      }
    }.unzip4

    val companion = entityType.typeSymbol.companion

    val code = q"""
      new ${weakTypeOf[JooqMeta[T, R, E]]} {
        override val table = $table
        override def joinedTable(t: ${weakTypeOf[Table[Record]]}, leftJoin: Boolean = false) = {
           var current = t
           ..${joins.flatten}
           current
        }
        override lazy val fields = {
          var f = Array.empty[${weakTypeOf[Field[_]]}]
          ..$mappedFields
          f
        }
        override def toEntityAliased(r: ${weakTypeOf[Record]}, aliased: Boolean = true) = $companion(..$toEntityParams)
        override def toRecord(e: $entityType, current: $recordType = null.asInstanceOf[$recordType])(implicit dsl: ${weakTypeOf[DSLContext]}): $recordType = {
          val r = if(current != null) current else dsl.newRecord(table)
          ..$toRecordParams
          r
        }
      }
    """


    c.Expr[JooqMeta[T, R, E]] {
      code
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy