From 42c4418ce158c30c8afe7ee1ad0dd4abcefd4667 Mon Sep 17 00:00:00 2001 From: Emilio Date: Wed, 28 Aug 2024 12:34:07 -0400 Subject: [PATCH] GEOMESA-3389 Fix conversion of attributes to java.sql dates (#3160) --- .../converters/JavaTimeConverterFactory.scala | 72 ++++++++++++------- .../converters/ConverterFactoriesTest.scala | 16 +++++ 2 files changed, 64 insertions(+), 24 deletions(-) diff --git a/geomesa-utils-parent/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/geotools/converters/JavaTimeConverterFactory.scala b/geomesa-utils-parent/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/geotools/converters/JavaTimeConverterFactory.scala index b3c3b47a2426..a94d9e9e7280 100644 --- a/geomesa-utils-parent/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/geotools/converters/JavaTimeConverterFactory.scala +++ b/geomesa-utils-parent/geomesa-utils/src/main/scala/org/locationtech/geomesa/utils/geotools/converters/JavaTimeConverterFactory.scala @@ -8,8 +8,10 @@ package org.locationtech.geomesa.utils.geotools.converters +import org.geotools.data.util.TemporalConverterFactory import org.geotools.util.factory.Hints import org.geotools.util.{Converter, ConverterFactory} +import org.locationtech.geomesa.utils.geotools.converters.JavaTimeConverterFactory.TwoStageTemporalConverter import org.locationtech.geomesa.utils.text.DateParsing import java.time._ @@ -18,20 +20,31 @@ import java.util.Date class JavaTimeConverterFactory extends ConverterFactory { def createConverter(source: Class[_], target: Class[_], hints: Hints): Converter = { - if (classOf[Date].isAssignableFrom(source) && classOf[String].isAssignableFrom(target)) { + if (classOf[Date].isAssignableFrom(source) && classOf[String] == target) { JavaTimeConverterFactory.DateToStringConverter } else if (classOf[Date].isAssignableFrom(target)) { - if (classOf[String].isAssignableFrom(source)) { - JavaTimeConverterFactory.StringToDateConverter - } else if (classOf[ZonedDateTime].isAssignableFrom(source) || classOf[Instant].isAssignableFrom(source) - || classOf[OffsetDateTime].isAssignableFrom(source)) { - JavaTimeConverterFactory.TemporalToDateConverter - } else if (classOf[LocalDateTime].isAssignableFrom(source)) { - JavaTimeConverterFactory.LocalDateTimeToDateConverter - } else if (classOf[LocalDate].isAssignableFrom(source)) { - JavaTimeConverterFactory.LocalDateToDateConverter + val toDate = + if (classOf[String].isAssignableFrom(source)) { + JavaTimeConverterFactory.StringToDateConverter + } else if (classOf[ZonedDateTime].isAssignableFrom(source) || classOf[Instant].isAssignableFrom(source) + || classOf[OffsetDateTime].isAssignableFrom(source)) { + JavaTimeConverterFactory.TemporalToDateConverter + } else if (classOf[LocalDateTime].isAssignableFrom(source)) { + JavaTimeConverterFactory.LocalDateTimeToDateConverter + } else if (classOf[LocalDate].isAssignableFrom(source)) { + JavaTimeConverterFactory.LocalDateToDateConverter + } else { + null + } + if (toDate == null || classOf[Date] == target) { + toDate } else { - null + val toTarget = new TemporalConverterFactory().createConverter(classOf[Date], target, hints) + if (toTarget == null) { + null + } else { + new TwoStageTemporalConverter(toDate, toTarget) + } } } else { null @@ -41,32 +54,43 @@ class JavaTimeConverterFactory extends ConverterFactory { object JavaTimeConverterFactory { - private val DateToStringConverter = new Converter { + private trait DateConverter extends Converter { + + override def convert[T](source: AnyRef, target: Class[T]): T = convert(source).asInstanceOf[T] + + def convert(source: AnyRef): Date + } + + private object DateToStringConverter extends Converter { override def convert[T](source: AnyRef, target: Class[T]): T = DateParsing.formatDate(source.asInstanceOf[Date]).asInstanceOf[T] } - private val StringToDateConverter = new Converter { - override def convert[T](source: AnyRef, target: Class[T]): T = - DateParsing.parseDate(source.asInstanceOf[String]).asInstanceOf[T] + private object StringToDateConverter extends DateConverter { + override def convert(source: AnyRef): Date = DateParsing.parseDate(source.asInstanceOf[String]) } - private val TemporalToDateConverter = new Converter { - override def convert[T](source: AnyRef, target: Class[T]): T = { + private object TemporalToDateConverter extends DateConverter { + override def convert(source: AnyRef): Date = { val accessor = source.asInstanceOf[TemporalAccessor] val millis = accessor.getLong(ChronoField.INSTANT_SECONDS) * 1000 + accessor.getLong(ChronoField.MILLI_OF_SECOND) - new Date(millis).asInstanceOf[T] + new Date(millis) } } - private val LocalDateToDateConverter = new Converter { - override def convert[T](source: AnyRef, target: Class[T]): T = - TemporalToDateConverter.convert(source.asInstanceOf[LocalDate].atStartOfDay(ZoneOffset.UTC), target) + private object LocalDateToDateConverter extends DateConverter { + override def convert(source: AnyRef): Date = + TemporalToDateConverter.convert(source.asInstanceOf[LocalDate].atStartOfDay(ZoneOffset.UTC)) + } + + private object LocalDateTimeToDateConverter extends DateConverter { + override def convert(source: AnyRef): Date = + TemporalToDateConverter.convert(source.asInstanceOf[LocalDateTime].atZone(ZoneOffset.UTC)) } - private val LocalDateTimeToDateConverter = new Converter { + private class TwoStageTemporalConverter(toDate: DateConverter, toTarget: Converter) extends Converter { override def convert[T](source: AnyRef, target: Class[T]): T = - TemporalToDateConverter.convert(source.asInstanceOf[LocalDateTime].atZone(ZoneOffset.UTC), target) + toTarget.convert(toDate.convert(source), target) } -} \ No newline at end of file +} diff --git a/geomesa-utils-parent/geomesa-utils/src/test/scala/org/locationtech/geomesa/utils/geotools/converters/ConverterFactoriesTest.scala b/geomesa-utils-parent/geomesa-utils/src/test/scala/org/locationtech/geomesa/utils/geotools/converters/ConverterFactoriesTest.scala index 7b3cacc15d5b..fefa8a4e31c5 100644 --- a/geomesa-utils-parent/geomesa-utils/src/test/scala/org/locationtech/geomesa/utils/geotools/converters/ConverterFactoriesTest.scala +++ b/geomesa-utils-parent/geomesa-utils/src/test/scala/org/locationtech/geomesa/utils/geotools/converters/ConverterFactoriesTest.scala @@ -13,6 +13,7 @@ import org.junit.runner.RunWith import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner +import java.sql.Timestamp import java.time._ import java.time.format.DateTimeFormatter import java.util.{Date, UUID} @@ -275,5 +276,20 @@ class ConverterFactoriesTest extends Specification { factory.createConverter(time.getClass, classOf[Date], null).convert(time, classOf[Date]) mustEqual date } } + + "convert java time classes to sql dates" >> { + val times = Seq( + Instant.ofEpochMilli(date.getTime), + ZonedDateTime.ofInstant(Instant.ofEpochMilli(date.getTime), ZoneOffset.UTC), + LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime), ZoneOffset.UTC), + OffsetDateTime.ofInstant(Instant.ofEpochMilli(date.getTime), ZoneOffset.UTC), + LocalDate.from(ZonedDateTime.ofInstant(Instant.ofEpochMilli(date.getTime), ZoneOffset.UTC)) + ) + foreach(times) { time => + val result = factory.createConverter(time.getClass, classOf[Timestamp], null).convert(time, classOf[Timestamp]) + result must beAnInstanceOf[Timestamp] + date mustEqual result // compare backwards (expected vs actual) so that equals check works + } + } } }