diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ec9b6d..2f3aa421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - WMS Parent Layer default time should be omitted [#368](/~https://github.com/geotrellis/geotrellis-server/pull/368) - Fix WCS DescribeCoverage extents axis ordering [#369](/~https://github.com/geotrellis/geotrellis-server/pull/369) +## Changed +- OgcTime logic cleanup [#371](/~https://github.com/geotrellis/geotrellis-server/pull/371) + ## [4.4.0] - 2021-04-30 ## Fixed diff --git a/ogc-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala b/ogc-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala index d745abab..8b16d741 100644 --- a/ogc-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala +++ b/ogc-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala @@ -41,7 +41,7 @@ case class RasterSourceConf( resampleMethod: ResampleMethod = ResampleMethod.DEFAULT, overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT, datetimeField: String = SimpleSource.TimeFieldDefault, - timeFormat: OgcTimeFormat = OgcTimeFormat.Self, + timeFormat: OgcTimeFormat = OgcTimeFormat.Default, timeDefault: OgcTimeDefault = OgcTimeDefault.Oldest ) extends OgcSourceConf { def toLayer: RasterOgcSource = { @@ -84,7 +84,7 @@ case class MapAlgebraSourceConf( styles: List[StyleConf], resampleMethod: ResampleMethod = ResampleMethod.DEFAULT, overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT, - timeFormat: OgcTimeFormat = OgcTimeFormat.Self, + timeFormat: OgcTimeFormat = OgcTimeFormat.Default, timeDefault: OgcTimeDefault = OgcTimeDefault.Oldest ) extends OgcSourceConf { private def listParams(expr: Expression): List[String] = { diff --git a/ogc-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala b/ogc-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala index d8860136..08b4cdb9 100644 --- a/ogc-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala +++ b/ogc-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala @@ -153,9 +153,7 @@ class WcsView[F[_]: Concurrent: Parallel: ApplicativeThrow: Logger]( WcsParams(req.multiParams) match { case Validated.Invalid(errors) => val msg = ParamError.generateErrorMessage(errors.toList) - logger.warn(s"""Error parsing parameters: ${msg}""") *> BadRequest( - s"""Error parsing parameters: ${msg}""" - ) + logger.warn(msg) >> BadRequest(msg) case Validated.Valid(wcsParams) => wcsParams match { diff --git a/ogc/src/main/scala/geotrellis/server/ogc/OgcSource.scala b/ogc/src/main/scala/geotrellis/server/ogc/OgcSource.scala index 0fdca6c1..83eab3e5 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/OgcSource.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/OgcSource.scala @@ -95,7 +95,7 @@ case class SimpleSource( timeFormat: OgcTimeFormat ) extends RasterOgcSource { val timeDefault: OgcTimeDefault = OgcTimeDefault.Oldest - lazy val time: OgcTime = source.time(timeMetadataKey).format(timeFormat) + lazy val time: OgcTime = source.time(timeMetadataKey).format(timeFormat).sorted def toLayer(crs: CRS, style: Option[OgcStyle], temporalSequence: List[OgcTime]): SimpleOgcLayer = SimpleOgcLayer(name, title, crs, source, style, resampleMethod, overviewStrategy) @@ -123,8 +123,8 @@ case class GeoTrellisOgcSource( temporalSequence.headOption match { case Some(t) if t.nonEmpty => sourceForTime(t) case _ if temporalSequence.isEmpty && source.isTemporal => - val sorted = source.times - val time = timeDefault match { + lazy val sorted = source.times.sorted + val time = timeDefault match { case OgcTimeDefault.Oldest => sorted.head case OgcTimeDefault.Newest => sorted.last case OgcTimeDefault.Time(dt) => dt @@ -155,7 +155,7 @@ case class GeoTrellisOgcSource( lazy val time: OgcTime = if (!source.isTemporal) OgcTimeEmpty - else OgcTimePositions(source.times).format(timeFormat) + else OgcTimePositions(source.times).format(timeFormat).sorted /** If temporal, try to match in the following order: * @@ -273,7 +273,7 @@ case class MapAlgebraSource( new GridExtent[Long](nativeExtent, cellSize) } - val time: OgcTime = ogcSources.values.toList.map(_.time).foldLeft[OgcTime](OgcTimeEmpty)(_ |+| _).format(timeFormat) + val time: OgcTime = ogcSources.values.toList.map(_.time).foldLeft[OgcTime](OgcTimeEmpty)(_ |+| _).format(timeFormat).sorted val attributes: Map[String, String] = Map.empty lazy val nativeCrs: Set[CRS] = ogcSourcesList.flatMap(_.nativeCrs).toSet diff --git a/ogc/src/main/scala/geotrellis/server/ogc/OgcSourceRepository.scala b/ogc/src/main/scala/geotrellis/server/ogc/OgcSourceRepository.scala index 732c14b3..c5d8f416 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/OgcSourceRepository.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/OgcSourceRepository.scala @@ -46,7 +46,7 @@ object OgcSourceRepository { _.filter { _.time match { case OgcTimePositions(list) => - val sorted = list.toList.sorted + val sorted = list.sorted val start = sorted.head val end = sorted.last t1 <= start && start <= t2 || t1 <= end && end <= t2 diff --git a/ogc/src/main/scala/geotrellis/server/ogc/OgcTime.scala b/ogc/src/main/scala/geotrellis/server/ogc/OgcTime.scala index 68bf0ce9..1288d0b1 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/OgcTime.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/OgcTime.scala @@ -17,7 +17,7 @@ package geotrellis.server.ogc import cats.data.NonEmptyList -import cats.{Monoid, Order, Semigroup} +import cats.{Monoid, Semigroup} import cats.syntax.semigroup._ import jp.ne.opt.chronoscala.Imports._ import org.threeten.extra.PeriodDuration @@ -67,7 +67,7 @@ object OgcTime { case positions: OgcTimePositions => positions case _ => self } - case OgcTimeFormat.Self => self + case OgcTimeFormat.Default => self } def strictTimeMatch(dt: ZonedDateTime): Boolean = @@ -76,6 +76,16 @@ object OgcTime { case OgcTimeInterval(start, _, _) => start == dt case OgcTimeEmpty => true } + + def sorted: OgcTime = + self match { + case OgcTimeEmpty => OgcTimeEmpty + case OgcTimePositions(list) => OgcTimePositions(list.sorted) + case OgcTimeInterval(start, end, interval) => + val List(s, e) = List(start, end).sorted + OgcTimeInterval(s, e, interval) + + } } } @@ -88,8 +98,6 @@ case object OgcTimeEmpty extends OgcTime { /** Represents the TimePosition used in TimeSequence requests */ final case class OgcTimePositions(list: NonEmptyList[ZonedDateTime]) extends OgcTime { - import OgcTimePositions._ - def sorted: NonEmptyList[ZonedDateTime] = list.sorted /** Compute (if possible) the period of the [[ZonedDateTime]] lists. */ @@ -114,8 +122,6 @@ final case class OgcTimePositions(list: NonEmptyList[ZonedDateTime]) extends Ogc } object OgcTimePositions { - implicit val timeOrder: Order[ZonedDateTime] = Order.fromOrdering - implicit val ogcTimePositionsSemigroup: Semigroup[OgcTimePositions] = { (l, r) => OgcTimePositions((l.list ::: r.list).distinct.sorted) } @@ -128,6 +134,8 @@ object OgcTimePositions { case _ => OgcTimeEmpty } def apply(times: List[String])(implicit d: DummyImplicit): OgcTime = apply(times.map(ZonedDateTime.parse)) + + def parse(times: List[String]): Try[OgcTime] = Try(apply(times.map(ZonedDateTime.parse))) } /** Represents the TimeInterval used in TimeSequence requests @@ -196,4 +204,6 @@ object OgcTimeInterval { case _ => throw new UnsupportedOperationException("Unsupported string format for OgcTimeInterval") } } + + def parse(timeString: String): Try[OgcTimeInterval] = Try(fromString(timeString)) } diff --git a/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeDefault.scala b/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeDefault.scala index 9d6d1221..8b7c786a 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeDefault.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeDefault.scala @@ -43,11 +43,13 @@ object OgcTimeDefault { } implicit class OgcTimeDefaultOps(val self: OgcTimeDefault) extends AnyVal { - def selectTime(list: NonEmptyList[ZonedDateTime]): ZonedDateTime = + def selectTime(list: NonEmptyList[ZonedDateTime]): ZonedDateTime = { + lazy val sorted = list.sorted self match { - case OgcTimeDefault.Oldest => list.head - case OgcTimeDefault.Newest => list.last + case OgcTimeDefault.Oldest => sorted.head + case OgcTimeDefault.Newest => sorted.last case OgcTimeDefault.Time(t) => t } + } } } diff --git a/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeFormat.scala b/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeFormat.scala index b432bc2c..f630f61b 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeFormat.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/OgcTimeFormat.scala @@ -32,7 +32,7 @@ object OgcTimeFormat { case object Interval extends OgcTimeFormat /** Don't change the internal [[OgcTime]] representation. */ - case object Self extends OgcTimeFormat + case object Default extends OgcTimeFormat private implicit val config: Configuration = Configuration.default.copy(transformConstructorNames = _.toLowerCase) implicit val ogcTimeFormatCodec: Codec[OgcTimeFormat] = deriveEnumerationCodec diff --git a/ogc/src/main/scala/geotrellis/server/ogc/package.scala b/ogc/src/main/scala/geotrellis/server/ogc/package.scala index e1d7b531..76d32e41 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/package.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/package.scala @@ -20,9 +20,15 @@ import geotrellis.proj4.CRS import geotrellis.raster.{CellSize, Histogram, MultibandTile, Raster} import geotrellis.server.ogc.style.OgcStyle import geotrellis.vector.Extent +import cats.Order import org.threeten.extra.PeriodDuration +import jp.ne.opt.chronoscala.Imports._ + +import java.time.ZonedDateTime package object ogc { + implicit val ZonedDateTimeOrder: Order[ZonedDateTime] = Order.fromOrdering[ZonedDateTime] + implicit class ExtentOps(val self: Extent) extends AnyVal { def swapXY: Extent = Extent(xmin = self.ymin, ymin = self.xmin, xmax = self.ymax, ymax = self.xmax) def buffer(cellSize: CellSize): Extent = self.buffer(cellSize.width / 2, cellSize.height / 2) diff --git a/ogc/src/main/scala/geotrellis/server/ogc/params/ParamMap.scala b/ogc/src/main/scala/geotrellis/server/ogc/params/ParamMap.scala index fe22aa89..25961123 100644 --- a/ogc/src/main/scala/geotrellis/server/ogc/params/ParamMap.scala +++ b/ogc/src/main/scala/geotrellis/server/ogc/params/ParamMap.scala @@ -17,9 +17,9 @@ package geotrellis.server.ogc.params import geotrellis.server.ogc._ - import cats.syntax.option._ -import cats.data.{Validated, ValidatedNel} +import cats.syntax.traverse._ +import cats.data.{NonEmptyList, Validated, ValidatedNel} import Validated._ import scala.util.{Failure, Success, Try} @@ -118,12 +118,27 @@ case class ParamMap(params: Map[String, Seq[String]]) { } }).toValidatedNel - def validatedOgcTimeSequence(field: String): ValidatedNel[ParamError, List[OgcTime]] = - validatedOptionalParam(field).map { - case Some(timeString) if timeString.contains("/") => timeString.split(",").map(OgcTimeInterval.fromString).toList - case Some(timeString) => OgcTimePositions(timeString.split(",").toList) :: Nil - case None => List.empty[OgcTime] + def validatedOgcTimeSequence(field: String): ValidatedNel[ParamError, List[OgcTime]] = { + validatedOptionalParam(field).andThen { + case Some(timeString) if timeString.contains("/") => + timeString + .split(",") + .map(OgcTimeInterval.parse) + .toList + .sequence match { + case Success(list) => Valid(list) + case Failure(e) => Invalid(NonEmptyList.of(ParamError.ParseError(field, e.getMessage))) + } + case Some(timeString) => + OgcTimePositions + .parse(timeString.split(",").toList) + .map(_ :: Nil) match { + case Success(list) => Valid(list) + case Failure(e) => Invalid(NonEmptyList.of(ParamError.ParseError(field, e.getMessage))) + } + case None => Valid(Nil) } + } def validatedOgcTime(field: String): ValidatedNel[ParamError, OgcTime] = validatedOgcTimeSequence(field).map(_.headOption.getOrElse(OgcTimeEmpty)) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 68c2e89c..e764b6d2 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -52,8 +52,8 @@ object Dependencies { val http4sDsl = http4sVer("dsl") val http4sXml = http4sVer("scala-xml") val jaxbApi = "javax.xml.bind" % "jaxb-api" % jaxbApiVer - val kindProjector = "org.typelevel" %% "kind-projector" % "0.11.3" - val semanticdbScalac = "org.scalameta" % "semanticdb-scalac" % "4.4.10" + val kindProjector = "org.typelevel" %% "kind-projector" % "0.13.0" + val semanticdbScalac = "org.scalameta" % "semanticdb-scalac" % "4.4.18" val log4cats = "io.chrisdavenport" %% "log4cats-slf4j" % "1.1.1" val mamlJvm = "com.azavea.geotrellis" %% "maml-jvm" % "0.6.1" val pureConfig = "com.github.pureconfig" %% "pureconfig" % "0.14.1" diff --git a/stac-example/Makefile b/stac-example/Makefile index b2f73d0b..49cf3b5c 100644 --- a/stac-example/Makefile +++ b/stac-example/Makefile @@ -10,7 +10,7 @@ postgres: migrations: docker run --rm \ --network stac-example_default \ - quay.io/azavea/franklin:404f1c0 \ + quay.io/azavea/franklin:367cdc7 \ migrate \ --db-user franklin \ --db-name franklin \ @@ -23,7 +23,7 @@ import-landsat-stac-collection: --network stac-example_default \ -e AWS_REGION=us-east-1 \ -v ${PWD}/catalog:/opt/data/ \ - quay.io/azavea/franklin:404f1c0 \ + quay.io/azavea/franklin:367cdc7 \ import-catalog \ --db-user franklin \ --db-name franklin \ @@ -36,7 +36,7 @@ import-landsat-stac-periodic-collection: --network stac-example_default \ -e AWS_REGION=us-east-1 \ -v ${PWD}/catalog:/opt/data/ \ - quay.io/azavea/franklin:404f1c0 \ + quay.io/azavea/franklin:367cdc7 \ import-catalog \ --db-user franklin \ --db-name franklin \ @@ -49,7 +49,7 @@ import-landsat-stac-layers: --network stac-example_default \ -e AWS_REGION=us-east-1 \ -v ${PWD}/catalog:/opt/data/ \ - quay.io/azavea/franklin:404f1c0 \ + quay.io/azavea/franklin:367cdc7 \ import-catalog \ --db-user franklin \ --db-name franklin \ @@ -61,7 +61,7 @@ run-franklin: docker run --rm \ --network stac-example_default \ -p 9090:9090 \ - quay.io/azavea/franklin:404f1c0 \ + quay.io/azavea/franklin:367cdc7 \ serve \ --db-user franklin \ --db-name franklin \ diff --git a/stac-example/README.md b/stac-example/README.md index 89040a0a..e532fb53 100644 --- a/stac-example/README.md +++ b/stac-example/README.md @@ -37,11 +37,11 @@ stac-lc8-red-us = { asset-limit = 1000 // Max assets returned by the STAC search request source = "http://localhost:9090/" // Path to the STAC API endpoint default-style = "red-to-blue" - // force time positions computation even if the range is given through the collection / layer summary - compute-time-positions = true + // force time positions fetch from items even if the range is given through the collection / layer summary + fetch-time-positions = true // optional field (can be added to all layers definitions) - // the default value is "self", meaning that it would use the OGC Time in a format recieved from the source - time-format = "{interval | positions | self}" + // the default value is "default", meaning that it would use the OGC Time in a format recieved from the source + time-format = "{interval | positions | default}" ignore-time = false // work with temporal layers as with spatial layers, false by default // specify the default temporal rollback, oldest by default time-default = "{newest | oldest | ISO compatible date time}" diff --git a/stac-example/src/main/scala/geotrellis/server/ogc/Main.scala b/stac-example/src/main/scala/geotrellis/server/ogc/Main.scala index 9727e2ef..30fdec77 100644 --- a/stac-example/src/main/scala/geotrellis/server/ogc/Main.scala +++ b/stac-example/src/main/scala/geotrellis/server/ogc/Main.scala @@ -112,11 +112,11 @@ object Main for { conf <- Conf.loadResourceF[IO](configPath) /** MosaicRasterSources pool init for the graceful shutdown. */ - blockingPool <- Resource.make(IO.delay(BlockingThreadPool.pool))(p => IO.delay(p.shutdown())) + // blockingPool <- Resource.make(IO.delay(BlockingThreadPool.pool))(p => IO.delay(p.shutdown())) /** Uses server pool. */ http4sClient <- Http4sBackend.usingDefaultBlazeClientBuilder[IO](Blocker.liftExecutionContext(executionContext), executionContext) simpleSources = conf.layers.values.collect { case rsc: RasterSourceConf => rsc.toLayer }.toList - _ <- Resource.liftF( + _ <- Resource.eval( logOptState( conf.wms, ansi"%green{WMS configuration detected}, starting Web Map Service", @@ -131,7 +131,7 @@ object Main ExtendedParameters.extendedParametersBinding ) } - _ <- Resource.liftF( + _ <- Resource.eval( logOptState( conf.wmts, ansi"%green{WMTS configuration detected}, starting Web Map Tiling Service", @@ -145,7 +145,7 @@ object Main svc.layerSources(simpleSources, http4sClient) ) } - _ <- Resource.liftF( + _ <- Resource.eval( logOptState( conf.wcs, ansi"%green{WCS configuration detected}, starting Web Coverage Service", diff --git a/stac-example/src/main/scala/geotrellis/server/ogc/StacOgcSource.scala b/stac-example/src/main/scala/geotrellis/server/ogc/StacOgcSource.scala index edabd49b..dd9534a9 100644 --- a/stac-example/src/main/scala/geotrellis/server/ogc/StacOgcSource.scala +++ b/stac-example/src/main/scala/geotrellis/server/ogc/StacOgcSource.scala @@ -38,13 +38,13 @@ case class StacOgcSource( resampleMethod: ResampleMethod, overviewStrategy: OverviewStrategy, timeMetadataKey: Option[String], - computeTimePositions: Boolean, + fetchTimePositions: Boolean, timeFormat: OgcTimeFormat, timeDefault: OgcTimeDefault ) extends RasterOgcSource { lazy val time: OgcTime = { - val summaryTime = if (!computeTimePositions) stacSource.stacExtent.ogcTime else None + val summaryTime = if (!fetchTimePositions) stacSource.stacExtent.ogcTime else None summaryTime .orElse( attributes @@ -53,6 +53,7 @@ case class StacOgcSource( ) .getOrElse(source.time(timeMetadataKey)) .format(timeFormat) + .sorted } def source: RasterSource = stacSource.source diff --git a/stac-example/src/main/scala/geotrellis/server/ogc/conf/Conf.scala b/stac-example/src/main/scala/geotrellis/server/ogc/conf/Conf.scala index d6ade736..d286eba7 100644 --- a/stac-example/src/main/scala/geotrellis/server/ogc/conf/Conf.scala +++ b/stac-example/src/main/scala/geotrellis/server/ogc/conf/Conf.scala @@ -52,7 +52,7 @@ object Conf { } def loadResourceF[F[_]: Sync](configPath: Option[String]): Resource[F, Conf] = - Resource.liftF(loadF[F](configPath)) + Resource.eval(loadF[F](configPath)) // This is a work-around to use pureconfig to read scalaxb generated case classes // DataRecord should never be specified from configuration, this satisfied the resolution diff --git a/stac-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala b/stac-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala index 4c17ac22..a763edbe 100644 --- a/stac-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala +++ b/stac-example/src/main/scala/geotrellis/server/ogc/conf/OgcSourceConf.scala @@ -55,9 +55,9 @@ case class StacSourceConf( overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT, ignoreTime: Boolean = false, datetimeField: String = "datetime", - timeFormat: OgcTimeFormat = OgcTimeFormat.Self, + timeFormat: OgcTimeFormat = OgcTimeFormat.Default, timeDefault: OgcTimeDefault = OgcTimeDefault.Oldest, - computeTimePositions: Boolean = false, + fetchTimePositions: Boolean = false, withGDAL: Boolean = false, parallelMosaic: Boolean = false ) extends OgcSourceConf { @@ -88,7 +88,7 @@ case class StacSourceConf( resampleMethod, overviewStrategy, datetimeField.some, - computeTimePositions, + fetchTimePositions, timeFormat, timeDefault ) @@ -113,7 +113,7 @@ case class RasterSourceConf( resampleMethod: ResampleMethod = ResampleMethod.DEFAULT, overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT, datetimeField: String = SimpleSource.TimeFieldDefault, - timeFormat: OgcTimeFormat = OgcTimeFormat.Self, + timeFormat: OgcTimeFormat = OgcTimeFormat.Default, timeDefault: OgcTimeDefault = OgcTimeDefault.Oldest ) extends OgcSourceConf { def toLayer: RasterOgcSource = @@ -155,7 +155,7 @@ case class MapAlgebraSourceConf( styles: List[StyleConf], resampleMethod: ResampleMethod = ResampleMethod.DEFAULT, overviewStrategy: OverviewStrategy = OverviewStrategy.DEFAULT, - timeFormat: OgcTimeFormat = OgcTimeFormat.Self, + timeFormat: OgcTimeFormat = OgcTimeFormat.Default, timeDefault: OgcTimeDefault = OgcTimeDefault.Oldest ) extends OgcSourceConf { def listParams(expr: Expression): List[String] = { diff --git a/stac-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala b/stac-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala index d8860136..08b4cdb9 100644 --- a/stac-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala +++ b/stac-example/src/main/scala/geotrellis/server/ogc/wcs/WcsView.scala @@ -153,9 +153,7 @@ class WcsView[F[_]: Concurrent: Parallel: ApplicativeThrow: Logger]( WcsParams(req.multiParams) match { case Validated.Invalid(errors) => val msg = ParamError.generateErrorMessage(errors.toList) - logger.warn(s"""Error parsing parameters: ${msg}""") *> BadRequest( - s"""Error parsing parameters: ${msg}""" - ) + logger.warn(msg) >> BadRequest(msg) case Validated.Valid(wcsParams) => wcsParams match {