diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala index 9ac04c42a..4f4e46793 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocs140.scala @@ -11,7 +11,7 @@ import code.apicollectionendpoint.MappedApiCollectionEndpointsProvider import code.util.Helper.{MdcLoggable, SILENCE_IS_GOLDEN} import com.openbankproject.commons.model.enums.ContentParam.{DYNAMIC, STATIC} import com.openbankproject.commons.util.{ApiVersion, ApiVersionStatus} -import net.liftweb.http.{GetRequest, InMemoryResponse, PlainTextResponse, Req, S, StreamingResponse} +import net.liftweb.http.{GetRequest, InMemoryResponse, PlainTextResponse, Req, S} object ResourceDocs140 extends OBPRestHelper with ResourceDocsAPIMethods with MdcLoggable { @@ -198,15 +198,10 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md ) val cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey) - if (cacheValueFromRedis.isDefined) { - // If we already have a cached YAML string, serve it as before. - val yamlString = cacheValueFromRedis.get - val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType, (ResponseHeader.`Correlation-Id` -> getCorrelationId())) - val bytes = yamlString.getBytes("UTF-8") - InMemoryResponse(bytes, headers, Nil, 200) + val yamlString = if (cacheValueFromRedis.isDefined) { + cacheValueFromRedis.get } else { - // Generate OpenAPI JSON JValue (this may be large) but stream YAML generation to avoid - // building a huge YAML string in memory. + // Generate OpenAPI JSON and convert to YAML val openApiJValue = try { val resourceDocsJsonFiltered = locale match { case _ if (apiCollectionIdParam.isDefined) => @@ -236,30 +231,14 @@ object ResourceDocs300 extends OBPRestHelper with ResourceDocsAPIMethods with Md throw e } - // Attempt to obtain an InputStream that streams YAML - YAMLUtils.jValueToYAMLInputStream(openApiJValue) match { - case scala.util.Success(in) => - val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType, (ResponseHeader.`Correlation-Id` -> getCorrelationId())) - // StreamingResponse takes a function that returns an InputStream when called by Lift - // StreamingResponse constructor expects: data, onEnd, size, headers, cookies, code - // Provide the InputStream directly as `data`, an onEnd that closes the stream, - // -1L for unknown size, the headers, empty cookies list, and HTTP 200 code. - StreamingResponse(in, () => { try { in.close() } catch { case _: Throwable => () } }, -1L, headers, Nil, 200) - case scala.util.Failure(e) => - logger.error(s"Error streaming OpenAPI YAML: ${e.getMessage}", e) - // Fallback: try a safe conversion to a string (may be memory heavy) and return that, or an error. - // We attempt a safe string conversion as a last resort. - val yamlResult = YAMLUtils.jValueToYAMLSafe(openApiJValue, s"# Error converting OpenAPI to YAML: ${openApiJValue.toString}") - if (yamlResult.nonEmpty) { - Caching.setStaticSwaggerDocCache(cacheKey, yamlResult) - val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType, (ResponseHeader.`Correlation-Id` -> getCorrelationId())) - val bytes = yamlResult.getBytes("UTF-8") - InMemoryResponse(bytes, headers, Nil, 200) - } else { - PlainTextResponse(s"Error generating OpenAPI YAML: ${e.getMessage}", 500) - } - } + val yamlResult = YAMLUtils.jValueToYAMLSafe(openApiJValue, s"# Error converting OpenAPI to YAML: ${openApiJValue.toString}") + Caching.setStaticSwaggerDocCache(cacheKey, yamlResult) + yamlResult } + + val headers = List("Content-Type" -> YAMLUtils.getYAMLContentType, (ResponseHeader.`Correlation-Id` -> getCorrelationId())) + val bytes = yamlString.getBytes("UTF-8") + InMemoryResponse(bytes, headers, Nil, 200) } } catch { case _: Exception => diff --git a/obp-api/src/main/scala/code/api/util/YAMLUtils.scala b/obp-api/src/main/scala/code/api/util/YAMLUtils.scala index ce7a00eab..16714ee50 100644 --- a/obp-api/src/main/scala/code/api/util/YAMLUtils.scala +++ b/obp-api/src/main/scala/code/api/util/YAMLUtils.scala @@ -28,22 +28,17 @@ package code.api.util import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} import com.fasterxml.jackson.dataformat.yaml.YAMLFactory -import com.fasterxml.jackson.core.{JsonGenerator, JsonFactory} -import net.liftweb.json.JsonAST.{JObject, JArray, JBool, JNull, JNothing, JDouble, JInt, JString, JField, JValue} +import net.liftweb.json.JsonAST.JValue import net.liftweb.json._ import net.liftweb.json.compactRender import code.util.Helper.MdcLoggable import scala.util.{Try, Success, Failure} -import java.io.{OutputStream, InputStream, PipedInputStream, PipedOutputStream} -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global /** * Utility object for YAML conversion operations * * This utility provides methods to convert Lift's JValue objects to YAML format - * using Jackson's YAML support. It provides both simple string-based conversion - * and streaming conversion APIs to avoid building huge intermediate strings. + * using Jackson's YAML support. */ object YAMLUtils extends MdcLoggable { @@ -51,121 +46,21 @@ object YAMLUtils extends MdcLoggable { private val yamlMapper = new ObjectMapper(new YAMLFactory()) /** - * Convert a Lift JValue by writing it token-by-token to a Jackson JsonGenerator. - * This avoids creating a very large intermediate JSON string. - */ - private def writeJValueToGenerator(gen: JsonGenerator, j: JValue): Unit = { - j match { - case JObject(fields) => - gen.writeStartObject() - fields.foreach { - case JField(name, value) => - gen.writeFieldName(name) - writeJValueToGenerator(gen, value) - } - gen.writeEndObject() - case JArray(items) => - gen.writeStartArray() - items.foreach(item => writeJValueToGenerator(gen, item)) - gen.writeEndArray() - case JString(s) => - gen.writeString(s) - case JInt(num) => - // Jackson supports BigInteger via writeNumber(String) - gen.writeNumber(num.toString) - case JDouble(d) => - gen.writeNumber(d) - // JDecimal is not available in this Lift version; high-precision decimals will - // fall through to the fallback case (written via compactRender) or be represented - // as JDouble/JInt depending on creation site. - case JBool(b) => - gen.writeBoolean(b) - case JNull | JNothing => - gen.writeNull() - case other => - // fallback: write compact rendering as string - gen.writeString(compactRender(other)) - } - } - - /** - * Stream a JValue as YAML into a supplied OutputStream. - * The caller is responsible for closing the OutputStream when appropriate. - * - * @param jValue the JValue to serialize - * @param out the OutputStream to write YAML bytes to - * @return Try[Unit] indicating success or failure - */ - def jValueToYAMLStream(jValue: JValue, out: OutputStream): Try[Unit] = { - Try { - val gen = yamlMapper.getFactory.createGenerator(out) - try { - writeJValueToGenerator(gen, jValue) - gen.flush() - } finally { - // Do not close the provided OutputStream here; just close the generator - try { gen.close() } catch { case _: Throwable => } - } - }.recoverWith { - case ex: Exception => - logger.error(s"Failed to stream JValue to YAML: ${ex.getMessage}", ex) - Failure(new RuntimeException(s"YAML streaming failed: ${ex.getMessage}", ex)) - } - } - - /** - * Provide an InputStream that streams YAML representation of the provided JValue. - * Writing is performed on a background thread into a PipedOutputStream connected to - * the returned PipedInputStream. Caller must close the InputStream when done. - * - * @param jValue the JValue to serialize - * @return Try[InputStream] that will yield the YAML bytes - */ - def jValueToYAMLInputStream(jValue: JValue): Try[InputStream] = { - Try { - val in = new PipedInputStream(64 * 1024) - val out = new PipedOutputStream(in) - // Write in a background thread so the caller can read as we generate - val writerThread = new Thread(new Runnable { - override def run(): Unit = { - try { - jValueToYAMLStream(jValue, out) match { - case Success(_) => // done - case Failure(e) => - // attempt to write an error message into the stream so the reader sees something useful - try { - val msg = s"# Error generating YAML: ${e.getMessage}\n" - out.write(msg.getBytes("UTF-8")) - } catch { case _: Throwable => } - } - } finally { - try { out.close() } catch { case _: Throwable => } - } - } - }, "yaml-stream-writer") - writerThread.setDaemon(true) - writerThread.start() - in - }.recoverWith { - case ex: Exception => - logger.error(s"Failed to create YAML InputStream: ${ex.getMessage}", ex) - Failure(new RuntimeException(s"Failed to create YAML InputStream: ${ex.getMessage}", ex)) - } - } - - /** - * Converts a JValue to YAML string (keeps compatibility). This method uses the streaming - * generator internally but still accumulates into a String (for callers that need a String). - * Prefer streaming APIs for large documents. + * Converts a JValue to YAML string * * @param jValue The Lift JValue to convert * @return Try containing the YAML string or error */ def jValueToYAML(jValue: JValue): Try[String] = { Try { - val baos = new java.io.ByteArrayOutputStream() - jValueToYAMLStream(jValue, baos).get - baos.toString("UTF-8") + // First convert JValue to JSON string + val jsonString = compactRender(jValue) + + // Parse JSON string to Jackson JsonNode + val jsonNode: JsonNode = jsonMapper.readTree(jsonString) + + // Convert JsonNode to YAML string + yamlMapper.writeValueAsString(jsonNode) }.recoverWith { case ex: Exception => logger.error(s"Failed to convert JValue to YAML: ${ex.getMessage}", ex)