diff --git a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/apirequest/Input.scala b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/apirequest/Input.scala index 851c5a34c..9ed656b5c 100644 --- a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/apirequest/Input.scala +++ b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/enrichments/registry/apirequest/Input.scala @@ -16,7 +16,7 @@ import cats.data.ValidatedNel import cats.implicits._ import io.circe.{Json => JSON, DecodingFailure, Decoder} -import io.gatling.jsonpath.{JsonPath => GatlingJsonPath} +import com.jayway.jsonpath.{JsonPath => JaywayJsonPath} import com.snowplowanalytics.iglu.core.{SchemaCriterion, SelfDescribingData} import com.snowplowanalytics.snowplow.badrows.igluSchemaCriterionDecoder @@ -34,7 +34,7 @@ sealed trait Input extends Product with Serializable { // We could short-circuit enrichment process on invalid JSONPath, // but it won't give user meaningful error message - def validatedJsonPath: Either[String, GatlingJsonPath] = + def validatedJsonPath: Either[String, JaywayJsonPath] = this match { case json: Input.Json => compileQuery(json.jsonPath) case _ => "No JSON Path given".asLeft diff --git a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/utils/JsonPath.scala b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/utils/JsonPath.scala index 00e2dd004..978dbd15c 100644 --- a/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/utils/JsonPath.scala +++ b/modules/common/src/main/scala/com.snowplowanalytics.snowplow.enrich/common/utils/JsonPath.scala @@ -11,51 +11,66 @@ package com.snowplowanalytics.snowplow.enrich.common.utils import cats.syntax.either._ +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.{ObjectMapper, SerializationFeature} +import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider +import com.jayway.jsonpath.{Configuration, JsonPath => JaywayJsonPath, Option => JOption} import io.circe._ -import io.gatling.jsonpath.{JsonPath => GatlingJsonPath} +import io.circe.jackson.{circeToJackson, jacksonToCirce} -/** Wrapper for `io.gatling.jsonpath` for circe and scalaz */ +import scala.jdk.CollectionConverters.asScalaIteratorConverter + +/** Wrapper for `com.jayway.jsonpath` for circe */ object JsonPath { - /** - * Wrapper method for not throwing an exception on JNothing, representing it as invalid JSON - * @param json JSON value, possibly JNothing - * @return successful POJO on any JSON except JNothing - */ - def convertToJson(json: Json): Object = - io.circe.jackson.mapper.convertValue(json, classOf[Object]) + private val JacksonNodeJsonObjectMapper = { + val objectMapper = new ObjectMapper() + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + objectMapper + } + private val JsonPathConf = + Configuration + .builder() + .options(JOption.SUPPRESS_EXCEPTIONS) + .options(JOption.ALWAYS_RETURN_LIST) + .jsonProvider(new JacksonJsonNodeJsonProvider(JacksonNodeJsonObjectMapper)) + .build() /** * Pimp-up JsonPath class to work with JValue * Unlike `query(jsonPath, json)` it gives empty list on any error (like JNothing) * @param jsonPath precompiled with [[compileQuery]] JsonPath object */ - implicit class CirceExtractor(jsonPath: GatlingJsonPath) { - def circeQuery(json: Json): List[Json] = { - val pojo = convertToJson(json) - jsonPath.query(pojo).map(anyToJson).toList - } + implicit class CirceExtractor(jsonPath: JaywayJsonPath) { + def circeQuery(json: Json): List[Json] = + arrayNodeToCirce(jsonPath.read[ArrayNode](circeToJackson(json), JsonPathConf)) } /** * Query some JSON by `jsonPath`. It always return List, even for single match. * Unlike `json.circeQuery(stringPath)` it gives error if JNothing was given */ - def query(jsonPath: String, json: Json): Either[String, List[Json]] = { - val pojo = convertToJson(json) - GatlingJsonPath.query(jsonPath, pojo) match { - case Right(iterator) => iterator.map(anyToJson).toList.asRight - case Left(error) => error.reason.asLeft + def query(jsonPath: String, json: Json): Either[String, List[Json]] = + Either.catchNonFatal { + JaywayJsonPath + .using(JsonPathConf) + .parse(circeToJackson(json)) + .read[ArrayNode](jsonPath) + } match { + case Right(jacksonArrayNode) => + arrayNodeToCirce(jacksonArrayNode).asRight + case Left(error) => + error.getMessage.asLeft } - } /** * Precompile JsonPath query + * * @param query JsonPath query as a string * @return valid JsonPath object either error message */ - def compileQuery(query: String): Either[String, GatlingJsonPath] = - GatlingJsonPath.compile(query).leftMap(_.reason) + def compileQuery(query: String): Either[String, JaywayJsonPath] = + Either.catchNonFatal(JaywayJsonPath.compile(query)).leftMap(_.getMessage) /** * Wrap list of values into JSON array if several values present @@ -70,12 +85,6 @@ object JsonPath { case many => Json.fromValues(many) } - /** - * Convert POJO to JValue with `jackson` mapper - * @param any raw JVM type representing JSON - * @return Json - */ - private[utils] def anyToJson(any: Any): Json = - if (any == null) Json.Null - else CirceUtils.mapper.convertValue(any, classOf[Json]) + private def arrayNodeToCirce(jacksonArrayNode: ArrayNode): List[Json] = + jacksonArrayNode.elements().asScala.toList.map(jacksonToCirce) } diff --git a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/apirequest/InputSpec.scala b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/apirequest/InputSpec.scala index 3f88c50d8..419999865 100644 --- a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/apirequest/InputSpec.scala +++ b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/enrichments/registry/apirequest/InputSpec.scala @@ -223,7 +223,8 @@ class InputSpec extends Specification with ValidatedMatchers with CatsEffect { unstructEvent = None ) templateContext must beInvalid.like { - case errors => errors.toList must have length 3 + case errors => + errors.toList must have length 2 // TODO it's not 3 anymore because `"*.invalidJsonPath"` path doesn't fail during jsonpath compilation } } diff --git a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/utils/JsonPathSpec.scala b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/utils/JsonPathSpec.scala index eab03196d..638ac60cb 100644 --- a/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/utils/JsonPathSpec.scala +++ b/modules/common/src/test/scala/com.snowplowanalytics.snowplow.enrich.common/utils/JsonPathSpec.scala @@ -21,7 +21,7 @@ class JsonPathSpec extends Specification { test query of non-exist value $e2 test query of empty array $e3 test primitive JSON type (JString) $e6 - invalid JSONPath (JQ syntax) must fail $e4 + test invalid JSONPath (JQ syntax) $e4 invalid JSONPath must fail $e5 test query of long $e7 test query of integer $e8 @@ -81,13 +81,12 @@ class JsonPathSpec extends Specification { JsonPath.query("$.store.unicorns", someJson) must beRight(Nil) def e4 = - JsonPath.query(".notJsonPath", someJson) must beLeft.like { - case f => f must beEqualTo("'$' expected but '.' found") - } + //TODO it's not failure anymore because `.notJsonPath` is not treated as invalid jsonpath by jayway + JsonPath.query(".notJsonPath", someJson) must beRight(Nil) def e5 = JsonPath.query("$.store.book[a]", someJson) must beLeft.like { - case f => f must beEqualTo("':' expected but 'a' found") + case f => f must beEqualTo("Could not parse token starting at position 12. Expected ?, ', 0-9, * ") } def e6 = diff --git a/project/BuildSettings.scala b/project/BuildSettings.scala index 7001f53cf..1d7aabf3c 100644 --- a/project/BuildSettings.scala +++ b/project/BuildSettings.scala @@ -28,7 +28,7 @@ object BuildSettings { lazy val projectSettings = Seq( organization := "com.snowplowanalytics", - scalaVersion := "2.12.15", + scalaVersion := "2.13.12", licenses += ("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0.html")) ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 6397f3f80..f707d325f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -58,7 +58,6 @@ object Dependencies { val circeJackson = "0.14.0" val scalaForex = "3.0.0" val scalaWeather = "2.0.0" - val gatlingJsonpath = "0.6.14" val scalaUri = "1.5.1" val badRows = "2.3.0" val igluClient = "3.1.0" @@ -132,7 +131,6 @@ object Dependencies { val circeOptics = "io.circe" %% "circe-optics" % V.circeOptics val circeJackson = "io.circe" %% "circe-jackson210" % V.circeJackson val scalaUri = "io.lemonlabs" %% "scala-uri" % V.scalaUri - val gatlingJsonpath = "io.gatling" %% "jsonpath" % V.gatlingJsonpath val scalaForex = "com.snowplowanalytics" %% "scala-forex" % V.scalaForex val refererParser = "com.snowplowanalytics" %% "scala-referer-parser" % V.refererParser val maxmindIplookups = "com.snowplowanalytics" %% "scala-maxmind-iplookups" % V.maxmindIplookups @@ -232,7 +230,6 @@ object Dependencies { scalaUri, scalaForex, scalaWeather, - gatlingJsonpath, badRows, igluClient, snowplowRawEvent,