diff --git a/build.sbt b/build.sbt index f705e8f1..14e6b5b1 100644 --- a/build.sbt +++ b/build.sbt @@ -110,7 +110,6 @@ val coreDependencies = Seq( "io.circe" %% "circe-generic" % Versions.CirceVersion, "io.circe" %% "circe-parser" % Versions.CirceVersion, "io.circe" %% "circe-refined" % Versions.CirceVersion, - "joda-time" % "joda-time" % Versions.JodaTime, "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, "org.locationtech.jts" % "jts-core" % Versions.Jts, "org.typelevel" %% "cats-core" % Versions.CatsVersion, @@ -124,7 +123,6 @@ val testingDependencies = Seq( "eu.timepit" %% "refined" % Versions.RefinedVersion, "io.chrisdavenport" %% "cats-scalacheck" % Versions.ScalacheckCatsVersion, "io.circe" %% "circe-core" % Versions.CirceVersion, - "joda-time" % "joda-time" % Versions.JodaTime, "org.locationtech.geotrellis" %% "geotrellis-vector" % Versions.GeoTrellisVersion, "org.locationtech.jts" % "jts-core" % Versions.Jts, "org.scalacheck" %% "scalacheck" % Versions.ScalacheckVersion, diff --git a/modules/core-test/src/test/scala/com/azavea/stac4s/SerDeSpec.scala b/modules/core-test/src/test/scala/com/azavea/stac4s/SerDeSpec.scala index 2475c925..3949dc18 100644 --- a/modules/core-test/src/test/scala/com/azavea/stac4s/SerDeSpec.scala +++ b/modules/core-test/src/test/scala/com/azavea/stac4s/SerDeSpec.scala @@ -10,7 +10,8 @@ import geotrellis.vector.Geometry import io.circe.syntax._ import io.circe.parser._ import io.circe.testing.{ArbitraryInstances, CodecTests} -import org.joda.time.Instant +import java.time.{Instant, OffsetDateTime} + import org.scalatest.Assertion import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -71,7 +72,7 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M // timezone parsing unit tests private def getTimeDecodeTest(timestring: String): Assertion = - timestring.asJson.as[Instant] shouldBe (Right(Instant.parse(timestring))) + timestring.asJson.as[Instant] shouldBe Right(OffsetDateTime.parse(timestring, RFC3339formatter).toInstant) test("Instant decodes timestrings with +0x:00 timezones") { getTimeDecodeTest("2018-01-01T00:00:00+05:00") @@ -92,4 +93,16 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M test("Instant decodes timestrings with -00 format timezone") { getTimeDecodeTest("2018-01-01T00:00:00-00") } + + test("Instant decodes timestring with Z format timezone") { + getTimeDecodeTest("2020-04-03T11:32:26Z") + } + + test("Instant decodes timestring with 1e3 Z format timezone") { + getTimeDecodeTest("2018-04-03T11:32:26.553Z") + } + + test("Instant decodes timestring with 1e9 Z format timezone") { + getTimeDecodeTest("2018-04-03T11:32:26.553955473Z") + } } diff --git a/modules/core/src/main/scala/com/azavea/stac4s/meta/ForeignImplicits.scala b/modules/core/src/main/scala/com/azavea/stac4s/meta/ForeignImplicits.scala index 2b3e6f09..d9bc719a 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/meta/ForeignImplicits.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/meta/ForeignImplicits.scala @@ -1,7 +1,6 @@ package com.azavea.stac4s.meta import com.azavea.stac4s.TemporalExtent - import cats.Eq import cats.syntax.either._ import eu.timepit.refined.api.RefType @@ -9,7 +8,8 @@ import geotrellis.vector.Geometry import io.circe._ import io.circe.parser.decode import io.circe.syntax._ -import org.joda.time.Instant +import java.time.{Instant, OffsetDateTime} +import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder} import scala.util.Try @@ -20,11 +20,30 @@ trait ForeignImplicits { implicit val eqGeometry: Eq[Geometry] = Eq.fromUniversalEquals // circe codecs + // A more flexible alternative to DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS[xxx][xx][X]") + // https://tools.ietf.org/html/rfc3339 + val RFC3339formatter = + new DateTimeFormatterBuilder() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .optionalStart() + .appendOffset("+HH:MM", "+00:00") + .optionalEnd() + .optionalStart() + .appendOffset("+HHMM", "+0000") + .optionalEnd() + .optionalStart() + .appendOffset("+HH", "Z") + .optionalEnd() + .toFormatter() - implicit val encodeJodaInstant: Encoder[Instant] = Encoder[String].contramap(_.toString) + implicit val encodeInstant: Encoder[Instant] = Encoder[String].contramap(_.toString) - implicit val decodeJodaInstant: Decoder[Instant] = - Decoder[String].emap(s => Either.fromTry(Try(Instant.parse(s))).leftMap(_ => s"$s was not a valid string format")) + implicit val decodeInstant: Decoder[Instant] = + Decoder[String].emap(s => + Either + .fromTry(Try(OffsetDateTime.parse(s, RFC3339formatter).toInstant)) + .leftMap(_ => s"$s was not a valid string format") + ) implicit val encodeTemporalExtent: Encoder[TemporalExtent] = _.value.map(x => x.asJson).asJson diff --git a/modules/core/src/main/scala/com/azavea/stac4s/meta/HasInstant.scala b/modules/core/src/main/scala/com/azavea/stac4s/meta/HasInstant.scala index d54c60da..5467c421 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/meta/HasInstant.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/meta/HasInstant.scala @@ -1,7 +1,7 @@ package com.azavea.stac4s.meta import eu.timepit.refined.api.Validate -import org.joda.time.Instant +import java.time.Instant final case class HasInstant() diff --git a/modules/core/src/main/scala/com/azavea/stac4s/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/package.scala index 29d6adb5..ae237cff 100644 --- a/modules/core/src/main/scala/com/azavea/stac4s/package.scala +++ b/modules/core/src/main/scala/com/azavea/stac4s/package.scala @@ -8,7 +8,7 @@ import eu.timepit.refined.api.{Refined, RefinedTypeOps} import eu.timepit.refined.boolean._ import eu.timepit.refined.collection.{Exists, MinSize, _} -import org.joda.time.Instant +import java.time.Instant package object stac4s { diff --git a/modules/testing/src/main/scala/testing.scala b/modules/testing/src/main/scala/testing.scala index 086b8087..45a3dec2 100644 --- a/modules/testing/src/main/scala/testing.scala +++ b/modules/testing/src/main/scala/testing.scala @@ -18,7 +18,7 @@ import io.circe.syntax._ import org.scalacheck._ import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.cats.implicits._ -import org.joda.time.Instant +import java.time.Instant package object testing extends NumericInstances { @@ -51,7 +51,7 @@ package object testing extends NumericInstances { ) }).widen - private def instantGen: Gen[Instant] = arbitrary[Int] map { x => new Instant(x.toLong) } + private def instantGen: Gen[Instant] = arbitrary[Int] map { x => Instant.now.plusMillis(x.toLong) } private def assetCollectionExtensionGen: Gen[AssetCollectionExtension] = possiblyEmptyMapGen( diff --git a/project/Versions.scala b/project/Versions.scala index 63d514c8..7d1a05fb 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -2,7 +2,6 @@ object Versions { val CatsVersion = "2.2.0" val CirceVersion = "0.13.0" val GeoTrellisVersion = "3.5.0" - val JodaTime = "2.10.6" val Jts = "1.16.1" val RefinedVersion = "0.9.17" val ScalacheckCatsVersion = "0.3.0"