package com.banesco.common.infraestructure.helpers; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; import jakarta.xml.bind.JAXBException; import jakarta.xml.bind.Marshaller; import javax.xml.namespace.QName; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; import java.util.logging.Logger; /** * Clase de utilidad para trabajar con XML utilizando JAXB. */ public class XmlHelper { private static final Logger logger = Logger.getLogger(XmlHelper.class.getName()); // Cache para las instancias de JAXBContext. // La creación de JAXBContext es una operación costosa, por lo que cachearlas // mejora significativamente el rendimiento en aplicaciones que procesan XML frecuentemente. // Se utiliza ConcurrentHashMap para garantizar la seguridad en entornos con múltiples hilos (thread-safe). private static final ConcurrentMap, JAXBContext> JAXB_CONTEXT_CACHE = new ConcurrentHashMap<>(); /** * Constructor privado para evitar la instanciación de esta clase de utilidad. */ private XmlHelper() { throw new UnsupportedOperationException("Esta es una clase de utilidad y no puede ser instanciada."); } /** * Imprime el contenido XML de un objeto JAXBElement en el log. * Utiliza JAXB para serializar el objeto a XML y lo formatea para una mejor legibilidad. * * @param element El JAXBElement que contiene el objeto a serializar a XML. * @param traceId requestID. * @throws JAXBException Si ocurre un error durante la serialización JAXB. */ public static void printXml(JAXBElement element, String traceId) throws JAXBException { // Verifica si el elemento es nulo para evitar NullPointerException Objects.requireNonNull(element, "El JAXBElement no puede ser nulo."); // Obtiene el tipo declarado del elemento para crear el contexto JAXB. // Esto asegura que JAXB conozca la clase raíz del objeto que se va a serializar. Class declaredType = element.getDeclaredType(); // Crea una instancia de JAXBContext para el tipo declarado. // JAXBContext es costoso de crear, en aplicaciones de alto rendimiento // se suele cachear o reutilizar. Para este caso de log, es aceptable crearlo aquí. JAXBContext ctx = JAXBContext.newInstance(declaredType); // Crea un Marshaller, que es responsable de convertir objetos Java a XML. Marshaller marshaller = ctx.createMarshaller(); // Configura la propiedad para que el XML de salida esté formateado (indentado). // Esto hace que el XML sea mucho más legible en los logs. marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // Configura la propiedad para que el XML no incluya la declaración XML (). // Esto puede ser útil si solo quieres el contenido XML puro en el log, // pero a menudo es mejor dejarlo si quieres un XML completo y válido. // marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); // Usa ByteArrayOutputStream para capturar la salida XML en memoria. // Esto es necesario porque Marshaller escribe en un OutputStream, // y necesitamos obtener el String resultante para el log. try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { // Realiza la serialización del JAXBElement al OutputStream. marshaller.marshal(element, outputStream); // Convierte el contenido del ByteArrayOutputStream a un String. // Es crucial especificar la codificación (UTF-8 es lo más común) // para asegurar que los caracteres se interpreten correctamente. String xmlString = outputStream.toString(StandardCharsets.UTF_8.name()); // Loggea el XML generado. logger.log(Level.INFO, traceId + " - XML del servicio SOAP:\n{0}", xmlString); } catch (IOException e) { // Manejo de excepción si hay un problema con el ByteArrayOutputStream (poco probable en este caso). logger.log(Level.SEVERE, "Error al procesar el OutputStream para el XML", e); } } /** * Crea un JAXBElement y lo convierte a un String XML en un solo paso. *

* Este es un método de conveniencia de alto nivel que simplifica la generación de XML * al encargarse de la creación del QName y del JAXBElement internamente. * * @param namespaceUri El URI del namespace para el elemento raíz XML. * @param localPart El nombre local del elemento raíz XML (ej: "readCustomerProducts_Rq"). * @param declaredType El .class del objeto JAXB que se está serializando. * @param value El objeto de datos JAXB (payload) que se va a convertir. * @param El tipo del objeto de datos. * @return Un String con la representación XML formateada del objeto. * @throws JAXBException Si ocurre algún error durante la serialización. */ public static String toXml(String namespaceUri, String localPart, Class declaredType, T value) throws JAXBException { // 1. Crear el QName (Qualified Name) a partir del namespace y el nombre local. QName qName = new QName(namespaceUri, localPart); // 2. Crear el JAXBElement, que envuelve el objeto de datos con su información de esquema XML. JAXBElement jaxbElement = new JAXBElement<>(qName, declaredType, value); // 3. Reutilizar el método original para hacer la conversión a String. return toXml(jaxbElement); } /** * Convierte un objeto JAXBElement genérico en una representación de String con formato XML. *

* Este método es genérico y puede manejar cualquier tipo de objeto contenido dentro de un JAXBElement. * Utiliza una caché para las instancias de JAXBContext, optimizando el rendimiento al evitar * la recreación costosa de estos objetos. * * @param element El objeto JAXBElement que se va a convertir a XML. No puede ser nulo. * @param El tipo del valor contenido en el JAXBElement. * @return Un String que representa el objeto en formato XML, indentado para fácil lectura. * @throws JAXBException Si ocurre un error durante el proceso de marshalling (conversión de objeto a XML). * Esto puede suceder si el contexto JAXB no se puede crear para la clase dada * o si hay un problema durante la serialización. */ public static String toXml(JAXBElement element) throws JAXBException { if (element == null) { // Es preferible devolver un string vacío o lanzar una excepción si el elemento es nulo. // Para un helper, lanzar IllegalArgumentException es una buena práctica. throw new IllegalArgumentException("El JAXBElement de entrada no puede ser nulo."); } // Obtiene la clase del objeto contenido en el JAXBElement (por ejemplo, ReadCustomerProductsRq.class). Class objectClass = element.getDeclaredType(); // Obtiene el JAXBContext de la caché o lo crea si no existe. // computeIfAbsent es una operación atómica que asegura que el contexto se cree solo una vez por clase. JAXBContext context = JAXB_CONTEXT_CACHE.computeIfAbsent(objectClass, clazz -> { try { return JAXBContext.newInstance(clazz); } catch (JAXBException e) { // Envuelve la excepción comprobada en una excepción de runtime para usarla dentro de la lambda. throw new RuntimeException("Error al crear la instancia de JAXBContext para la clase: " + clazz.getName(), e); } }); // Crea un Marshaller a partir del contexto. El Marshaller es el que realiza la conversión. Marshaller marshaller = context.createMarshaller(); // Configura la propiedad para que la salida XML esté formateada con indentación y saltos de línea. // Esto es clave para que el XML sea legible en los logs. marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); // Se utiliza StringWriter para escribir la salida XML en un String. StringWriter stringWriter = new StringWriter(); // Realiza la serialización del objeto JAXBElement y escribe el resultado en el StringWriter. marshaller.marshal(element, stringWriter); // Devuelve el contenido del StringWriter como un String. return stringWriter.toString(); } }