null

Конвертация XML в JSON с Groovy

​​​​​​В предыдущей статье мы рассматривали возможности Groovy по конвертации данных из формата JSON в формат XML, в том числе и в контексте NiFi. Специфика использования Groovy-скриптов в NiFi и причина, почему бывает полезно их использовать, также описаны в указанной статье, не будем вновь останавливаться на этом. Сегодня же рассмотрим обратную ситуацию - когда необходимо конвертировать данные из XML в JSON. Происходит это несколько иным образом по сравнению с процессом перехода от JSON к XML и также достойно отдельной заметки в блоге.

На этот раз возьмем вот такой демонстрационный XML, который необходимо преобразовать к JSON-формату:

<Request Name="DemoService">
    <Service>
        <Name>DemoService</Name>
        <Version>1</Version>
    </Service>
    <Data>
        <Main>
            <Code>10</Code>
            <Name>Demo</Name>
            <Bool>N</Bool>
            <Empty></Empty>
            <Date>2024-07-26</Date>
            <Time>15:05:30</Time>
        </Main>
        <Details>
            <Detail>
                <Num>1</Num>
                <Name>Test 1</Name>
                <Amount>1800</Amount>
            </Detail>
            <Detail>
                <Num>2</Num>
                <Name>Test 2</Name>
                <Amount>2400</Amount>
            </Detail>
            <Detail>
                <Num>3</Num>
                <Name>Test 3</Name>
                <Amount>500</Amount>
            </Detail>
        </Details>
    </Data>
</Request>

В этот раз нам понадобятся следующие классы:

  • groovy.util.XmlParser - парсер XML, используемый для преобразования XML-текста в дерево узлов в соответствии с заданной структурой;
  • groovy.json.JsonBuilder - инструмент для построения структур данных в формате JSON с удобным JSON подобным синтаксисом.

Скрипт для конвертации же мы напишем следующим образом:

import org.apache.commons.io.IOUtils
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import groovy.json.JsonBuilder
import groovy.util.XmlParser

def flowFile = session.get()
if (!flowFile) {
    return
}

def formatMainDataDate(date) {
    return new SimpleDateFormat("yyyyMMdd").format(Date.parse("yyyy-MM-dd", date))
}

try {
    flowFile = session.write(
        flowFile,
        {
            inputStream, outputStream -> def text = IOUtils.toString(inputStream, StandardCharsets.UTF_8)
            
            def doc = new XmlParser().parseText(text)
            def json = new JsonBuilder()
            def details = []
            for (detail in doc.Data.Details.Detail) {
                def mapDetailData = [:]
                mapDetailData["num"] = detail.Num.text()
                mapDetailData["name"] = detail.Name.text().replace(" ", "_")
                mapDetailData["amount"] = detail.Amount.text()
                details << mapDetailData
            }
            
            json {
                mainData {
                    type          "XML to JSON demo"
                    serviceName   doc.@'Name'
                    code          "Example: " + doc.Data.Main.Code.text()
                    name          doc.Data.Main.Name.text().toLowerCase()
                    empty         ""
                    bool          (doc.Data.Main.Bool.text() == "N")
                    date          this.formatMainDataDate(doc.Data.Main.Date.text())
                }
                detailsList(
                    details
                )
            }
            
            outputStream.write(json.toString().getBytes(StandardCharsets.UTF_8))
        } as StreamCallback
    )
    
    flowFile = session.putAttribute(flowFile, "filename", flowFile.getAttribute('filename').tokenize('.')[0] + '.json')
    session.transfer(flowFile, REL_SUCCESS)
} catch (Exception e) {
    log.error('Error during XML to JSON conversion', e)
    session.transfer(flowFile, REL_FAILURE)
}

Опишем, что и для чего используется.

Для начала повторим общие моменты с предыдущего раза:

  • строки 1-5 представляют собой импорт необходимых пакетов и классов;
  • строки 7-10 специфичны для NiFi; они нужны для получения текущего файла потока (или окончанию работы с файлом, если его нет);
  • строки 12-14 - типовая функция для форматирования даты, которая будет использована далее;
  • строки 16-20 и 48-57 - каркас для работы с файлами потока NiFi, внутрь которого следует писать все остальное, более подробно это также было описано в предыдущей статье.

Непосредственно работа по конвертации же происходит в участке кода начиная со строки 22 и заканчивая строкой 46. Сначала мы получаем дерево XML-объектов (строка 22), спарсив строку с содержимым flowfile’а (при необходимости переменная text может быть заполнена иным способом). Затем инициализируем JsonBuilder, с помощью которого будем строить результирующий JSON. В коде к элементам XML мы сможем обращаться как doc.Data.Main.Code и подобным образом в зависимости от того, к какому тегу хотим получить доступ.

В строках 24-31 мы занимаемся подготовкой списка элементов для представления его в JSON. Для этого мы создаем список, в который последовательно добавляем пары “ключ - значение”. В цикле мы проходимся по всем элементам Detail, располагающимся внутри тега Details. Для каждого из них мы объявляем свою структуру, где ключом будет название элемента в JSON, а значением - текст, который будет соответствовать указанному элементу. В качестве такого текста мы используем XML-элементы (строки 27-29), после чего добавляем новый элемент в общий список details.

В строках 33-46 происходит непосредственно построение результирующего JSON на основе рассматриваемого XML. Структура итогового файла будет соответствовать тому, что мы описываем в JsonBuilder. В нашем случае JSON будет состоять из mainData с вложенной в него “основной” информацией и detailsList со списком “деталей”. С деталями, как и со списками в целом, все получается достаточно просто - передаем список, собранный несколькими строками ранее, и как итог получаем его представление в JSON. Заполнение же mainData происходит в строках 35-41, где мы указываем список JSON-элементов и значения, которые им необходимо присвоить.

Как и в случае с построением XML, в JSON мы также можем использовать множество возможностей языка, позволяющих записать в JSON следующее:

  • обычную фиксированную строку (строка 35);
  • значение атрибута указанного тега (строка 36), стоит обратить внимание на синтаксис обращения к атрибуту Name, а также тот факт, что для обращения к корневому XML-тегу Request мы используем переменную doc без спуска в дальнейшую вложенность;
  • смесь строк и значений XML-элементов с возможностью проведения дополнительных операций (строки 37-38);
  • пустую строку (строка 39);
  • значение в зависимости от поля типа boolean (строка 40);
  • результат работы функции (строка 41), при этом важно обратить внимание на ключевое слово this() при обращении к функции (!), без него название функции будет воспринято как еще один JSON-элемент.

В результате работы мы имеем сконвертированные данные в формате JSON. Как и в случае конвертации в XML, скрипт предоставляет гибкие возможности для поддержки цикла преобразования данных, позволяя менять структуру результирующего JSON’а практически без ограничений.

{
  "mainData": {
    "type": "XML to JSON demo",
    "serviceName": "DemoService",
    "code": "Example: 10",
    "name": "demo",
    "empty": "",
    "bool": true,
    "date": "20240726"
  },
  "detailsList": [
    {
      "num": "1",
      "name": "Test_1",
      "amount": "1800"
    },
    {
      "num": "2",
      "name": "Test_2",
      "amount": "2400"
    },
    {
      "num": "3",
      "name": "Test_3",
      "amount": "500"
    }
  ]
}