From 5d0cebbf42800908a7fba2491891e06b7056b90a Mon Sep 17 00:00:00 2001 From: dafreels Date: Wed, 18 Jan 2023 10:36:40 -0500 Subject: [PATCH] #345 Forced all JSON parsing through a single object --- .../acxiom/metalus/PipelineStepMapper.scala | 19 ++- .../applications/ApplicationUtils.scala | 55 +++++---- .../metalus/context/Json4sContext.scala | 66 +---------- .../acxiom/metalus/flow/StepGroupFlow.scala | 6 +- .../acxiom/metalus/parser/JsonParser.scala | 111 +++++++++++++++--- .../applications/ApplicationTests.scala | 12 +- 6 files changed, 141 insertions(+), 128 deletions(-) diff --git a/metalus-core/src/main/scala/com/acxiom/metalus/PipelineStepMapper.scala b/metalus-core/src/main/scala/com/acxiom/metalus/PipelineStepMapper.scala index 67d29ba8..dc1d54cf 100644 --- a/metalus-core/src/main/scala/com/acxiom/metalus/PipelineStepMapper.scala +++ b/metalus-core/src/main/scala/com/acxiom/metalus/PipelineStepMapper.scala @@ -1,7 +1,7 @@ package com.acxiom.metalus import com.acxiom.metalus.applications.Json4sSerializers -import com.acxiom.metalus.context.Json4sContext +import com.acxiom.metalus.parser.JsonParser import com.acxiom.metalus.utils.{ReflectionUtils, ScalaScriptEngine} import org.apache.log4j.Logger @@ -268,16 +268,15 @@ trait PipelineStepMapper { parameter: Parameter, pipelineContext: PipelineContext): Option[Any] = { val workingMap = map.asInstanceOf[Map[String, Any]] - val jsonContext = pipelineContext.contextManager.getContext("json").get.asInstanceOf[Json4sContext] val paramSerializers = parameter.json4sSerializers Some(if (parameter.className.isDefined && parameter.className.get.nonEmpty) { // Skip the embedded variable mapping if this is a step-group pipeline parameter // TODO [2.0 Review] Pipeline.category has been removed if (workingMap.getOrElse("category", "pipeline").asInstanceOf[String] == "step-group") { - jsonContext.parseJson(jsonContext.serializeJson(workingMap), parameter.className.get, paramSerializers) + JsonParser.parseJson(JsonParser.serialize(workingMap), parameter.className.get, paramSerializers) } else { - jsonContext.parseJson( - jsonContext.serializeJson(mapEmbeddedVariables(workingMap, pipelineContext, paramSerializers), paramSerializers), + JsonParser.parseJson( + JsonParser.serialize(mapEmbeddedVariables(workingMap, pipelineContext, paramSerializers), paramSerializers), parameter.className.get, paramSerializers) } } else { @@ -296,12 +295,11 @@ trait PipelineStepMapper { */ private def handleListParameter(list: List[_], parameter: Parameter, pipelineContext: PipelineContext): Option[Any] = { val dropNone = pipelineContext.getGlobalAs[Boolean]("dropNoneFromLists").getOrElse(true) - val jsonContext = pipelineContext.contextManager.getContext("json").get.asInstanceOf[Json4sContext] val paramSerializers = parameter.json4sSerializers Some(if (parameter.className.isDefined && parameter.className.get.nonEmpty) { list.map(value => - jsonContext.parseJson( - jsonContext.serializeJson(mapEmbeddedVariables(value.asInstanceOf[Map[String, Any]], + JsonParser.parseJson( + JsonParser.serialize(mapEmbeddedVariables(value.asInstanceOf[Map[String, Any]], pipelineContext, paramSerializers)), parameter.className.get, paramSerializers)) } else if (list.nonEmpty && list.head.isInstanceOf[Map[_, _]]) { list.map(value => { @@ -336,14 +334,13 @@ trait PipelineStepMapper { private[metalus] def mapEmbeddedVariables(classMap: Map[String, Any], pipelineContext: PipelineContext, json4sSerializers: Option[Json4sSerializers]): Map[String, Any] = { - val jsonContext = pipelineContext.contextManager.getContext("json").get.asInstanceOf[Json4sContext] classMap.foldLeft(classMap)((map, entry) => { entry._2 match { case s: String if containsSpecialCharacters(s) => map + (entry._1 -> getBestValue(s.split("\\|\\|"), Parameter(), pipelineContext)) case m: Map[String, Any] if m.contains("className")=> - map + (entry._1 -> jsonContext.parseJson( - jsonContext.serializeJson( + map + (entry._1 -> JsonParser.parseJson( + JsonParser.serialize( mapEmbeddedVariables(m("object").asInstanceOf[Map[String, Any]], pipelineContext, json4sSerializers)), m("className").asInstanceOf[String])) case m: Map[_, _] => diff --git a/metalus-core/src/main/scala/com/acxiom/metalus/applications/ApplicationUtils.scala b/metalus-core/src/main/scala/com/acxiom/metalus/applications/ApplicationUtils.scala index 93f83939..792eb6a5 100644 --- a/metalus-core/src/main/scala/com/acxiom/metalus/applications/ApplicationUtils.scala +++ b/metalus-core/src/main/scala/com/acxiom/metalus/applications/ApplicationUtils.scala @@ -5,8 +5,6 @@ import com.acxiom.metalus.context.Json4sContext import com.acxiom.metalus.parser.JsonParser import com.acxiom.metalus.utils.ReflectionUtils import org.apache.log4j.Logger -import org.json4s.Formats -import org.json4s.native.Serialization /** * Provides a set of utility functions for working with Application metadata @@ -37,17 +35,16 @@ object ApplicationUtils { val validateArgumentTypes = parameters.getOrElse(Map()).getOrElse("validateStepParameterTypes", false).asInstanceOf[Boolean] // Create the ContextManager val contextManager = new ContextManager(application.contexts.getOrElse(Map()), parameters.getOrElse(Map())) - val jsonContext = contextManager.getContext("json").asInstanceOf[Option[Json4sContext]].get - implicit val formats: Formats = jsonContext.generateFormats(None) + val tempCtx = PipelineContext(globals, List(), contextManager = contextManager) val globalStepMapper = generateStepMapper(application.stepMapper, Some(PipelineStepMapper()), - validateArgumentTypes, credentialProvider) + validateArgumentTypes, credentialProvider, tempCtx) val rootGlobals = globals.getOrElse(Map[String, Any]()) // Create the default globals val globalListener = generatePipelineListener(application.pipelineListener, Some(pipelineListener), - validateArgumentTypes, credentialProvider) + validateArgumentTypes, credentialProvider, tempCtx) val globalPipelineParameters = generatePipelineParameters(application.pipelineParameters, Some(List[PipelineParameter]())) val pipelineManager = generatePipelineManager(application.pipelineManager, Some(PipelineManager(application.pipelineTemplates)), - validateArgumentTypes, credentialProvider).get + validateArgumentTypes, credentialProvider, tempCtx).get val initialContext = PipelineContext(Some(rootGlobals), globalPipelineParameters.get, application.stepPackages, globalStepMapper.get, globalListener, List(), pipelineManager, credentialProvider, contextManager, Map(), None) @@ -82,10 +79,11 @@ object ApplicationUtils { private def generatePipelineManager(pipelineManagerInfo: Option[ClassInfo], pipelineManager: Option[PipelineManager], validateArgumentTypes: Boolean, - credentialProvider: Option[CredentialProvider])(implicit formats: Formats): Option[PipelineManager] = { + credentialProvider: Option[CredentialProvider], + pipelineContext: PipelineContext): Option[PipelineManager] = { if (pipelineManagerInfo.isDefined && pipelineManagerInfo.get.className.isDefined) { Some(ReflectionUtils.loadClass(pipelineManagerInfo.get.className.getOrElse("com.acxiom.metalus.CachedPipelineManager"), - Some(parseParameters(pipelineManagerInfo.get, credentialProvider)), validateArgumentTypes).asInstanceOf[PipelineManager]) + Some(parseParameters(pipelineManagerInfo.get, credentialProvider, pipelineContext)), validateArgumentTypes).asInstanceOf[PipelineManager]) } else { pipelineManager } @@ -94,10 +92,11 @@ object ApplicationUtils { private def generatePipelineListener(pipelineListenerInfo: Option[ClassInfo], pipelineListener: Option[PipelineListener], validateArgumentTypes: Boolean, - credentialProvider: Option[CredentialProvider])(implicit formats: Formats): Option[PipelineListener] = { + credentialProvider: Option[CredentialProvider], + pipelineContext: PipelineContext): Option[PipelineListener] = { if (pipelineListenerInfo.isDefined && pipelineListenerInfo.get.className.isDefined) { Some(ReflectionUtils.loadClass(pipelineListenerInfo.get.className.getOrElse("com.acxiom.metalus.DefaultPipelineListener"), - Some(parseParameters(pipelineListenerInfo.get, credentialProvider)), validateArgumentTypes).asInstanceOf[PipelineListener]) + Some(parseParameters(pipelineListenerInfo.get, credentialProvider, pipelineContext)), validateArgumentTypes).asInstanceOf[PipelineListener]) } else { pipelineListener } @@ -106,10 +105,11 @@ object ApplicationUtils { private def generateStepMapper(stepMapperInfo: Option[ClassInfo], stepMapper: Option[PipelineStepMapper], validateArgumentTypes: Boolean, - credentialProvider: Option[CredentialProvider])(implicit formats: Formats): Option[PipelineStepMapper] = { + credentialProvider: Option[CredentialProvider], + pipelineContext: PipelineContext): Option[PipelineStepMapper] = { if (stepMapperInfo.isDefined && stepMapperInfo.get.className.isDefined) { Some(ReflectionUtils.loadClass(stepMapperInfo.get.className.getOrElse("com.acxiom.metalus.DefaultPipelineStepMapper"), - Some(parseParameters(stepMapperInfo.get, credentialProvider)), validateArgumentTypes).asInstanceOf[PipelineStepMapper]) + Some(parseParameters(stepMapperInfo.get, credentialProvider, pipelineContext)), validateArgumentTypes).asInstanceOf[PipelineStepMapper]) } else { stepMapper } @@ -128,9 +128,9 @@ object ApplicationUtils { rootGlobals: Map[String, Any], defaultGlobals: Option[Map[String, Any]], pipelineContext: PipelineContext, - merge: Boolean = false)(implicit formats: Formats): Option[Map[String, Any]] = { + merge: Boolean = false): Option[Map[String, Any]] = { globals.map { baseGlobals => - val result = baseGlobals.foldLeft(rootGlobals)((rootMap, entry) => parseValue(rootMap, entry._1, entry._2, Some(pipelineContext))) + val result = baseGlobals.foldLeft(rootGlobals)((rootMap, entry) => parseValue(rootMap, entry._1, entry._2, pipelineContext)) if (merge) { defaultGlobals.getOrElse(Map[String, Any]()) ++ result } else { @@ -139,9 +139,10 @@ object ApplicationUtils { }.orElse(defaultGlobals) } - private def parseParameters(classInfo: ClassInfo, credentialProvider: Option[CredentialProvider])(implicit formats: Formats): Map[String, Any] = { + private def parseParameters(classInfo: ClassInfo, credentialProvider: Option[CredentialProvider], pipelineContext: PipelineContext): Map[String, Any] = { classInfo.parameters.getOrElse(Map[String, Any]()) - .foldLeft(Map[String, Any]("credentialProvider" -> credentialProvider))((rootMap, entry) => parseValue(rootMap, entry._1, entry._2)) + .foldLeft(Map[String, Any]("credentialProvider" -> credentialProvider))((rootMap, entry) => + parseValue(rootMap, entry._1, entry._2, pipelineContext)) } /** @@ -151,36 +152,40 @@ object ApplicationUtils { * @param key The key to use when adding teh result to the rootMap * @param value The value to be parsed * @param ctx The PipelineContext that will provide the mapper - * @param formats Implicit formats used for JSON conversion * @return A map containing the converted value */ - def parseValue(rootMap: Map[String, Any], key: String, value: Any, ctx: Option[PipelineContext] = None)(implicit formats: Formats): Map[String, Any] = { + def parseValue(rootMap: Map[String, Any], key: String, value: Any, ctx: PipelineContext): Map[String, Any] = { + val jsonContext = ctx.contextManager.getContext("json").asInstanceOf[Option[Json4sContext]].get value match { case map: Map[String, Any] if map.contains("className") => - val mapEmbedded = map.get("mapEmbeddedVariables").exists(_.toString.toBoolean) && ctx.isDefined + val mapEmbedded = map.get("mapEmbeddedVariables").exists(_.toString.toBoolean) val finalMap = if (mapEmbedded) { - ctx.get.parameterMapper.mapEmbeddedVariables(map("object").asInstanceOf[Map[String, Any]], ctx.get, None) + ctx.parameterMapper.mapEmbeddedVariables(map("object").asInstanceOf[Map[String, Any]], ctx, None) } else { map("object").asInstanceOf[Map[String, Any]] } - val obj = JsonParser.parseJson(Serialization.write(finalMap), map("className").asInstanceOf[String]) + val obj = JsonParser.parseJson( + JsonParser.serialize(finalMap, jsonContext.serializers), + map("className").asInstanceOf[String], jsonContext.serializers) rootMap + (key -> obj) case listMap: List[Any] => val obj = listMap.map { case m: Map[String, Any] => if (m.contains("className")) { - val mapEmbedded = m.get("mapEmbeddedVariables").exists(_.toString.toBoolean) && ctx.isDefined + val mapEmbedded = m.get("mapEmbeddedVariables").exists(_.toString.toBoolean) val map = if (m.contains("parameters")) { m("parameters").asInstanceOf[Map[String, Any]] } else { m("object").asInstanceOf[Map[String, Any]] } val finalMap = if (mapEmbedded) { - ctx.get.parameterMapper.mapEmbeddedVariables(map, ctx.get, None) + ctx.parameterMapper.mapEmbeddedVariables(map, ctx, None) } else { map } - JsonParser.parseJson(Serialization.write(finalMap), m("className").asInstanceOf[String]) + JsonParser.parseJson( + JsonParser.serialize(finalMap, jsonContext.serializers), + m("className").asInstanceOf[String], jsonContext.serializers) } else { m } diff --git a/metalus-core/src/main/scala/com/acxiom/metalus/context/Json4sContext.scala b/metalus-core/src/main/scala/com/acxiom/metalus/context/Json4sContext.scala index bd048d12..315e12a1 100644 --- a/metalus-core/src/main/scala/com/acxiom/metalus/context/Json4sContext.scala +++ b/metalus-core/src/main/scala/com/acxiom/metalus/context/Json4sContext.scala @@ -2,6 +2,7 @@ package com.acxiom.metalus.context import com.acxiom.metalus.Context import com.acxiom.metalus.applications.Json4sSerializers +import com.acxiom.metalus.parser.JsonParser import com.acxiom.metalus.parser.JsonParser.StepSerializer import com.acxiom.metalus.utils.ReflectionUtils import org.json4s.ext.{EnumNameSerializer, EnumSerializer} @@ -17,71 +18,12 @@ import org.json4s.{CustomSerializer, DefaultFormats, Extraction, Formats, FullTy * @param jsonSerializers Contains ClassInfo objects for custom serializers and enum serializers. */ class Json4sContext(jsonSerializers: Option[Map[String, Any]] = None) extends Context { - private val localSerializers = { + val serializers: Option[Json4sSerializers] = { if (jsonSerializers.isDefined) { - val jsonString = serializeJson(jsonSerializers.get) - Some(parseJson(jsonString, "com.acxiom.metalus.applications.Json4sSerializers").asInstanceOf[Json4sSerializers]) + val jsonString = JsonParser.serialize(jsonSerializers.get) + Some(JsonParser.parseJson(jsonString, "com.acxiom.metalus.applications.Json4sSerializers").asInstanceOf[Json4sSerializers]) } else { None } } - - /** - * Parse the provided JSON string into an object of the provided class name. - * - * @param json The JSON string to parse. - * @param className The fully qualified name of the class. - * @return An instantiation of the class from the provided JSON. - */ - def parseJson(json: String, className: String, serializers: Option[Json4sSerializers] = None): Any = { - implicit val formats: Formats = generateFormats(serializers) - val clazz = Class.forName(className) - val scalaType = Reflector.scalaTypeOf(clazz) - Extraction.extract(parse(json), scalaType) - } - - /** - * Convert the provided obj into a JSON string. - * @param obj The object to convert. - * @return A JSON string representation of the object. - */ - def serializeJson(obj: Any, serializers: Option[Json4sSerializers] = None): String = { - implicit val formats: Formats = generateFormats(serializers) - Serialization.write(obj) - } - - def generateFormats(json4sSerializers: Option[Json4sSerializers]): Formats = { - getDefaultSerializers(json4sSerializers).map { j => - val enumNames = j.enumNameSerializers.map(_.map(ci => new EnumNameSerializer(ReflectionUtils.loadEnumeration(ci.className.getOrElse(""))))) - .getOrElse(List()) - val enumIds = j.enumIdSerializers.map(_.map(ci => new EnumSerializer(ReflectionUtils.loadEnumeration(ci.className.getOrElse(""))))) - .getOrElse(List()) - val customSerializers = j.customSerializers.map(_.map { ci => - ReflectionUtils.loadClass(ci.className.getOrElse(""), ci.parameters).asInstanceOf[CustomSerializer[_]] - }).getOrElse(List()) - val baseFormats: Formats = if (j.hintSerializers.isDefined && j.hintSerializers.get.nonEmpty) { - Serialization.formats(FullTypeHints( - j.hintSerializers.map(_.map { hint => Class.forName(hint.className.getOrElse("")) }).get)) - } else { - DefaultFormats - } - (customSerializers ++ enumNames ++ enumIds).foldLeft(baseFormats: Formats) { (formats, custom) => - formats + custom - } - }.getOrElse(DefaultFormats) + new StepSerializer - } - - /** - * This method is responsible for ensuring that there is always a set of serializers or None. - * @param serializers The serializerrs to verify. - * @return An option to use when generrating formats - */ - private def getDefaultSerializers(serializers: Option[Json4sSerializers]) = - if (serializers.isDefined) { - serializers - } else if (Option(localSerializers).isDefined) { - localSerializers - } else { - None - } } diff --git a/metalus-core/src/main/scala/com/acxiom/metalus/flow/StepGroupFlow.scala b/metalus-core/src/main/scala/com/acxiom/metalus/flow/StepGroupFlow.scala index 45c3a6ef..3da04691 100644 --- a/metalus-core/src/main/scala/com/acxiom/metalus/flow/StepGroupFlow.scala +++ b/metalus-core/src/main/scala/com/acxiom/metalus/flow/StepGroupFlow.scala @@ -3,7 +3,6 @@ package com.acxiom.metalus.flow import com.acxiom.metalus._ import com.acxiom.metalus.applications.ApplicationUtils import com.acxiom.metalus.audits.{AuditType, ExecutionAudit} -import com.acxiom.metalus.context.Json4sContext import scala.runtime.BoxedUnit @@ -60,7 +59,6 @@ case class StepGroupFlow(pipeline: Pipeline, private def preparePipelineContext(parameterValues: Map[String, Any], pipelineContext: PipelineContext, subPipeline: Pipeline): PipelineContext = { - implicit val formats = pipelineContext.contextManager.getContext("json").get.asInstanceOf[Json4sContext].generateFormats(None) val updates = if (subPipeline.parameters.isDefined && subPipeline.parameters.get.inputs.isDefined && subPipeline.parameters.get.inputs.get.nonEmpty) { @@ -70,11 +68,11 @@ case class StepGroupFlow(pipeline: Pipeline, if (parameterValues.contains(input.name)) { val paramVals = parameterValues - input.name if (input.global) { - (ApplicationUtils.parseValue(tuple._1, input.name, parameterValues(input.name), Some(pipelineContext)), + (ApplicationUtils.parseValue(tuple._1, input.name, parameterValues(input.name), pipelineContext), tuple._2, paramVals) } else { (tuple._1, - tuple._2.copy(parameters = ApplicationUtils.parseValue(tuple._2.parameters, input.name, parameterValues(input.name), Some(pipelineContext))), + tuple._2.copy(parameters = ApplicationUtils.parseValue(tuple._2.parameters, input.name, parameterValues(input.name), pipelineContext)), paramVals) } } else { diff --git a/metalus-core/src/main/scala/com/acxiom/metalus/parser/JsonParser.scala b/metalus-core/src/main/scala/com/acxiom/metalus/parser/JsonParser.scala index 744e37b5..0b4a999a 100644 --- a/metalus-core/src/main/scala/com/acxiom/metalus/parser/JsonParser.scala +++ b/metalus-core/src/main/scala/com/acxiom/metalus/parser/JsonParser.scala @@ -1,16 +1,19 @@ package com.acxiom.metalus.parser import com.acxiom.metalus._ -import com.acxiom.metalus.applications.{Application, ApplicationResponse} +import com.acxiom.metalus.applications.{Application, ApplicationResponse, Json4sSerializers} +import com.acxiom.metalus.utils.ReflectionUtils +import org.json4s.ext.{EnumNameSerializer, EnumSerializer} import org.json4s.native.JsonMethods.parse import org.json4s.native.Serialization import org.json4s.reflect.Reflector -import org.json4s.{CustomSerializer, DefaultFormats, Extraction, Formats, JField, JObject} +import org.json4s.{CustomSerializer, DefaultFormats, Extraction, Formats, FullTypeHints, JField, JObject} object JsonParser { - private implicit val formats: Formats = DefaultFormats + new StepSerializer + // This should only be used by the StepSerializer + private implicit val formats: Formats = DefaultFormats - class StepSerializer extends CustomSerializer[FlowStep](_ => ( { // This is the deserializer + private class StepSerializer extends CustomSerializer[FlowStep](_ => ( { // This is the deserializer case input: JObject if input.values.contains("type") && input.values("type").toString.toLowerCase == "step-group" => val params = extractStepFields(input) :+ (input \ "pipelineId").extractOpt[String] PipelineStepGroup.getClass @@ -35,12 +38,12 @@ object JsonParser { * @param json The json string representing an Application. * @return An Application object. */ - def parseApplication(json: String): Application = { + def parseApplication(json: String, serializers: Option[Json4sSerializers] = None): Application = { // See if this is an application response if (json.indexOf("application\"") > -1 && json.indexOf("application") < 15) { - parseJson(json, "com.acxiom.metalus.applications.ApplicationResponse").asInstanceOf[ApplicationResponse].application + parseJson(json, "com.acxiom.metalus.applications.ApplicationResponse", serializers).asInstanceOf[ApplicationResponse].application } else { - parseJson(json, "com.acxiom.metalus.applications.Application").asInstanceOf[Application] + parseJson(json, "com.acxiom.metalus.applications.Application", serializers).asInstanceOf[Application] } } @@ -51,40 +54,78 @@ object JsonParser { * @param pipelineJson The JSON string containing the Pipeline metadata * @return A List of Pipeline objects */ - def parsePipelineJson(pipelineJson: String): Option[List[Pipeline]] = { + def parsePipelineJson(pipelineJson: String, serializers: Option[Json4sSerializers] = None): Option[List[Pipeline]] = { val json = if (pipelineJson.nonEmpty && pipelineJson.trim()(0) != '[') { s"[$pipelineJson]" } else { pipelineJson } + implicit val formats: Formats = generateFormats(serializers) parse(json).extractOpt[List[Pipeline]] } + /** + * Parse the provided JSON string into an object of the provided class name. + * + * @param json The JSON string to parse. + * @param className The fully qualified name of the class. + * @return An instantiation of the class from the provided JSON. + */ + def parseJson(json: String, className: String, serializers: Option[Json4sSerializers] = None): Any = { + val clazz = Class.forName(className) + val scalaType = Reflector.scalaTypeOf(clazz) + implicit val formats: Formats = generateFormats(serializers) + Extraction.extract(parse(json), scalaType) + } + + def parseJsonList(json: String, className: String, serializers: Option[Json4sSerializers] = None): Any = { + val clazz = Class.forName(className) + val scalaType = Reflector.scalaTypeOf(clazz) + implicit val formats: Formats = generateFormats(serializers) + parse(json).extract[List[Any]].map(i => { + Extraction.extract(parse(Serialization.write(i)), scalaType) + }) + } + + def parseMap(json: String, serializers: Option[Json4sSerializers] = None): Map[String, Any] = { + implicit val formats: Formats = generateFormats(serializers) + parse(json).extract[Map[String, Any]] + } + + def parseJsonString(json: String, serializers: Option[Json4sSerializers] = None): Any = { + implicit val formats: Formats = generateFormats(serializers) + parse(json) + } + /** * Takes a single Pipeline and serializes it to a json string * @param pipeline The pipeline to serialize * @return A string representing the JSON */ - def serializePipeline(pipeline: Pipeline): String = Serialization.write(pipeline) + def serializePipeline(pipeline: Pipeline, serializers: Option[Json4sSerializers] = None): String = { + implicit val formats: Formats = generateFormats(serializers) + Serialization.write(pipeline) + } /** * Takes a list of pipelines and serializes to a json string * @param pipelines The list of pipelines to serialize. * @return A string representing the JSON */ - def serializePipelines(pipelines: List[Pipeline]): String = Serialization.write(pipelines) + def serializePipelines(pipelines: List[Pipeline], serializers: Option[Json4sSerializers] = None): String = { + implicit val formats: Formats = generateFormats(serializers) + Serialization.write(pipelines) + } /** - * Parse the provided JSON string into an object of the provided class name. + * Convert the provided obj into a JSON string. * - * @param json The JSON string to parse. - * @param className The fully qualified name of the class. - * @return An instantiation of the class from the provided JSON. + * @param obj The object to convert. + * @return A JSON string representation of the object. */ - def parseJson(json: String, className: String)(implicit formats: Formats): Any = { - val clazz = Class.forName(className) - val scalaType = Reflector.scalaTypeOf(clazz) - Extraction.extract(parse(json), scalaType) + def serialize(obj: Any, serializers: Option[Json4sSerializers] = None): String = { + implicit val formats: Formats = generateFormats(serializers) + Serialization.write(obj) } private def extractStepFields(input: JObject): List[Option[Any]] = { @@ -112,4 +153,38 @@ object JsonParser { JField("nextStepOnError", Extraction.decompose(step.nextStepOnError)), JField("retryLimit", Extraction.decompose(step.retryLimit))) } + + private def generateFormats(json4sSerializers: Option[Json4sSerializers]): Formats = { + getDefaultSerializers(json4sSerializers).map { j => + val enumNames = j.enumNameSerializers.map(_.map(ci => new EnumNameSerializer(ReflectionUtils.loadEnumeration(ci.className.getOrElse(""))))) + .getOrElse(List()) + val enumIds = j.enumIdSerializers.map(_.map(ci => new EnumSerializer(ReflectionUtils.loadEnumeration(ci.className.getOrElse(""))))) + .getOrElse(List()) + val customSerializers = j.customSerializers.map(_.map { ci => + ReflectionUtils.loadClass(ci.className.getOrElse(""), ci.parameters).asInstanceOf[CustomSerializer[_]] + }).getOrElse(List()) + val baseFormats: Formats = if (j.hintSerializers.isDefined && j.hintSerializers.get.nonEmpty) { + Serialization.formats(FullTypeHints( + j.hintSerializers.map(_.map { hint => Class.forName(hint.className.getOrElse("")) }).get)) + } else { + DefaultFormats + } + (customSerializers ++ enumNames ++ enumIds).foldLeft(baseFormats: Formats) { (formats, custom) => + formats + custom + } + }.getOrElse(DefaultFormats) + new StepSerializer + } + + /** + * This method is responsible for ensuring that there is always a set of serializers or None. + * + * @param serializers The serializers to verify. + * @return An option to use when generating formats + */ + private def getDefaultSerializers(serializers: Option[Json4sSerializers]) = + if (serializers.isDefined) { + serializers + } else { + None + } } diff --git a/metalus-core/src/test/scala/com/acxiom/metalus/applications/ApplicationTests.scala b/metalus-core/src/test/scala/com/acxiom/metalus/applications/ApplicationTests.scala index 7f124d14..c919d0bd 100644 --- a/metalus-core/src/test/scala/com/acxiom/metalus/applications/ApplicationTests.scala +++ b/metalus-core/src/test/scala/com/acxiom/metalus/applications/ApplicationTests.scala @@ -4,9 +4,7 @@ import com.acxiom.metalus.Constants.{NINE, TWELVE} import com.acxiom.metalus._ import com.acxiom.metalus.api.BasicAuthorization import com.acxiom.metalus.applications.Color.Color -import com.acxiom.metalus.context.Json4sContext import com.acxiom.metalus.parser.JsonParser -import com.acxiom.metalus.parser.JsonParser.StepSerializer import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock.{aResponse, get, urlPathEqualTo} import org.apache.commons.io.FileUtils @@ -181,10 +179,9 @@ class ApplicationTests extends AnyFunSpec with BeforeAndAfterAll { assert(!setup.pipelineContext.globals.get.contains("applicationConfigPath")) assert(!setup.pipelineContext.globals.get.contains("applicationConfigurationLoader")) - implicit val formats: Formats = DefaultFormats + new StepSerializer val application = JsonParser.parseJson(applicationJson, "com.acxiom.metalus.applications.Application").asInstanceOf[Application] - val json = org.json4s.native.Serialization.write(ApplicationResponse(application)) + val json = JsonParser.serialize(ApplicationResponse(application)) wireMockServer.addStubMapping(get(urlPathEqualTo("/applications/54321")) .willReturn(aResponse() @@ -279,7 +276,7 @@ class ApplicationTests extends AnyFunSpec with BeforeAndAfterAll { | } | ] |}""".stripMargin - val serializers = JsonParser.parseJson(jsonString, "com.acxiom.metalus.applications.Json4sSerializers")(DefaultFormats).asInstanceOf[Json4sSerializers] + val serializers = JsonParser.parseJson(jsonString, "com.acxiom.metalus.applications.Json4sSerializers").asInstanceOf[Json4sSerializers] assert(Option(serializers).isDefined) assert(serializers.customSerializers.isDefined) assert(serializers.customSerializers.get.nonEmpty) @@ -297,8 +294,7 @@ class ApplicationTests extends AnyFunSpec with BeforeAndAfterAll { | } | ]""".stripMargin - implicit val formats: Formats = new Json4sContext().generateFormats(Some(serializers)) - val chickenList = parse(jsonDoc).extract[List[Chicken]] + val chickenList = JsonParser.parseJsonList(jsonDoc, "com.acxiom.metalus.applications.Chicken", Some(serializers)).asInstanceOf[List[Chicken]] assert(Option(chickenList).isDefined) assert(chickenList.nonEmpty) assert(chickenList.head.isInstanceOf[Silkie]) @@ -315,7 +311,7 @@ class ApplicationTests extends AnyFunSpec with BeforeAndAfterAll { | "name": "child2" | } |}""".stripMargin - val rootMap = parse(roots).extract[Map[String, Any]] + val rootMap = JsonParser.parseMap(roots, Some(serializers)) assert(rootMap.nonEmpty) assert(rootMap("child1").isInstanceOf[Child1]) assert(rootMap("child2").isInstanceOf[Child2])