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

anorm.Column.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc. 
 */

package anorm

import java.io.{ ByteArrayInputStream, InputStream }

import java.util.{ Date, UUID }

import java.math.{ BigDecimal => JBigDec, BigInteger }

import java.net.{ URI, URL }

import java.sql.Timestamp

import scala.util.{ Failure, Success => TrySuccess, Try }
import scala.util.control.NonFatal

import scala.reflect.ClassTag

import resource.managed

/**
 * Column mapping
 *
 * @define mapDescription If the column is successfully parsed, then apply the given function on the result.
 */
@annotation.implicitNotFound(
  "No column extractor found for the type ${A}: `anorm.Column[${A}]` required; See https://github.com/playframework/anorm/blob/main/docs/manual/working/scalaGuide/main/sql/ScalaAnorm.md#column-parsers"
)
trait Column[A] extends ((Any, MetaDataItem) => Either[SqlRequestError, A]) { parent =>

  /**
   * $mapDescription
   *
   * {{{
   * import anorm._
   *
   * sealed trait MyEnum
   * case object Foo extends MyEnum
   * case object Bar extends MyEnum
   *
   * val myEnumCol: Column[MyEnum] = Column.of[Int].mapResult {
   *   case 1 => Right(Foo) // `Right` means successful
   *   case 2 => Right(Bar)
   *   case _ => Left(TypeDoesNotMatch("Unexpected"))
   * }
   *
   * def find(id: String)(implicit con: java.sql.Connection) =
   *   SQL"SELECT enum_code FROM my_table WHERE id = \$id".
   *     as(SqlParser.scalar(myEnumCol).single)
   * }}}
   */
  final def mapResult[B](f: A => Either[SqlRequestError, B]): Column[B] =
    Column[B] { (v: Any, m: MetaDataItem) =>
      Compat.rightFlatMap(parent(v, m))(f)
    }

  /**
   * $mapDescription
   *
   * {{{
   * import anorm._
   *
   * sealed trait MyEnum
   * case object Foo extends MyEnum
   * case object Bar extends MyEnum
   *
   * val myEnumCol: Column[MyEnum] = Column.of[Int].map {
   *   case 1 => Foo
   *   case 2 => Bar
   * }
   *
   * def find(id: String)(implicit con: java.sql.Connection) =
   *   SQL"SELECT enum_code FROM my_table WHERE id = \$id".
   *     as(SqlParser.scalar(myEnumCol).single)
   * }}}
   */
  def map[B](f: A => B): Column[B] = mapResult[B] { a =>
    try {
      Right(f(a))
    } catch {
      case NonFatal(cause) => Left(SqlRequestError(cause))
    }
  }
}

/** Column companion, providing default conversions. */
object Column extends JodaColumn with JavaTimeColumn {

  /**
   * Resolves the `Column` instance for the given type.
   * (equivalent to `implicitly[Column[A]]`).
   *
   * @tparam A the type of the column value
   *
   * {{{
   * import anorm.Column
   *
   * val resolved: Column[String] = Column.of[String]
   * }}}
   */
  @inline def of[A](implicit resolved: Column[A]): Column[A] = resolved

  def apply[A](transformer: ((Any, MetaDataItem) => Either[SqlRequestError, A])): Column[A] = new Column[A] {

    def apply(value: Any, meta: MetaDataItem): Either[SqlRequestError, A] =
      transformer(value, meta)

  }

  @deprecated(message = "Use [[nonNull]]", since = "2.5.1")
  def nonNull1[A](transformer: ((Any, MetaDataItem) => Either[SqlRequestError, A])): Column[A] = nonNull[A](transformer)

  /**
   * Helper function to implement column conversion.
   *
   * @param transformer Function converting raw value of column
   * @tparam Output type
   */
  def nonNull[A](transformer: ((Any, MetaDataItem) => Either[SqlRequestError, A])): Column[A] = Column[A] {
    case (value, meta @ MetaDataItem(qualified, _, _)) =>
      if (value != null) transformer(value, meta)
      else Left[SqlRequestError, A](UnexpectedNullableFound(qualified.toString))

  }

  @inline private[anorm] def className(that: Any): String =
    if (that == (null: Any)) "" else that.getClass.getName

  @SuppressWarnings(Array("AsInstanceOf"))
  private[anorm] def string[T](s: String)(f: String => T): Either[SqlRequestError, T] = Right(
    if (s == null) null.asInstanceOf[T] else f(s)
  )

  implicit val columnToString: Column[String] =
    nonNull[String] { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta

      @SuppressWarnings(Array("AsInstanceOf"))
      def unsafe = value match {
        case string: String      => Right(string)
        case clob: java.sql.Clob => Right(clob.getSubString(1, clob.length.asInstanceOf[Int]))
        case StringWrapper2(s)   => string(s)(identity)
        case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to String for column $qualified"))
      }

      unsafe
    }

  /**
   * Column conversion to bytes array.
   *
   * {{{
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToByteArray
   *
   * def bytes(implicit con: java.sql.Connection): Array[Byte] =
   *   SQL("SELECT bin FROM tbl").as(scalar[Array[Byte]].single)
   * }}}
   */
  implicit val columnToByteArray: Column[Array[Byte]] =
    nonNull[Array[Byte]] { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta
      value match {
        case bytes: Array[Byte]  => Right(bytes)
        case stream: InputStream => streamBytes(stream)
        case string: String      => Right(string.getBytes)
        case blob: java.sql.Blob => streamBytes(blob.getBinaryStream)
        case StringWrapper2(s)   => string(s)(_.getBytes)
        case _ =>
          Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to bytes array for column $qualified"))
      }
    }

  /**
   * Column conversion to character.
   *
   * {{{
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToChar
   *
   * def c(implicit con: java.sql.Connection): Char =
   *   SQL("SELECT char FROM tbl").as(scalar[Char].single)
   * }}}
   */
  implicit val columnToChar: Column[Char] = nonNull[Char] { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case string: String      => Right(string.charAt(0))
      case clob: java.sql.Clob => Right(clob.getSubString(1, 1).charAt(0))
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Char for column $qualified"))
    }
  }

  implicit val columnToInt: Column[Int] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bi: BigInteger => Right(bi.intValue)
      case bd: JBigDec    => Right(bd.intValue)
      case l: Long        => Right(l.toInt)
      case i: Int         => Right(i)
      case s: Short       => Right(s.toInt)
      case b: Byte        => Right(b.toInt)
      case bool: Boolean  => Right(if (!bool) 0 else 1)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Int for column $qualified"))
    }
  }

  /**
   * Column conversion to bytes array.
   *
   * {{{
   * import java.io.InputStream
   *
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToInputStream
   *
   * def bytes(implicit con: java.sql.Connection): InputStream =
   *   SQL("SELECT bin FROM tbl").as(scalar[InputStream].single)
   * }}}
   */
  implicit val columnToInputStream: Column[InputStream] =
    nonNull[InputStream] { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta
      value match {
        case bytes: Array[Byte]  => Right(new ByteArrayInputStream(bytes))
        case stream: InputStream => Right(stream)
        case string: String      => Right(new ByteArrayInputStream(string.getBytes))
        case blob: java.sql.Blob => Right(blob.getBinaryStream)
        case _ =>
          Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to input stream for column $qualified"))
      }
    }

  implicit val columnToFloat: Column[Float] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case f: Float       => Right(f)
      case bi: BigInteger => Right(bi.floatValue)
      case i: Int         => Right(i.toFloat)
      case s: Short       => Right(s.toFloat)
      case b: Byte        => Right(b.toFloat)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Float for column $qualified"))
    }
  }

  implicit val columnToDouble: Column[Double] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bg: JBigDec    => Right(bg.doubleValue)
      case d: Double      => Right(d)
      case f: Float       => Right(new JBigDec(f.toString).doubleValue)
      case bi: BigInteger => Right(bi.doubleValue)
      case i: Int         => Right(i.toDouble)
      case s: Short       => Right(s.toDouble)
      case b: Byte        => Right(b.toDouble)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Double for column $qualified"))
    }
  }

  implicit val columnToShort: Column[Short] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case b: Byte       => Right(b.toShort)
      case s: Short      => Right(s)
      case bool: Boolean => Right(if (!bool) 0.toShort else 1.toShort)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Short for column $qualified"))
    }
  }

  implicit val columnToByte: Column[Byte] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case b: Byte       => Right(b)
      case s: Short      => Right(s.toByte)
      case bool: Boolean => Right(if (!bool) 0.toByte else 1.toByte)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Byte for column $qualified"))
    }
  }

  implicit val columnToBoolean: Column[Boolean] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bool: Boolean => Right(bool)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Boolean for column $qualified"))
    }
  }

  private[anorm] val columnByteToBoolean: Column[Boolean] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bit: Byte if bit == 1 => Right(true)
      case bit: Byte if bit == 0 => Right(false)
      case _: Byte => Left(TypeDoesNotMatch(s"Cannot convert Byte $value to Boolean for column $qualified"))
      case bit: Short if bit == 1 => Right(true)
      case bit: Short if bit == 0 => Right(false)
      case _: Short => Left(TypeDoesNotMatch(s"Cannot convert Short $value to Boolean for column $qualified"))
      case _        => columnToBoolean(value, meta)
    }
  }

  @SuppressWarnings(Array("AsInstanceOf"))
  private[anorm] def timestamp[T](ts: Timestamp)(f: Timestamp => T): Either[SqlRequestError, T] = Right(
    if (ts == null) null.asInstanceOf[T] else f(ts)
  )

  implicit val columnToLong: Column[Long] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bi: BigInteger        => Right(bi.longValue)
      case bd: JBigDec           => Right(bd.longValue)
      case int: Int              => Right(int.toLong)
      case long: Long            => Right(long)
      case s: Short              => Right(s.toLong)
      case b: Byte               => Right(b.toLong)
      case bool: Boolean         => Right(if (!bool) 0L else 1L)
      case date: Date            => Right(date.getTime)
      case TimestampWrapper1(ts) => timestamp(ts)(_.getTime)
      case TimestampWrapper2(ts) => timestamp(ts)(_.getTime)
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Long for column $qualified"))
    }
  }

  // Used to convert Java or Scala big integer
  private def anyToBigInteger(value: Any, meta: MetaDataItem): Either[SqlRequestError, BigInteger] = {
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bi: BigInteger => Right(bi)
      case bd: JBigDec    => Right(bd.toBigInteger)
      case long: Long     => Right(BigInteger.valueOf(long))
      case int: Int       => Right(BigInteger.valueOf(int.toLong))
      case s: Short       => Right(BigInteger.valueOf(s.toLong))
      case b: Byte        => Right(BigInteger.valueOf(b.toLong))
      case _ =>
        Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to BigInteger for column $qualified"))
    }
  }

  /**
   * Column conversion to Java big integer.
   *
   * {{{
   * import java.math.BigInteger
   *
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToBigInteger
   *
   * def c(implicit con: java.sql.Connection): BigInteger =
   *   SQL("SELECT COUNT(*) FROM tbl").as(scalar[BigInteger].single)
   * }}}
   */
  implicit val columnToBigInteger: Column[BigInteger] = nonNull(anyToBigInteger)

  /**
   * Column conversion to big integer.
   *
   * {{{
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToBigInt
   *
   * def c(implicit con: java.sql.Connection): BigInt =
   *   SQL("SELECT COUNT(*) FROM tbl").as(scalar[BigInt].single)
   * }}}
   */
  implicit val columnToBigInt: Column[BigInt] = nonNull { (value, meta) =>
    Compat.rightMap(anyToBigInteger(value, meta))(BigInt(_))
  }

  implicit val columnToUUID: Column[UUID] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case d: UUID => Right(d)
      case s: String =>
        Try { UUID.fromString(s) } match {
          case TrySuccess(v) => Right(v)
          case Failure(ex) =>
            Left(
              TypeDoesNotMatch(
                s"Cannot convert $value: ${className(value)} to UUID for column $qualified: ${ex.getMessage}"
              )
            )
        }
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to UUID for column $qualified"))
    }
  }

  implicit val columnToURI: Column[URI] = columnToString.mapResult { str =>
    try {
      Right(new URI(str))
    } catch {
      case NonFatal(cause) => Left(SqlRequestError(cause))
    }
  }

  implicit val columnToURL: Column[URL] = columnToString.mapResult { str =>
    try {
      Right(new URL(str))
    } catch {
      case NonFatal(cause) => Left(SqlRequestError(cause))
    }
  }

  // Used to convert Java or Scala big decimal
  private def anyToBigDecimal(value: Any, meta: MetaDataItem): Either[SqlRequestError, JBigDec] = {
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case bd: JBigDec    => Right(bd)
      case bi: BigInteger => Right(new JBigDec(bi))
      case d: Double      => Right(JBigDec.valueOf(d))
      case f: Float       => Right(JBigDec.valueOf(f.toDouble))
      case l: Long        => Right(JBigDec.valueOf(l))
      case i: Int         => Right(JBigDec.valueOf(i.toLong))
      case s: Short       => Right(JBigDec.valueOf(s.toLong))
      case b: Byte        => Right(JBigDec.valueOf(b.toLong))
      case _ =>
        Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to BigDecimal for column $qualified"))
    }
  }

  /**
   * Column conversion to Java big decimal.
   *
   * {{{
   * import java.math.{ BigDecimal => JBigDecimal }
   *
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToJavaBigDecimal
   *
   * def c(implicit con: java.sql.Connection): JBigDecimal =
   *   SQL("SELECT COUNT(*) FROM tbl").as(scalar[JBigDecimal].single)
   * }}}
   */
  implicit val columnToJavaBigDecimal: Column[JBigDec] =
    nonNull(anyToBigDecimal)

  /**
   * Column conversion to big decimal.
   *
   * {{{
   * import anorm._, SqlParser.scalar
   * import anorm.Column.columnToScalaBigDecimal
   *
   * def c(implicit con: java.sql.Connection): BigDecimal =
   *   SQL("SELECT COUNT(*) FROM tbl").as(scalar[BigDecimal].single)
   * }}}
   */
  implicit val columnToScalaBigDecimal: Column[BigDecimal] =
    nonNull((value, meta) => Compat.rightMap(anyToBigDecimal(value, meta))(BigDecimal(_)))

  /**
   * Parses column as Java Date.
   * Time zone offset is the one of default JVM time zone
   * (see `java.util.TimeZone.getDefault`).
   *
   * {{{
   * import java.util.Date
   * import anorm._, SqlParser._
   *
   * def d(implicit con: java.sql.Connection): Date =
   *   SQL("SELECT last_mod FROM tbl").as(scalar[Date].single)
   * }}}
   */
  implicit val columnToDate: Column[Date] = nonNull { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case date: Date            => Right(date)
      case time: Long            => Right(new Date(time))
      case TimestampWrapper1(ts) => timestamp(ts)(t => new Date(t.getTime))
      case TimestampWrapper2(ts) => timestamp(ts)(t => new Date(t.getTime))
      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Date for column $qualified"))
    }
  }

  implicit def columnToOption[T](implicit transformer: Column[T]): Column[Option[T]] = Column { (value, meta) =>
    if (value != null) {
      Compat.rightMap(transformer(value, meta))(Some(_))
    } else Right[SqlRequestError, Option[T]](None)
  }

  /**
   * Parses column as array.
   *
   * {{{
   * import anorm._, SqlParser._
   *
   * def a(implicit con: java.sql.Connection): Array[String] =
   *   SQL"SELECT str_arr FROM tbl".as(scalar[Array[String]].single)
   * }}}
   */
  implicit def columnToArray[T](implicit transformer: Column[T], t: scala.reflect.ClassTag[T]): Column[Array[T]] =
    Column.nonNull[Array[T]] { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta

      @inline def typeNotMatch(value: Any, target: String, cause: Any) =
        TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to $target for column $qualified: $cause")

      @annotation.tailrec
      def transf(a: Array[_], p: Array[T]): Either[SqlRequestError, Array[T]] =
        a.headOption match {
          case Some(r) =>
            transformer(r, meta) match {
              case Right(v)    => transf(a.tail, p :+ v)
              case Left(cause) => Left(typeNotMatch(value, "array", cause))
            }
          case _ => Right(p)
        }

      @annotation.tailrec
      def jiter(i: java.util.Iterator[_], p: Array[T]): Either[SqlRequestError, Array[T]] = if (!i.hasNext) Right(p)
      else
        transformer(i.next, meta) match {
          case Right(v)    => jiter(i, p :+ v)
          case Left(cause) => Left(typeNotMatch(value, "list", cause))
        }

      @SuppressWarnings(Array("AsInstanceOf"))
      def unsafe = value match {
        case sql: java.sql.Array =>
          try {
            transf(sql.getArray.asInstanceOf[Array[_]], Array.empty[T])
          } catch {
            case NonFatal(cause) => Left(typeNotMatch(value, "array", cause))
          }

        case arr: Array[_] =>
          try {
            transf(arr, Array.empty[T])
          } catch {
            case NonFatal(cause) => Left(typeNotMatch(value, "list", cause))
          }

        case it: java.lang.Iterable[_] =>
          try {
            jiter(it.iterator, Array.empty[T])
          } catch {
            case NonFatal(cause) => Left(typeNotMatch(value, "list", cause))
          }

        case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to array for column $qualified"))
      }

      unsafe
    }

  /**
   * Parses column as list.
   *
   * {{{
   * import anorm._, SqlParser._
   *
   * def a(implicit con: java.sql.Connection): List[String] =
   *   SQL"SELECT str_arr FROM tbl".as(scalar[String].*)
   * }}}
   */
  @SuppressWarnings(Array("UnusedMethodParameter" /* deprecated */ ))
  implicit def columnToList[T](implicit
      transformer: Column[T],
      @deprecated("Unused", "2.5.4") t: scala.reflect.ClassTag[T]
  ): Column[List[T]] = Column.nonNull[List[T]] { (value, meta) =>
    val MetaDataItem(qualified, _, _) = meta

    @inline def typeNotMatch(value: Any, target: String, cause: Any) =
      TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to $target for column $qualified: $cause")

    @annotation.tailrec
    def transf(a: Array[_], p: List[T]): Either[SqlRequestError, List[T]] =
      a.headOption match {
        case Some(r) =>
          transformer(r, meta) match {
            case Right(v)    => transf(a.tail, v :: p)
            case Left(cause) => Left(typeNotMatch(value, "list", cause))
          }

        case _ => Right(p.reverse)
      }

    @annotation.tailrec
    def jiter(i: java.util.Iterator[_], p: List[T]): Either[SqlRequestError, List[T]] = if (!i.hasNext) Right(p.reverse)
    else
      transformer(i.next, meta) match {
        case Right(v)    => jiter(i, v :: p)
        case Left(cause) => Left(typeNotMatch(value, "list", cause))
      }

    @SuppressWarnings(Array("AsInstanceOf"))
    def unsafe = value match {
      case sql: java.sql.Array =>
        try {
          transf(sql.getArray.asInstanceOf[Array[_]], Nil)
        } catch {
          case NonFatal(cause) => Left(typeNotMatch(value, "list", cause))
        }

      case arr: Array[_] =>
        try {
          transf(arr, Nil)
        } catch {
          case NonFatal(cause) => Left(typeNotMatch(value, "list", cause))
        }

      case it: java.lang.Iterable[_] =>
        try {
          jiter(it.iterator, Nil)
        } catch {
          case NonFatal(cause) => Left(typeNotMatch(value, "list", cause))
        }

      case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to list for column $qualified"))
    }

    unsafe
  }

  @inline private def streamBytes(in: InputStream): Either[SqlRequestError, Array[Byte]] = {
    import resource.extractedEitherToEither
    implicit val cls: ClassTag[InputStream] = inputStreamClassTag

    managed(in)
      .acquireFor(streamToBytes(_))
      .fold(
        { errs =>
          Left(TypeDoesNotMatch(errs.headOption.fold("Fails to read binary stream")(_.getMessage)))
        },
        Right(_)
      )
  }

  @annotation.tailrec
  private def streamToBytes(
      in: InputStream,
      bytes: Array[Byte] = Array(),
      buffer: Array[Byte] = Array.ofDim(1024)
  ): Array[Byte] = {
    val count = in.read(buffer)

    if (count == -1) bytes
    else streamToBytes(in, bytes ++ buffer.take(count), buffer)
  }

  private[anorm] lazy val inputStreamClassTag =
    implicitly[ClassTag[InputStream]]
}

sealed trait JodaColumn {
  import org.joda.time.{ DateTime, LocalDate, LocalDateTime, Instant }
  import Column.{ nonNull, className, timestamp => Ts }

  /**
   * Parses column as Joda local date.
   * Time zone is the one of default JVM time zone
   * (see `org.joda.time.DateTimeZone.getDefault`).
   *
   * {{{
   * import org.joda.time.LocalDate
   * import anorm._, SqlParser.scalar
   *
   * def ld(implicit con: java.sql.Connection): LocalDate =
   *   SQL("SELECT last_mod FROM tbl").as(scalar[LocalDate].single)
   * }}}
   */
  implicit val columnToJodaLocalDate: Column[LocalDate] =
    nonNull { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta

      value match {
        case date: java.util.Date  => Right(new LocalDate(date.getTime))
        case time: Long            => Right(new LocalDate(time))
        case TimestampWrapper1(ts) => Ts(ts)(t => new LocalDate(t.getTime))
        case TimestampWrapper2(ts) => Ts(ts)(t => new LocalDate(t.getTime))
        case _ =>
          Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Joda LocalDate for column $qualified"))
      }
    }

  /**
   * Parses column as Joda local date/time.
   * Time zone is the one of default JVM time zone
   * (see `org.joda.time.DateTimeZone.getDefault`).
   *
   * {{{
   * import org.joda.time.LocalDateTime
   * import anorm._, SqlParser._
   *
   * def ldt(implicit con: java.sql.Connection): LocalDateTime =
   *   SQL("SELECT last_mod FROM tbl").as(scalar[LocalDateTime].single)
   * }}}
   */
  implicit val columnToJodaLocalDateTime: Column[LocalDateTime] =
    nonNull { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta

      value match {
        case date: java.util.Date  => Right(new LocalDateTime(date.getTime))
        case time: Long            => Right(new LocalDateTime(time))
        case TimestampWrapper1(ts) => Ts(ts)(t => new LocalDateTime(t.getTime))
        case TimestampWrapper2(ts) => Ts(ts)(t => new LocalDateTime(t.getTime))
        case _ =>
          Left(
            TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Joda LocalDateTime for column $qualified")
          )
      }
    }

  /**
   * Parses column as joda DateTime
   *
   * {{{
   * import org.joda.time.DateTime
   * import anorm._, SqlParser._
   *
   * def dt(implicit con: java.sql.Connection): DateTime =
   *   SQL("SELECT last_mod FROM tbl").as(scalar[DateTime].single)
   * }}}
   */
  implicit val columnToJodaDateTime: Column[DateTime] =
    nonNull { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta

      @SuppressWarnings(Array("AsInstanceOf"))
      def unsafe = value match {
        case date: Date => Right(new DateTime(date.getTime))
        case time: Long => Right(new DateTime(time))
        case TimestampWrapper1(ts) =>
          Option(ts).fold(Right(null.asInstanceOf[DateTime]))(t => Right(new DateTime(t.getTime)))

        case TimestampWrapper2(ts) =>
          Option(ts).fold(Right(null.asInstanceOf[DateTime]))(t => Right(new DateTime(t.getTime)))

        case _ =>
          Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to DateTime for column $qualified"))
      }

      unsafe
    }

  /**
   * Parses column as joda Instant
   *
   * {{{
   * import anorm._, SqlParser.scalar
   * import org.joda.time.Instant
   *
   * def d(implicit con: java.sql.Connection): Instant =
   *   SQL("SELECT last_mod FROM tbl").as(scalar[Instant].single)
   * }}}
   */
  implicit val columnToJodaInstant: Column[Instant] =
    nonNull { (value, meta) =>
      val MetaDataItem(qualified, _, _) = meta
      value match {
        case date: Date            => Right(new Instant(date.getTime))
        case time: Long            => Right(new Instant(time))
        case TimestampWrapper1(ts) => Ts(ts)(t => new Instant(t.getTime))
        case TimestampWrapper2(ts) => Ts(ts)(t => new Instant(t.getTime))
        case _ => Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to Instant for column $qualified"))
      }
    }
}

sealed trait JavaTimeColumn {
  import java.time.{ ZonedDateTime, ZoneOffset, ZoneId, LocalDate, LocalDateTime, Instant }
  import Column.{ nonNull, className, timestamp => Ts }

  /**
   * Parses column as Java8 instant.
   * Time zone offset is the one of default JVM time zone
   * (see `java.time.ZoneId.systemDefault`).
   *
   * {{{
   * import java.time.Instant
   * import anorm._, SqlParser.scalar
   *
   * def i(implicit con: java.sql.Connection): Instant =
   *   SQL("SELECT last_mod FROM tbl").as(scalar[Instant].single)
   * }}}
   */
  implicit val columnToInstant: Column[Instant] =
    nonNull(instantValueTo(identity, "Java8 Instant"))

  private def instantValueTo[T](
      epoch: Instant => T,
      description: String
  )(value: Any, meta: MetaDataItem): Either[SqlRequestError, T] = {
    val MetaDataItem(qualified, _, _) = meta

    value match {
      case date: LocalDateTime    => Right(epoch(date.toInstant(ZoneOffset.UTC)))
      case ts: java.sql.Timestamp => Ts(ts)(t => epoch(t.toInstant))
      case date: java.util.Date =>
        Right(epoch(Instant.ofEpochMilli(date.getTime)))

      case time: Long =>
        Right(epoch(Instant.ofEpochMilli(time)))

      case TimestampWrapper1(ts) => Ts(ts)(t => epoch(t.toInstant))
      case TimestampWrapper2(ts) => Ts(ts)(t => epoch(t.toInstant))
      case _ =>
        Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to $description for column $qualified"))
    }
  }

  private def temporalValueTo[T](
      epoch: Long => T,
      description: String
  )(value: Any, meta: MetaDataItem): Either[SqlRequestError, T] = {
    val MetaDataItem(qualified, _, _) = meta
    value match {
      case date: java.util.Date  => Right(epoch(date.getTime))
      case time: Long            => Right(epoch(time))
      case TimestampWrapper1(ts) => Ts(ts)(t => epoch(t.getTime))
      case TimestampWrapper2(ts) => Ts(ts)(t => epoch(t.getTime))
      case _ =>
        Left(TypeDoesNotMatch(s"Cannot convert $value: ${className(value)} to $description for column $qualified"))
    }
  }

  private def temporalColumn[T](epoch: Long => T, description: String): Column[T] = nonNull(
    temporalValueTo(epoch, description)
  )

  /**
   * Parses column as Java8 local date/time.
   * Time zone offset is the one of default JVM time zone
   * (see `java.time.ZoneId.systemDefault`).
   *
   * {{{
   * import java.time.LocalDateTime
   *
   * import anorm._
   *
   * def i(implicit con: java.sql.Connection): LocalDateTime =
   *   SQL("SELECT last_mod FROM tbl").
   *     as(SqlParser.scalar[LocalDateTime].single)
   * }}}
   */
  implicit val columnToLocalDateTime: Column[LocalDateTime] = {
    def millisToLocalDateTime(ts: Long) =
      LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.systemDefault)

    nonNull { (value, meta) =>
      value match {
        case localDateTime: LocalDateTime => Right(localDateTime)

        case _ =>
          temporalValueTo[LocalDateTime](millisToLocalDateTime, "Java8 LocalDateTime")(value, meta)
      }
    }
  }

  /**
   * Parses column as Java8 local date.
   * Time zone offset is the one of default JVM time zone
   * (see `java.time.ZoneId.systemDefault`).
   *
   * {{{
   * import java.time.LocalDateTime
   *
   * import anorm._
   *
   * def i(implicit con: java.sql.Connection): LocalDateTime =
   *   SQL("SELECT last_mod FROM tbl").
   *     as(SqlParser.scalar[LocalDateTime].single)
   * }}}
   */
  implicit val columnToLocalDate: Column[LocalDate] =
    temporalColumn[LocalDate](
      { (ts: Long) =>
        LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneId.systemDefault).toLocalDate
      },
      "Java8 LocalDate"
    )

  /**
   * Parses column as Java8 zoned date/time.
   * Time zone offset is the one of default JVM time zone
   * (see `java.time.ZoneId.systemDefault`).
   *
   * {{{
   * import java.time.ZonedDateTime
   *
   * import anorm._
   *
   * def i(implicit con: java.sql.Connection): ZonedDateTime =
   *   SQL("SELECT last_mod FROM tbl").
   *     as(SqlParser.scalar[ZonedDateTime].single)
   * }}}
   */
  implicit val columnToZonedDateTime: Column[ZonedDateTime] =
    nonNull(instantValueTo(ZonedDateTime.ofInstant(_: Instant, ZoneId.systemDefault), "Java8 ZonedDateTime"))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy