Як красиво роздрукувати XML з Java?


443

У мене є рядок Java, що містить XML, без каналів рядків чи відступів. Я хотів би перетворити його на String з добре відформатованим XML. Як це зробити?

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

Примітка: Мій вхід - це String . Мій вихід - це String .

(Основний) макетний результат:

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

перевірити це питання: stackoverflow.com/questions/1264849/…
dfa

10
Цікаво, ви надсилаєте цей вихід у XML-файл чи щось інше, де відступ дійсно має значення? Деякий час тому я був дуже стурбований форматуванням свого XML для того, щоб він був належним чином відображений ... але, витративши на це купу часу, я зрозумів, що мені потрібно надіслати свій вихід у веб-браузер та будь-який відносно сучасний веб-браузер насправді відобразить XML в хорошій структурі дерева, тож я міг забути про цю проблему і рухатися далі. Я згадую про це лише в тому випадку, якщо ви (або інший користувач із тією ж проблемою) могли не помітити ту саму деталь.
Абель Морелос

3
@Abel, збереження у текстових файлах, вставлення у текстові області HTML та перекидання на консоль для налагодження.
Стів Маклеод

2
"затримка як занадто широка" - важко бути більш точним, ніж питання, яке зараз є!
Стів Маклеод

Відповіді:


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);

Примітка: Результати можуть відрізнятися залежно від версії Java. Шукайте обхідні шляхи, характерні для вашої платформи.


1
Як зробити так, щоб вихідний вміст містив <?xml version="1.0" encoding="UTF-8"?>?
Thang Pham

19
Щоб пропустити <?xml ...>декларацію, додайтеtransformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
rustyx

4
Випадковим читачам може бути корисна вдосконалена версія описаного тут рішення ( stackoverflow.com/a/33541820/363573 ).
Стефан

5
де docвизначено?
Флоріан F

6
Це не відповідає на моє запитання: як я відформатую рядок, що містить XML? Ця відповідь вже передбачає, що ви якось перетворили об'єкт String в інший об'єкт.
Стів МакЛьюд,

135

Ось відповідь на моє власне запитання. Я поєднав відповіді з різних результатів, щоб написати клас, який гарненько друкує XML.

Ніяких гарантій того, як він реагує на недійсні XML або великі документи.

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
Тільки зазначимо, що ця відповідь вимагає використання Xerces. Якщо ви не хочете додавати цю залежність, тоді ви можете просто використовувати стандартні бібліотеки jdk та javax.xml.transform.Transformer (див. Мою відповідь нижче)
khylo

45
Ще в 2008 році це було гарною відповіддю, але тепер це все можна зробити за допомогою стандартних класів JDK, а не для класів Apache. Див. Xerces.apache.org/xerces2-j/faq-general.html#faq-6 . Так, це питання Xerces FAQ, але відповідь стосується стандартних класів JDK. Початкова реалізація цих класів за 1,5 мала багато питань, але все працює добре від 1.6. Скопіюйте приклад LSSerializer у FAQ, подрібніть біт "..." і додайте writer.getDomConfig().setParameter("format-pretty-print", Boolean.TRUE);після LSSerializer writer = ...рядка.
Джордж Хокінс

2
Я створив невеликий клас на прикладі, який дав Apache, на який @GeorgeHawkins дав посилання. Не було пропущено documentініціалізацію змінної , тому я подумав, що я можу додати уповільнення та зробити короткий приклад із неї. Повідомте мене, чи варто щось змінити, pastebin.com/XL7932aC
samwell

це неправда, що ви можете це робити лише з jdk. принаймні не надійно. це залежить від деякої внутрішньої реалізації реєстру, яка не активна для мого jdk7u72 за замовчуванням. тож вам все-таки краще використовувати речі апаша безпосередньо.
користувач1050755

Ось рішення без будь-яких залежностей: stackoverflow.com/a/33541820/363573 .
Стефан

131

більш просте рішення, засноване на цій відповіді :

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);
}

перевірка:

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

повертає:

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

1
Це код, який я завжди використовував, але в цій компанії він не працював, я вважаю, що вони використовують іншу бібліотеку, що перетворює XML. Я створив завод як окремий рядок, а потім зробив factory.setAttribute("indent-number", 4);і зараз він працює.
Адріан Сміт

Як зробити так, щоб вихідний вміст містив <?xml version="1.0" encoding="UTF-8"?>?
Thang Pham

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

5
Привіт Я використовую цей точний код, і мої формати належним чином, за винятком першого елемента. Отже, це: <?xml version="1.0" encoding="UTF-8"?><root>все в одному рядку. Будь-які ідеї чому?
CodyK

2
@Codemiester: Здається, помилка (див. Stackoverflow.com/a/18251901/3375325 ). Додавання transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "yes");працювало для мене.
Янсон

100

Тепер це 2012 рік, і Java може зробити більше, ніж раніше, ніж XML, я хотів би додати альтернативу моїй прийнятій відповіді. Це не має залежності від 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));
    }
}

Відступів немає, але він працює з цим: System.setProperty (DOMImplementationRegistry.PROPERTY, "com.sun.org.apache.xerces.internal.dom.DOMImplementationSourceImpl");
ggb667

1
Як ви додаєте відступ до цього прикладу?
ggb667

2
@DanTemple Схоже, вам потрібно використовувати LSOutput для управління кодуванням. Дивіться chipkillmar.net/2009/03/25/pretty-print-xml-from-a-dom
Джошуа Девіс

1
Я намагався використовувати це в Andriod, але мені не вдається знайти пакет `DOMImplementationRegistry. Я використовую java 8.
Чінтан Соні

2
дякую за включення списку імпорту, також доступно багато конфліктних пакетів, щоб мати сенс комбінації, необхідної інакше ..
Леон,

54

Зауважимо лише, що відповідь з найвищою оцінкою вимагає використання xerces.

Якщо ви не хочете додавати цю зовнішню залежність, ви можете просто використовувати стандартні бібліотеки jdk (які фактично побудовані за допомогою xerces всередині).

Примітка: Була помилка з версією jdk 1.5, див. Http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6296446, але вона вирішена зараз.,

(Зверніть увагу, якщо сталася помилка, це поверне початковий текст)

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;
        }
    }

}

У цьому випадку ліві вкладки не використовуються. Усі теги починаються спочатку символом рядка, як звичайний текст.
Руслан

вам не потрібно вказувати діаграму при перетворенні вперед і назад між байтами і рядками?
Буде Скло

2
Не повинно бути необхідності перетворювати масиви та рядки в байт та в них. Принаймні, вам потрібно буде вказати схему, виконуючи це. Кращим варіантом буде використання класів StringReader і StringWriter, обгорнуті InputSource та StreamResult.
maximdim

не працює. вам потрібно заплутатися з деякою реалізацією внутрішнього реєстру.
користувач1050755

Ось простіший варіант цього рішення: stackoverflow.com/a/33541820/363573
Stephan

32

Я досить друкував у минулому, використовуючи метод 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
Прийняте рішення не вводить належним чином відступ вкладених тегів у моєму випадку.
Чейз Сейберт

3
Я використав це разом із видаленням усіх пробілів у кінці рядків:prettyPrintedString.replaceAll("\\s+\n", "\n")
jediz

19

Ось спосіб зробити це за допомогою dom4j :

Імпорт:

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

Код:

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
Це не працювало для мене. Це просто дало щось на кшталт: <?xml version...на одній лінії, а на всьому - на іншій.
шістдесят футів

14

Оскільки ви починаєте з символу a String, вам потрібно скопіювати DOMоб'єкт (наприклад Node), перш ніж ви можете використовувати Transformer. Однак якщо ви знаєте, що ваш XML-рядок є дійсним, і ви не хочете перешкоджати накладній пам'яті для розбору рядка в DOM, то запустіть перетворення над DOM, щоб повернути рядок назад - ви можете просто зробити якийсь старомодний персонаж за розбором символів. Вставте новий рядок та пробіли після кожного </...>символу, збережіть і лічильник відступів (щоб визначити кількість пробілів), які ви збільшуєте для кожного, <...>і декремент для кожного</...> що бачите.

Відмова від відповідальності - я зробив розріз / вставлення / редагування тексту функцій, наведених нижче, тому вони можуть не складатись так, як є.

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
"Однак, якщо ви знаєте, що ваш рядок XML є дійсним ..." хороший момент. Дивіться моє рішення на основі такого підходу нижче.
Девід Ізлі

12

При використанні 3 - ї сторонньої бібліотеки XML в порядку, ви можете піти з чим - то значно простіше , ніж те , що в даний час найвищим проголосували відповіді запропонувати.

Було заявлено, що і вхід, і вихід повинні бути рядками, тому ось корисний метод, який робить саме це, реалізований з бібліотекою 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");
}

Я перевірив, що це працює, і результати не залежать від вашої версії JRE або чогось подібного. Щоб дізнатися, як налаштувати вихідний формат на свій смак, подивіться SerializerAPI.

Це насправді виходило довше, ніж я думав - потрібні були додаткові рядки, оскільки Serializerхочеться OutputStreamнаписати. Але зауважте, що тут дуже мало коду для фактичного твітання XML.

(Ця відповідь є частиною моєї оцінки XOM, яка була запропонована як один із варіантів мого запитання про кращу бібліотеку Java XML для заміни dom4j. Для запису, з dom4j ви могли досягти цього з подібною легкістю за допомогою XMLWriterта OutputFormat. Редагувати : .. . продемонстровано у відповіді mlo55 .)


2
Дякую, саме це я шукав. Якщо у вас є XML вже розібраний з XOM в об’єкті «Документ», ви можете передати його безпосередньо serializer.write (document);
Тібо Д.

12

Кевін Хакансон сказав: "Однак, якщо ви знаєте, що ваш XML-рядок є дійсним, і ви не хочете створювати накладні витрати на розбір рядка в DOM, то запустіть перетворення на DOM, щоб повернути рядок назад - ви могли б просто зробіть якийсь старомодний персонаж за допомогою синтаксичного розбору символів. Вставте новий рядок та пробіли після кожного символу, утримуйте лічильник і відступ (щоб визначити кількість пробілів), який ви збільшуєте для кожного <...> та зменшення для кожного, що бачите ".

Домовились. Такий підхід набагато швидший і має набагато менше залежностей.

Приклад рішення:

/**
 * 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
Це так, як це слід робити. Форматування на льоту на рівні струни. Це єдине рішення, яке буде форматувати недійсний або неповний XML.
Флоріан F

11

Гммм ... зіткнувся з чимось подібним, і це відома помилка ... просто додайте цю OutputProperty ..

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

Сподіваюся, це допомагає ...


2
Звідки береться ця OutputPropertiesFactory?
helenov

імпортувати com.sun.org.apache.xml.internal.serializer. *;
gaurav

9

Щодо коментаря, що "спочатку ви повинні створити дерево DOM": Ні, вам цього не потрібно і не слід робити.

Натомість створіть StreamSource (новий StreamSource (новий StringReader (str)) та подайте його до згаданого трансформатора ідентифікації. Це використовуватиме SAX-аналізатор, а результат буде набагато швидшим. Інакше відповідь, що займає перше місце, хороша.


1
Я від усієї душі погоджуюся: побудова проміжного дерева DOM - це марно пам'ять. Танськ за цю відповідь.
Флоріан F

9

Використання шкали:

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

Ви можете зробити це і в Java, якщо залежати від scala-library.jar. Це виглядає приблизно так:

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);
    }
}

PrettyPrinterОб'єкт побудований з два інтсія, першими з яких максимальною довжиною лінії , а другі етап вдавлення.


9

трохи покращена версія від 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();
} 

де повторенняString (стек ++); метод ..?
користувач1912935

2
приватний статичний String repeatString (int stack) {StringBuilder indent = new StringBuilder (); for (int i = 0; i <стек; i ++) {indent.append (""); } повернути indent.toString (); }
codekraps

Відступ не працює нормально в кінцевих тегах. Вам потрібно змінити } else if (row.startsWith("</")) {частину на це: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

Тільки для подальшого ознайомлення, ось рішення, яке працювало на мене (завдяки коментарю, який @George Хокінс розмістив в одній з відповідей):

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

Якщо ви впевнені, що у вас є дійсний XML, цей простий і уникає дерев XML DOM. Можливо, є якісь помилки, коментуйте, якщо ви щось бачите

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
де метод repeatString ..?
користувач1912935

3
приватний статичний String repeatString (int stack) {StringBuilder indent = new StringBuilder (); for (int i = 0; i <стек; i ++) {indent.append (""); } повернути indent.toString (); }
codekraps

Так [user1912935], що написав @codeskraps, має бути досить простим :)
milosmns

Об’єднання з StringBuilder всередині циклу: погана практика.
james.garriss

@ james.garriss Але дуже легко розділити на нові рядки, це просто ілюструє простий підхід без дерев DOM.
milosmns

5

Усі вищезазначені рішення для мене не спрацювали, тоді я знайшов це http://myshittycode.com/2014/02/10/java-properly-indenting-xml-string/

Підказкою є видалення пробілів з 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
Зауважте, що використання властивості "{ xml.apache.org/xslt } відступ" призведе до конкретної реалізації трансформатора.
vallismortis

1
З усіх рішень цей працював найкраще. У мене було пробіли та нові рядки вже в моєму XML плюс я не хотів додавати більше залежностей до свого проекту. Я б хотів, щоб мені не довелося розбирати XML, але добре.
Фабіо

5

Цей код нижче працює ідеально

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

Я все їх змішую і пишу одну маленьку програму. Це зчитування з файлу xml та друк. Просто замість xzy дайте шлях до файлу.

    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

Просто ще одне рішення, яке працює для нас

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

Використання 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

В якості альтернативи відповіді від max , codecraps , David Easley та milosmns , розгляньте мою легку, високопродуктивну бібліотеку симпатичних принтерів: 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
}

Іноді, як, наприклад, при запуску знущаються служб SOAP безпосередньо з файлу, добре мати гарний принтер, який також обробляє вже симпатичний друк XML:

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

Як зауважили деякі, гарне друк - це лише спосіб представлення XML у більш зручній для людини формі - пробіл просто не належить до ваших XML-даних.

Бібліотека призначена для симпатичного друку для цілей ведення журналів, а також включає функції фільтрації (видалення підмереж / анонімізація) та гарного друку XML у вузлах CDATA та Text.



2

Підкреслення-ява має статичний метод U.formatXml(string). Я організатор проекту. Живий приклад

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>"));
    }
}

Вихід:

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

Це круто!
сеньйор

1

є дуже хороша утиліта xml командного рядка під назвою xmlstarlet ( http://xmlstar.sourceforge.net/ ), яка може робити багато речей, якими користується багато людей.

Ви можете виконати цю програму програмно за допомогою Runtime.exec, а потім прочитати відформатований вихідний файл. Він має більше варіантів та кращу звітність про помилки, ніж може запропонувати кілька рядків коду Java.

завантажити xmlstarlet: http://sourceforge.net/project/showfiles.php?group_id=66612&package_id=64589


1

Я виявив, що в Java 1.6.0_32 звичайний метод гарненько надрукувати рядок XML (використовуючи Трансформатор з нульовим чи ідентифікаційним xslt) не веде себе так, як хотілося б, якщо теги просто розділені пробілом, на відміну від відсутності розділення текст. Я намагався використовувати <xsl:strip-space elements="*"/>в своєму шаблоні безрезультатно. Найпростішим рішенням, який я знайшов, було знімати простір так, як я хотів, використовуючи фільтр SAXSource та XML. Оскільки моє рішення стосувалося ведення журналу, я також розширив це для роботи з неповними фрагментами XML. Зауважте, звичайний метод, здається, працює добре, якщо ви використовуєте DOMSource, але я не хотів використовувати це через неповноту та обсяг пам'яті.

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

Я знайшов тут рішення для Java 1.6+ і не переформатую код, якщо він уже відформатований. Той, який працював для мене (і переформатував уже відформатований код), був наступним.

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);
    }
}

Це хороший інструмент для використання у ваших тестових одиницях для порівняння повноцінних xml.

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

Для тих, хто шукає швидке і брудне рішення - для якого XML не повинен бути 100% дійсним. наприклад, у випадку реєстрації REST / SOAP (ви ніколи не знаєте, що надсилають інші ;-))

Я знайшов і вдосконалив дістаний код, який я знайшов в Інтернеті, який, на мою думку, досі відсутній як можливий підхід:

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));
}

ось вихід:

<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

Я бачив одну відповідь, використовуючи Scala, тому ось ще одна Groovy, на всякий випадок, якщо хтось вважає це цікавою. Відступ за замовчуванням - 2 кроки, XmlNodePrinterконструктору також може передаватися інше значення.

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()

Використання з Java, якщо шорсткий баночка знаходиться на уроці

  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

Якщо вам не потрібна велика кількість відступів, але декілька розривів рядків, це може бути достатньо просто переробити ...

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

Код приємний, а не результат через відсутність відступу.


(Про рішення з відступом див. Інші відповіді.)


1
Гмммм ... Просто голосно подумавши, кому буде потрібно таке рішення? Я бачу лише область, яку ми отримуємо від деяких веб-служб, і просто для перевірки цих даних та її дійсності, розробнику чи тестувальнику можуть знадобитися такі легкі. Інакше не вдалий варіант ....
Sudhakar Chavali

1
@SudhakarChavali Я розробник. мені може знадобитися це для брудних хакер println () та log.debug (); тобто я можу використовувати файли журналів лише з обмеженого серверного середовища (з інтерфейсом веб-адміністратора замість доступу до оболонки), а не за допомогою крок за кроком налагодження програми.
comonad
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.