¿Cómo imprimir bastante XML desde Java?


443

Tengo una cadena de Java que contiene XML, sin avances de línea ni sangrías. Me gustaría convertirlo en una cadena con XML bien formateado. ¿Cómo hago esto?

String unformattedXml = "<tag><nested>hello</nested></tag>";
String formattedXml = new [UnknownClass]().format(unformattedXml);

Nota: Mi entrada es una cadena . Mi salida es una cadena .

(Básico) resultado simulado:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <tag>
    <nested>hello</nested>
  </tag>
</root>


10
Solo por curiosidad, ¿está enviando esta salida a un archivo XML o algo más donde la sangría realmente importa? Hace algún tiempo, estaba muy preocupado por formatear mi XML para que se mostrara correctamente ... pero después de pasar mucho tiempo en esto, me di cuenta de que tenía que enviar mi salida a un navegador web, y a cualquier navegador web relativamente moderno. en realidad mostrará el XML en una bonita estructura de árbol, por lo que podría olvidar este problema y seguir adelante. Menciono esto en caso de que usted (u otro usuario con el mismo problema) pueda haber pasado por alto el mismo detalle.
Abel Morelos

3
@Abel, guardar en archivos de texto, insertar en áreas de texto HTML y descargar en la consola con fines de depuración.
Steve McLeod

2
"poner en espera como demasiado amplio" - ¡es difícil ser más preciso de lo que es la pregunta actualmente!
Steve McLeod

Respuestas:


266
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
//initialize StreamResult with File object to save to file
StreamResult result = new StreamResult(new StringWriter());
DOMSource source = new DOMSource(doc);
transformer.transform(source, result);
String xmlString = result.getWriter().toString();
System.out.println(xmlString);

Nota: Los resultados pueden variar según la versión de Java. Busque soluciones específicas para su plataforma.


1
¿Cómo hacer para que la salida no contenga <?xml version="1.0" encoding="UTF-8"?>?
Thang Pham

19
Para omitir la <?xml ...>declaración, agreguetransformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
rustyx

44
Los lectores ocasionales pueden encontrar útil una versión mejorada de la solución descrita aquí ( stackoverflow.com/a/33541820/363573 ).
Stephan

55
donde se docdefine
Florian F

66
Esto no responde a mi pregunta: ¿cómo formateo una cadena que contiene XML? Esta respuesta ya supone que de alguna manera has convertido el objeto String en otro objeto.
Steve McLeod

135

Aquí hay una respuesta a mi propia pregunta. Combiné las respuestas de los distintos resultados para escribir una clase que imprima bastante XML.

No hay garantías sobre cómo responde con XML inválido o documentos grandes.

package ecb.sdw.pretty;

import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

/**
 * Pretty-prints xml, supplied as a string.
 * <p/>
 * eg.
 * <code>
 * String formattedXml = new XmlFormatter().format("<tag><nested>hello</nested></tag>");
 * </code>
 */
public class XmlFormatter {

    public XmlFormatter() {
    }

    public String format(String unformattedXml) {
        try {
            final Document document = parseXmlFile(unformattedXml);

            OutputFormat format = new OutputFormat(document);
            format.setLineWidth(65);
            format.setIndenting(true);
            format.setIndent(2);
            Writer out = new StringWriter();
            XMLSerializer serializer = new XMLSerializer(out, format);
            serializer.serialize(document);

            return out.toString();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Document parseXmlFile(String in) {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            DocumentBuilder db = dbf.newDocumentBuilder();
            InputSource is = new InputSource(new StringReader(in));
            return db.parse(is);
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        } catch (SAXException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        String unformattedXml =
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><QueryMessage\n" +
                        "        xmlns=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/message\"\n" +
                        "        xmlns:query=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/query\">\n" +
                        "    <Query>\n" +
                        "        <query:CategorySchemeWhere>\n" +
                        "   \t\t\t\t\t         <query:AgencyID>ECB\n\n\n\n</query:AgencyID>\n" +
                        "        </query:CategorySchemeWhere>\n" +
                        "    </Query>\n\n\n\n\n" +
                        "</QueryMessage>";

        System.out.println(new XmlFormatter().format(unformattedXml));
    }

}

13
Solo para notar que esta respuesta requiere el uso de Xerces. Si no desea agregar esta dependencia, simplemente puede usar las bibliotecas jdk estándar y javax.xml.transform.Transformer (vea mi respuesta a continuación)
khylo

45
En 2008, esta fue una buena respuesta, pero ahora todo esto se puede hacer con las clases estándar de JDK en lugar de las clases de Apache. Ver xerces.apache.org/xerces2-j/faq-general.html#faq-6 . Sí, esta es una pregunta frecuente de Xerces, pero la respuesta cubre las clases estándar de JDK. La implementación inicial de 1.5 de estas clases tuvo muchos problemas, pero todo funciona bien desde 1.6 en adelante. Copie el ejemplo de LSSerializer en las preguntas frecuentes, corte el bit "..." y agregue writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);después de la LSSerializer writer = ...línea.
George Hawkins,

2
Creé una pequeña clase usando el ejemplo que Apache dio, al que @GeorgeHawkins le dio un enlace. Faltaba cómo documentse inicializó la variable , por lo que pensé que podría agregar la desaceleración y hacer un ejemplo rápido de ella. Avíseme si debo cambiar algo, pastebin.com/XL7932aC
samwell

no es cierto que pueda hacer eso solo con jdk. al menos no de manera confiable. depende de alguna implementación de registro interno que no esté activa con mi jdk7u72 de manera predeterminada. así que es mejor que uses las cosas de Apache directamente.
user1050755

Aquí hay una solución sin dependencias: stackoverflow.com/a/33541820/363573 .
Stephan

131

Una solución más simple basada en esta respuesta :

public static String prettyFormat(String input, int indent) {
    try {
        Source xmlInput = new StreamSource(new StringReader(input));
        StringWriter stringWriter = new StringWriter();
        StreamResult xmlOutput = new StreamResult(stringWriter);
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        transformerFactory.setAttribute("indent-number", indent);
        Transformer transformer = transformerFactory.newTransformer(); 
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.transform(xmlInput, xmlOutput);
        return xmlOutput.getWriter().toString();
    } catch (Exception e) {
        throw new RuntimeException(e); // simple exception handling, please review it
    }
}

public static String prettyFormat(String input) {
    return prettyFormat(input, 2);
}

caso de prueba:

prettyFormat("<root><child>aaa</child><child/></root>");

devoluciones:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <child>aaa</child>
  <child/>
</root>

1
Este es el código que siempre he usado, pero en esta empresa no funcionó, supongo que están usando otra biblioteca de transformación XML. Creé la fábrica como una línea separada y luego lo hice factory.setAttribute("indent-number", 4);y ahora funciona.
Adrian Smith

¿Cómo hacer para que la salida no contenga <?xml version="1.0" encoding="UTF-8"?>?
Thang Pham

44
@Harry:transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
jjmontes

55
Hola, estoy usando este código exacto, y mina los formatos correctamente con la excepción del primer elemento Entonces, esto: <?xml version="1.0" encoding="UTF-8"?><root>está todo en una línea. Alguna idea de por qué?
CodyK

2
@ Codemiester: Parece ser un error (consulte stackoverflow.com/a/18251901/3375325 ). Agregar transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");funcionó para mí.
jansohn

100

Ahora es 2012 y Java puede hacer más de lo que solía hacer con XML, me gustaría agregar una alternativa a mi respuesta aceptada. Esto no tiene dependencias fuera de Java 6.

import org.w3c.dom.Node;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;

/**
 * Pretty-prints xml, supplied as a string.
 * <p/>
 * eg.
 * <code>
 * String formattedXml = new XmlFormatter().format("<tag><nested>hello</nested></tag>");
 * </code>
 */
public class XmlFormatter {

    public String format(String xml) {

        try {
            final InputSource src = new InputSource(new StringReader(xml));
            final Node document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement();
            final Boolean keepDeclaration = Boolean.valueOf(xml.startsWith("<?xml"));

        //May need this: System.setProperty(DOMImplementationRegistry.PROPERTY,"com.sun.org.apache.xerces.internal.dom.DOMImplementationSourceImpl");


            final DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
            final DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
            final LSSerializer writer = impl.createLSSerializer();

            writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE); // Set this to true if the output needs to be beautified.
            writer.getDomConfig().setParameter("xml-declaration", keepDeclaration); // Set this to true if the declaration is needed to be outputted.

            return writer.writeToString(document);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        String unformattedXml =
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?><QueryMessage\n" +
                        "        xmlns=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/message\"\n" +
                        "        xmlns:query=\"http://www.SDMX.org/resources/SDMXML/schemas/v2_0/query\">\n" +
                        "    <Query>\n" +
                        "        <query:CategorySchemeWhere>\n" +
                        "   \t\t\t\t\t         <query:AgencyID>ECB\n\n\n\n</query:AgencyID>\n" +
                        "        </query:CategorySchemeWhere>\n" +
                        "    </Query>\n\n\n\n\n" +
                        "</QueryMessage>";

        System.out.println(new XmlFormatter().format(unformattedXml));
    }
}

Sin sangría, pero funciona con esto: System.setProperty (DOMImplementationRegistry.PROPERTY, "com.sun.org.apache.xerces.internal.dom.DOMImplementationSourceImpl");
ggb667

1
¿Cómo agrega sangría a este ejemplo?
ggb667

2
@DanTemple Parece que necesita usar LSOutput para controlar la codificación. Ver chipkillmar.net/2009/03/25/pretty-print-xml-from-a-dom
Joshua Davis

1
Traté de usar esto en Andriod pero no puedo encontrar el paquete `DOMImplementationRegistry. Estoy usando Java 8.
Chintan Soni

2
gracias por incluir también la lista de importación, tantos paquetes conflictivos disponibles para dar sentido a la combinación necesaria de otra manera ...
Leon

54

Solo para notar que la respuesta mejor calificada requiere el uso de xerces.

Si no desea agregar esta dependencia externa, simplemente puede usar las bibliotecas jdk estándar (que en realidad se crean utilizando xerces internamente).

NB: Hubo un error con la versión 1.5 de jdk, consulte http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446 pero ya está resuelto.

(Tenga en cuenta que si se produce un error, esto devolverá el texto original)

package com.test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamResult;

import org.xml.sax.InputSource;

public class XmlTest {
    public static void main(String[] args) {
        XmlTest t = new XmlTest();
        System.out.println(t.formatXml("<a><b><c/><d>text D</d><e value='0'/></b></a>"));
    }

    public String formatXml(String xml){
        try{
            Transformer serializer= SAXTransformerFactory.newInstance().newTransformer();
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            //serializer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
            //serializer.setOutputProperty("{http://xml.customer.org/xslt}indent-amount", "2");
            Source xmlSource=new SAXSource(new InputSource(new ByteArrayInputStream(xml.getBytes())));
            StreamResult res =  new StreamResult(new ByteArrayOutputStream());            
            serializer.transform(xmlSource, res);
            return new String(((ByteArrayOutputStream)res.getOutputStream()).toByteArray());
        }catch(Exception e){
            //TODO log error
            return xml;
        }
    }

}

En este caso, las pestañas izquierdas no se utilizan. Todas las etiquetas comienzan en el primer símbolo de la línea, como el texto habitual.
Ruslan

¿no necesita especificar un juego de caracteres al convertir de ida y vuelta entre bytes y cadena?
Will Glass

2
No debería haber necesidad de convertir y bytear arrays / String. Como mínimo, tendría que especificar charset al hacerlo. Una mejor opción sería usar las clases StringReader y StringWriter envueltas en InputSource y StreamResult.
maximdim

no funciona. necesita perder el tiempo con alguna implementación de registro interno.
user1050755

Aquí hay una variante más simple de esta solución: stackoverflow.com/a/33541820/363573
Stephan

32

Ya imprimí bastante en el pasado usando el método org.dom4j.io.OutputFormat.createPrettyPrint ()

public String prettyPrint(final String xml){  

    if (StringUtils.isBlank(xml)) {
        throw new RuntimeException("xml was null or blank in prettyPrint()");
    }

    final StringWriter sw;

    try {
        final OutputFormat format = OutputFormat.createPrettyPrint();
        final org.dom4j.Document document = DocumentHelper.parseText(xml);
        sw = new StringWriter();
        final XMLWriter writer = new XMLWriter(sw, format);
        writer.write(document);
    }
    catch (Exception e) {
        throw new RuntimeException("Error pretty printing xml:\n" + xml, e);
    }
    return sw.toString();
}

3
La solución aceptada no sangra correctamente las etiquetas anidadas en mi caso, esta sí.
Chase Seibert

3
prettyPrintedString.replaceAll("\\s+\n", "\n")
Usé

19

Aquí hay una forma de hacerlo usando dom4j :

Importaciones:

import org.dom4j.Document;  
import org.dom4j.DocumentHelper;  
import org.dom4j.io.OutputFormat;  
import org.dom4j.io.XMLWriter;

Código:

String xml = "<your xml='here'/>";  
Document doc = DocumentHelper.parseText(xml);  
StringWriter sw = new StringWriter();  
OutputFormat format = OutputFormat.createPrettyPrint();  
XMLWriter xw = new XMLWriter(sw, format);  
xw.write(doc);  
String result = sw.toString();

1
Esto no funcionó para mí. Simplemente dio algo como: <?xml version...en una línea y todo lo demás en otra línea.
sixtyfootersdude

14

Dado que está comenzando con a String, debe convertirlo en un DOMobjeto (por ejemplo Node) antes de poder usar el Transformer. Sin embargo, si sabe que su cadena XML es válida y no desea incurrir en la sobrecarga de memoria al analizar una cadena en un DOM, luego ejecute una transformación sobre el DOM para recuperar una cadena; simplemente puede hacer algo antiguo análisis de carácter por carácter. Inserte una nueva línea y espacios después de cada </...>carácter, mantenga un contador de sangría (para determinar el número de espacios) que incremente para cada uno <...>y disminuya para cada </...>que vea.

Descargo de responsabilidad: hice una edición de cortar / pegar / texto de las funciones a continuación, por lo que es posible que no se compilen tal como están.

public static final Element createDOM(String strXML) 
    throws ParserConfigurationException, SAXException, IOException {

    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setValidating(true);
    DocumentBuilder db = dbf.newDocumentBuilder();
    InputSource sourceXML = new InputSource(new StringReader(strXML));
    Document xmlDoc = db.parse(sourceXML);
    Element e = xmlDoc.getDocumentElement();
    e.normalize();
    return e;
}

public static final void prettyPrint(Node xml, OutputStream out)
    throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException {
    Transformer tf = TransformerFactory.newInstance().newTransformer();
    tf.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    tf.setOutputProperty(OutputKeys.INDENT, "yes");
    tf.transform(new DOMSource(xml), new StreamResult(out));
}

1
"Sin embargo, si sabe que su cadena XML es válida ..." buen punto. Vea mi solución basada en este enfoque a continuación.
David Easley

12

Si usar una biblioteca XML de terceros está bien, puede salirse con la suya con algo significativamente más simple de lo que sugieren las respuestas más votadas actualmente .

Se indicó que tanto la entrada como la salida deberían ser cadenas, por lo que aquí hay un método de utilidad que hace exactamente eso, implementado con la biblioteca XOM :

import nu.xom.*;
import java.io.*;

[...]

public static String format(String xml) throws ParsingException, IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    Serializer serializer = new Serializer(out);
    serializer.setIndent(4);  // or whatever you like
    serializer.write(new Builder().build(xml, ""));
    return out.toString("UTF-8");
}

Probé que funciona, y los resultados no dependen de su versión de JRE ni nada de eso. Para ver cómo personalizar el formato de salida a su gusto, eche un vistazo a la SerializerAPI.

En realidad, esto salió más tiempo de lo que pensaba: se necesitaban algunas líneas adicionales porque Serializerquiere OutputStreamescribir. Pero tenga en cuenta que aquí hay muy poco código para el giro XML real.

(Esta respuesta es parte de mi evaluación de XOM, que se sugirió como una opción en mi pregunta sobre la mejor biblioteca XML de Java para reemplazar dom4j. Para el registro, con dom4j podría lograr esto con la misma facilidad usando XMLWritery OutputFormat. Editar : .. Como se demostró en la respuesta de mlo55 .)


2
Gracias, eso es lo que estaba buscando. Si ya tiene un XML analizado con XOM en un objeto "Documento", puede pasarlo directamente a serializer.write (documento);
Thibault D.

12

Kevin Hakanson dijo: "Sin embargo, si sabe que su cadena XML es válida y no desea incurrir en la sobrecarga de memoria al analizar una cadena en un DOM, luego ejecute una transformación sobre el DOM para recuperar una cadena: podría simplemente realice algunos caracteres anticuados mediante el análisis de caracteres. Inserte una nueva línea y espacios después de cada carácter, mantenga un contador de sangría (para determinar el número de espacios) que incremente por cada <...> y disminuya por cada que vea ".

Convenido. Tal enfoque es mucho más rápido y tiene muchas menos dependencias.

Solución de ejemplo:

/**
 * XML utils, including formatting.
 */
public class XmlUtils
{
  private static XmlFormatter formatter = new XmlFormatter(2, 80);

  public static String formatXml(String s)
  {
    return formatter.format(s, 0);
  }

  public static String formatXml(String s, int initialIndent)
  {
    return formatter.format(s, initialIndent);
  }

  private static class XmlFormatter
  {
    private int indentNumChars;
    private int lineLength;
    private boolean singleLine;

    public XmlFormatter(int indentNumChars, int lineLength)
    {
      this.indentNumChars = indentNumChars;
      this.lineLength = lineLength;
    }

    public synchronized String format(String s, int initialIndent)
    {
      int indent = initialIndent;
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < s.length(); i++)
      {
        char currentChar = s.charAt(i);
        if (currentChar == '<')
        {
          char nextChar = s.charAt(i + 1);
          if (nextChar == '/')
            indent -= indentNumChars;
          if (!singleLine)   // Don't indent before closing element if we're creating opening and closing elements on a single line.
            sb.append(buildWhitespace(indent));
          if (nextChar != '?' && nextChar != '!' && nextChar != '/')
            indent += indentNumChars;
          singleLine = false;  // Reset flag.
        }
        sb.append(currentChar);
        if (currentChar == '>')
        {
          if (s.charAt(i - 1) == '/')
          {
            indent -= indentNumChars;
            sb.append("\n");
          }
          else
          {
            int nextStartElementPos = s.indexOf('<', i);
            if (nextStartElementPos > i + 1)
            {
              String textBetweenElements = s.substring(i + 1, nextStartElementPos);

              // If the space between elements is solely newlines, let them through to preserve additional newlines in source document.
              if (textBetweenElements.replaceAll("\n", "").length() == 0)
              {
                sb.append(textBetweenElements + "\n");
              }
              // Put tags and text on a single line if the text is short.
              else if (textBetweenElements.length() <= lineLength * 0.5)
              {
                sb.append(textBetweenElements);
                singleLine = true;
              }
              // For larger amounts of text, wrap lines to a maximum line length.
              else
              {
                sb.append("\n" + lineWrap(textBetweenElements, lineLength, indent, null) + "\n");
              }
              i = nextStartElementPos - 1;
            }
            else
            {
              sb.append("\n");
            }
          }
        }
      }
      return sb.toString();
    }
  }

  private static String buildWhitespace(int numChars)
  {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < numChars; i++)
      sb.append(" ");
    return sb.toString();
  }

  /**
   * Wraps the supplied text to the specified line length.
   * @lineLength the maximum length of each line in the returned string (not including indent if specified).
   * @indent optional number of whitespace characters to prepend to each line before the text.
   * @linePrefix optional string to append to the indent (before the text).
   * @returns the supplied text wrapped so that no line exceeds the specified line length + indent, optionally with
   * indent and prefix applied to each line.
   */
  private static String lineWrap(String s, int lineLength, Integer indent, String linePrefix)
  {
    if (s == null)
      return null;

    StringBuilder sb = new StringBuilder();
    int lineStartPos = 0;
    int lineEndPos;
    boolean firstLine = true;
    while(lineStartPos < s.length())
    {
      if (!firstLine)
        sb.append("\n");
      else
        firstLine = false;

      if (lineStartPos + lineLength > s.length())
        lineEndPos = s.length() - 1;
      else
      {
        lineEndPos = lineStartPos + lineLength - 1;
        while (lineEndPos > lineStartPos && (s.charAt(lineEndPos) != ' ' && s.charAt(lineEndPos) != '\t'))
          lineEndPos--;
      }
      sb.append(buildWhitespace(indent));
      if (linePrefix != null)
        sb.append(linePrefix);

      sb.append(s.substring(lineStartPos, lineEndPos + 1));
      lineStartPos = lineEndPos + 1;
    }
    return sb.toString();
  }

  // other utils removed for brevity
}

2
Esta es la forma en que debe hacerse. Formato sobre la marcha en el nivel de cadena. Esta es la única solución que formateará XML no válido o incompleto.
Florian F

11

Hmmm ... se enfrentó a algo como esto y es un error conocido ... solo agregue esta OutputProperty ...

transformer.setOutputProperty(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "8");

Espero que esto ayude ...


2
¿De dónde viene esta OutputPropertiesFactory?
helenov

import com.sun.org.apache.xml.internal.serializer. *;
Gaurav

9

Respecto al comentario de que "primero debe construir un árbol DOM": No, no necesita y no debe hacer eso.

En su lugar, cree un StreamSource (nuevo StreamSource (nuevo StringReader (str)) y alimente eso al transformador de identidad mencionado. Utilizará el analizador SAX y el resultado será mucho más rápido. La construcción de un árbol intermedio es pura sobrecarga para este caso. De lo contrario, la respuesta mejor clasificada es buena.


1
Estoy totalmente de acuerdo: construir el árbol DOM intermedio es una pérdida de memoria. Thansk por esa respuesta.
Florian F

9

Usando scala:

import xml._
val xml = XML.loadString("<tag><nested>hello</nested></tag>")
val formatted = new PrettyPrinter(150, 2).format(xml)
println(formatted)

También puede hacer esto en Java, si depende de scala-library.jar. Se parece a esto:

import scala.xml.*;

public class FormatXML {
    public static void main(String[] args) {
        String unformattedXml = "<tag><nested>hello</nested></tag>";
        PrettyPrinter pp = new PrettyPrinter(150, 3);
        String formatted = pp.format(XML.loadString(unformattedXml), TopScope$.MODULE$);
        System.out.println(formatted);
    }
}

El PrettyPrinterobjeto se construye con dos entradas, la primera es la longitud máxima de la línea y la segunda el paso de sangría.


9

versión ligeramente mejorada de milosmns ...

public static String getPrettyXml(String xml) {
    if (xml == null || xml.trim().length() == 0) return "";

    int stack = 0;
    StringBuilder pretty = new StringBuilder();
    String[] rows = xml.trim().replaceAll(">", ">\n").replaceAll("<", "\n<").split("\n");

    for (int i = 0; i < rows.length; i++) {
        if (rows[i] == null || rows[i].trim().length() == 0) continue;

        String row = rows[i].trim();
        if (row.startsWith("<?")) {
            pretty.append(row + "\n");
        } else if (row.startsWith("</")) {
            String indent = repeatString(--stack);
            pretty.append(indent + row + "\n");
        } else if (row.startsWith("<") && row.endsWith("/>") == false) {
            String indent = repeatString(stack++);
            pretty.append(indent + row + "\n");
            if (row.endsWith("]]>")) stack--;
        } else {
            String indent = repeatString(stack);
            pretty.append(indent + row + "\n");
        }
    }

    return pretty.toString().trim();
}

private static String repeatString(int stack) {
     StringBuilder indent = new StringBuilder();
     for (int i = 0; i < stack; i++) {
        indent.append(" ");
     }
     return indent.toString();
} 

donde es repeatString (stack ++); método..?
user1912935

2
cadena estática privada repeatString (int stack) {StringBuilder indent = new StringBuilder (); for (int i = 0; i <stack; i ++) {indent.append (""); } return indent.toString (); }
codeskraps

La sangría no funciona bien en las etiquetas finales. Debe cambiar } else if (row.startsWith("</")) {parte de esto:else if (row.startsWith("</")) { String indent = repeatIdent(--stack); if (pretty.charAt(pretty.length() - 1) == '\n') { pretty.append(indent + row + "\n"); } else { pretty.append(row + "\n"); } }
Csaba Tenkes

8

Solo para referencia futura, aquí hay una solución que funcionó para mí (gracias a un comentario que @George Hawkins publicó en una de las respuestas):

DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
LSSerializer writer = impl.createLSSerializer();
writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
LSOutput output = impl.createLSOutput();
ByteArrayOutputStream out = new ByteArrayOutputStream();
output.setByteStream(out);
writer.write(document, output);
String xmlStr = new String(out.toByteArray());

6

Si está seguro de que tiene un XML válido, este es simple y evita árboles XML DOM. Tal vez tiene algunos errores, comente si ve algo

public String prettyPrint(String xml) {
            if (xml == null || xml.trim().length() == 0) return "";

            int stack = 0;
            StringBuilder pretty = new StringBuilder();
            String[] rows = xml.trim().replaceAll(">", ">\n").replaceAll("<", "\n<").split("\n");

            for (int i = 0; i < rows.length; i++) {
                    if (rows[i] == null || rows[i].trim().length() == 0) continue;

                    String row = rows[i].trim();
                    if (row.startsWith("<?")) {
                            // xml version tag
                            pretty.append(row + "\n");
                    } else if (row.startsWith("</")) {
                            // closing tag
                            String indent = repeatString("    ", --stack);
                            pretty.append(indent + row + "\n");
                    } else if (row.startsWith("<")) {
                            // starting tag
                            String indent = repeatString("    ", stack++);
                            pretty.append(indent + row + "\n");
                    } else {
                            // tag data
                            String indent = repeatString("    ", stack);
                            pretty.append(indent + row + "\n");
                    }
            }

            return pretty.toString().trim();
    }

2
¿Dónde está el método repeatString ..?
user1912935

3
cadena estática privada repeatString (int stack) {StringBuilder indent = new StringBuilder (); for (int i = 0; i <stack; i ++) {indent.append (""); } return indent.toString (); }
codeskraps

Sí [user1912935], lo que escribió @codeskraps, debería ser lo suficientemente simple :)
milosmns

Concatenación con un StringBuilder dentro de un bucle: mala práctica.
james.garriss

@ james.garriss Pero es muy fácil dividirse en nuevas líneas, esto solo ilustra un enfoque simple sin ningún árbol DOM.
milosmns

5

Todas las soluciones anteriores no funcionaron para mí, entonces encontré este http://myshittycode.com/2014/02/10/java-properly-indenting-xml-string/

La pista es eliminar espacios en blanco con XPath

    String xml = "<root>" +
             "\n   " +
             "\n<name>Coco Puff</name>" +
             "\n        <total>10</total>    </root>";

try {
    Document document = DocumentBuilderFactory.newInstance()
            .newDocumentBuilder()
            .parse(new InputSource(new ByteArrayInputStream(xml.getBytes("utf-8"))));

    XPath xPath = XPathFactory.newInstance().newXPath();
    NodeList nodeList = (NodeList) xPath.evaluate("//text()[normalize-space()='']",
                                                  document,
                                                  XPathConstants.NODESET);

    for (int i = 0; i < nodeList.getLength(); ++i) {
        Node node = nodeList.item(i);
        node.getParentNode().removeChild(node);
    }

    Transformer transformer = TransformerFactory.newInstance().newTransformer();
    transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

    StringWriter stringWriter = new StringWriter();
    StreamResult streamResult = new StreamResult(stringWriter);

    transformer.transform(new DOMSource(document), streamResult);

    System.out.println(stringWriter.toString());
}
catch (Exception e) {
    e.printStackTrace();
}

1
Tenga en cuenta que el uso de la propiedad '{ xml.apache.org/xslt } indent-amount' lo vinculará a una implementación específica del transformador.
vallismortis

1
De todas las soluciones, esta funcionó mejor. Ya tenía espacios y nuevas líneas en mi XML plus, no quería agregar más dependencias a mi proyecto. Desearía no tener que analizar el XML, pero bueno.
Fabio

5

Este código a continuación funciona perfectamente

import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

String formattedXml1 = prettyFormat("<root><child>aaa</child><child/></root>");

public static String prettyFormat(String input) {
    return prettyFormat(input, "2");
}

public static String prettyFormat(String input, String indent) {
    Source xmlInput = new StreamSource(new StringReader(input));
    StringWriter stringWriter = new StringWriter();
    try {
        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", indent);
        transformer.transform(xmlInput, new StreamResult(stringWriter));

        String pretty = stringWriter.toString();
        pretty = pretty.replace("\r\n", "\n");
        return pretty;              
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

5

Los mezclo todos y escribo un pequeño programa. Está leyendo del archivo xml e imprimiendo. Solo en lugar de xzy, proporcione su ruta de archivo.

    public static void main(String[] args) throws Exception {
    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    dbf.setValidating(false);
    DocumentBuilder db = dbf.newDocumentBuilder();
    Document doc = db.parse(new FileInputStream(new File("C:/Users/xyz.xml")));
    prettyPrint(doc);

}

private static String prettyPrint(Document document)
        throws TransformerException {
    TransformerFactory transformerFactory = TransformerFactory
            .newInstance();
    Transformer transformer = transformerFactory.newTransformer();
    transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
    transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
    transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
    DOMSource source = new DOMSource(document);
    StringWriter strWriter = new StringWriter();
    StreamResult result = new StreamResult(strWriter);transformer.transform(source, result);
    System.out.println(strWriter.getBuffer().toString());

    return strWriter.getBuffer().toString();

}

4

Solo otra solución que funciona para nosotros

import java.io.StringWriter;
import org.dom4j.DocumentHelper;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

**
 * Pretty Print XML String
 * 
 * @param inputXmlString
 * @return
 */
public static String prettyPrintXml(String xml) {

    final StringWriter sw;

    try {
        final OutputFormat format = OutputFormat.createPrettyPrint();
        final org.dom4j.Document document = DocumentHelper.parseText(xml);
        sw = new StringWriter();
        final XMLWriter writer = new XMLWriter(sw, format);
        writer.write(document);
    }
    catch (Exception e) {
        throw new RuntimeException("Error pretty printing xml:\n" + xml, e);
    }
    return sw.toString();
}

3

Usando jdom2: http://www.jdom.org/

import java.io.StringReader;
import org.jdom2.input.SAXBuilder;
import org.jdom2.output.Format;
import org.jdom2.output.XMLOutputter;

String prettyXml = new XMLOutputter(Format.getPrettyFormat()).
                         outputString(new SAXBuilder().build(new StringReader(uglyXml)));

3

Como alternativa a las respuestas de max , codeskraps , David Easley y milosmns , eche un vistazo a mi biblioteca de impresoras bonitas livianas y de alto rendimiento: xml-formatter

// construct lightweight, threadsafe, instance
PrettyPrinter prettyPrinter = PrettyPrinterBuilder.newPrettyPrinter().build();

StringBuilder buffer = new StringBuilder();
String xml = ..; // also works with char[] or Reader

if(prettyPrinter.process(xml, buffer)) {
     // valid XML, print buffer
} else {
     // invalid XML, print xml
}

A veces, como cuando se ejecutan servicios SOAP simulados directamente desde un archivo, es bueno tener una impresora bonita que también maneje XML ya impreso:

PrettyPrinter prettyPrinter = PrettyPrinterBuilder.newPrettyPrinter().ignoreWhitespace().build();

Como algunos han comentado, la impresión bonita es solo una forma de presentar XML en una forma más legible para los humanos: los espacios en blanco estrictamente no pertenecen a sus datos XML.

La biblioteca está diseñada para la impresión bonita con fines de registro, y también incluye funciones para el filtrado (eliminación de subárbol / anonimización) y la impresión bonita de XML en CDATA y nodos de texto.


2

Tuve el mismo problema y estoy teniendo un gran éxito con JTidy ( http://jtidy.sourceforge.net/index.html )

Ejemplo:

Tidy t = new Tidy();
t.setIndentContent(true);
Document d = t.parseDOM(
    new ByteArrayInputStream("HTML goes here", null);

OutputStream out = new ByteArrayOutputStream();
t.pprint(d, out);
String html = out.toString();

2

Underscore-java tiene un método estático U.formatXml(string). Soy el mantenedor del proyecto. Ejemplo en vivo

import com.github.underscore.lodash.U;

public class MyClass {
    public static void main(String args[]) {
        String xml = "<tag><nested>hello</nested></tag>";

        System.out.println(U.formatXml("<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>" + xml + "</root>"));
    }
}

Salida:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <tag>
      <nested>hello</nested>
   </tag>
</root>

¡Esto es asombroso!
senyor

1

hay una muy buena utilidad de línea de comandos xml llamada xmlstarlet ( http://xmlstar.sourceforge.net/ ) que puede hacer muchas cosas que mucha gente usa.

Podrías ejecutar este programa programáticamente usando Runtime.exec y luego leer el archivo de salida formateado. Tiene más opciones y mejores informes de errores que algunas líneas de código Java pueden proporcionar.

descarga xmlstarlet: http://sourceforge.net/project/showfiles.php?group_id=66612&package_id=64589


1

Descubrí que en Java 1.6.0_32 el método normal para imprimir una cadena XML (usando un Transformador con un nulo o identidad xslt) no se comporta como me gustaría si las etiquetas se separan simplemente por espacios en blanco, en lugar de no tener separación texto. Intenté usar <xsl:strip-space elements="*"/>mi plantilla en vano. La solución más simple que encontré fue quitar el espacio de la manera que quería usando un filtro SAXSource y XML. Como mi solución fue para el registro, también extendí esto para trabajar con fragmentos XML incompletos. Tenga en cuenta que el método normal parece funcionar bien si usa un DOMSource pero no quería usarlo debido a la incompletitud y la sobrecarga de memoria.

public static class WhitespaceIgnoreFilter extends XMLFilterImpl
{

    @Override
    public void ignorableWhitespace(char[] arg0,
                                    int arg1,
                                    int arg2) throws SAXException
    {
        //Ignore it then...
    }

    @Override
    public void characters( char[] ch,
                            int start,
                            int length) throws SAXException
    {
        if (!new String(ch, start, length).trim().equals("")) 
               super.characters(ch, start, length); 
    }
}

public static String prettyXML(String logMsg, boolean allowBadlyFormedFragments) throws SAXException, IOException, TransformerException
    {
        TransformerFactory transFactory = TransformerFactory.newInstance();
        transFactory.setAttribute("indent-number", new Integer(2));
        Transformer transformer = transFactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
        StringWriter out = new StringWriter();
        XMLReader masterParser = SAXHelper.getSAXParser(true);
        XMLFilter parser = new WhitespaceIgnoreFilter();
        parser.setParent(masterParser);

        if(allowBadlyFormedFragments)
        {
            transformer.setErrorListener(new ErrorListener()
            {
                @Override
                public void warning(TransformerException exception) throws TransformerException
                {
                }

                @Override
                public void fatalError(TransformerException exception) throws TransformerException
                {
                }

                @Override
                public void error(TransformerException exception) throws TransformerException
                {
                }
            });
        }

        try
        {
            transformer.transform(new SAXSource(parser, new InputSource(new StringReader(logMsg))), new StreamResult(out));
        }
        catch (TransformerException e)
        {
            if(e.getCause() != null && e.getCause() instanceof SAXParseException)
            {
                if(!allowBadlyFormedFragments || !"XML document structures must start and end within the same entity.".equals(e.getCause().getMessage()))
                {
                    throw e;
                }
            }
            else
            {
                throw e;
            }
        }
        out.flush();
        return out.toString();
    }

1

Las soluciones que he encontrado aquí para Java 1.6+ no reformatean el código si ya está formateado. El que funcionó para mí (y volvió a formatear el código ya formateado) fue el siguiente.

import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.c14n.InvalidCanonicalizerException;
import org.w3c.dom.Element;
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.io.StringReader;

public class XmlUtils {
    public static String toCanonicalXml(String xml) throws InvalidCanonicalizerException, ParserConfigurationException, SAXException, CanonicalizationException, IOException {
        Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
        byte canonXmlBytes[] = canon.canonicalize(xml.getBytes());
        return new String(canonXmlBytes);
    }

    public static String prettyFormat(String input) throws TransformerException, ParserConfigurationException, IOException, SAXException, InstantiationException, IllegalAccessException, ClassNotFoundException {
        InputSource src = new InputSource(new StringReader(input));
        Element document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(src).getDocumentElement();
        Boolean keepDeclaration = input.startsWith("<?xml");
        DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
        DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS");
        LSSerializer writer = impl.createLSSerializer();
        writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);
        writer.getDomConfig().setParameter("xml-declaration", keepDeclaration);
        return writer.writeToString(document);
    }
}

Es una buena herramienta para usar en las pruebas unitarias para la comparación xml de cadena completa.

private void assertXMLEqual(String expected, String actual) throws ParserConfigurationException, IOException, SAXException, CanonicalizationException, InvalidCanonicalizerException, TransformerException, IllegalAccessException, ClassNotFoundException, InstantiationException {
    String canonicalExpected = prettyFormat(toCanonicalXml(expected));
    String canonicalActual = prettyFormat(toCanonicalXml(actual));
    assertEquals(canonicalExpected, canonicalActual);
}

1

Para aquellos que buscan una solución rápida y sucia, que no necesita que el XML sea 100% válido. por ejemplo, en caso de registro REST / SOAP (nunca se sabe lo que envían los demás ;-))

Encontré y avancé un código cortado que encontré en línea que creo que todavía falta aquí como un posible enfoque válido:

public static String prettyPrintXMLAsString(String xmlString) {
    /* Remove new lines */
    final String LINE_BREAK = "\n";
    xmlString = xmlString.replaceAll(LINE_BREAK, "");
    StringBuffer prettyPrintXml = new StringBuffer();
    /* Group the xml tags */
    Pattern pattern = Pattern.compile("(<[^/][^>]+>)?([^<]*)(</[^>]+>)?(<[^/][^>]+/>)?");
    Matcher matcher = pattern.matcher(xmlString);
    int tabCount = 0;
    while (matcher.find()) {
        String str1 = (null == matcher.group(1) || "null".equals(matcher.group())) ? "" : matcher.group(1);
        String str2 = (null == matcher.group(2) || "null".equals(matcher.group())) ? "" : matcher.group(2);
        String str3 = (null == matcher.group(3) || "null".equals(matcher.group())) ? "" : matcher.group(3);
        String str4 = (null == matcher.group(4) || "null".equals(matcher.group())) ? "" : matcher.group(4);

        if (matcher.group() != null && !matcher.group().trim().equals("")) {
            printTabs(tabCount, prettyPrintXml);
            if (!str1.equals("") && str3.equals("")) {
                ++tabCount;
            }
            if (str1.equals("") && !str3.equals("")) {
                --tabCount;
                prettyPrintXml.deleteCharAt(prettyPrintXml.length() - 1);
            }

            prettyPrintXml.append(str1);
            prettyPrintXml.append(str2);
            prettyPrintXml.append(str3);
            if (!str4.equals("")) {
                prettyPrintXml.append(LINE_BREAK);
                printTabs(tabCount, prettyPrintXml);
                prettyPrintXml.append(str4);
            }
            prettyPrintXml.append(LINE_BREAK);
        }
    }
    return prettyPrintXml.toString();
}

private static void printTabs(int count, StringBuffer stringBuffer) {
    for (int i = 0; i < count; i++) {
        stringBuffer.append("\t");
    }
}

public static void main(String[] args) {
    String x = new String(
            "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><soap:Fault><faultcode>soap:Client</faultcode><faultstring>INVALID_MESSAGE</faultstring><detail><ns3:XcbSoapFault xmlns=\"\" xmlns:ns3=\"http://www.someapp.eu/xcb/types/xcb/v1\"><CauseCode>20007</CauseCode><CauseText>INVALID_MESSAGE</CauseText><DebugInfo>Problems creating SAAJ object model</DebugInfo></ns3:XcbSoapFault></detail></soap:Fault></soap:Body></soap:Envelope>");
    System.out.println(prettyPrintXMLAsString(x));
}

Aquí está la salida:

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <soap:Fault>
        <faultcode>soap:Client</faultcode>
        <faultstring>INVALID_MESSAGE</faultstring>
        <detail>
            <ns3:XcbSoapFault xmlns="" xmlns:ns3="http://www.someapp.eu/xcb/types/xcb/v1">
                <CauseCode>20007</CauseCode>
                <CauseText>INVALID_MESSAGE</CauseText>
                <DebugInfo>Problems creating SAAJ object model</DebugInfo>
            </ns3:XcbSoapFault>
        </detail>
    </soap:Fault>
  </soap:Body>
</soap:Envelope>

1

Vi una respuesta usando Scala, así que aquí hay otra Groovy, por si alguien lo encuentra interesante. La sangría predeterminada es de 2 pasos, al XmlNodePrinterconstructor también se le puede pasar otro valor.

def xml = "<tag><nested>hello</nested></tag>"
def stringWriter = new StringWriter()
def node = new XmlParser().parseText(xml);
new XmlNodePrinter(new PrintWriter(stringWriter)).print(node)
println stringWriter.toString()

Uso de Java si groovy jar está en classpath

  String xml = "<tag><nested>hello</nested></tag>";
  StringWriter stringWriter = new StringWriter();
  Node node = new XmlParser().parseText(xml);
  new XmlNodePrinter(new PrintWriter(stringWriter)).print(node);
  System.out.println(stringWriter.toString());

1

En caso de que no necesite tanto sangrado sino algunos saltos de línea, podría ser suficiente simplemente regex ...

String leastPrettifiedXml = uglyXml.replaceAll("><", ">\n<");

El código es bueno, no el resultado debido a la falta de sangría.


(Para soluciones con sangría, vea otras respuestas).


1
Hmmmm ... Solo pensando en voz alta, ¿quién necesitaría tal solución? El único área que puedo ver son los datos que obtenemos de algunos servicios web y solo para probar esos datos y su validez, el desarrollador o el probador podrían necesitar unos tan fáciles. De lo contrario, no es una buena opción ...
Sudhakar Chavali

1
@SudhakarChavali Soy un desarrollador. podría necesitar eso para los hacks println () y log.debug () sucios; es decir, algunas veces solo puedo usar archivos de registro desde un entorno de servidor restringido (con interfaz de administración web en lugar de acceso de shell) en lugar de depurar el programa paso a paso.
comonad
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.