В предыдущей статье мы рассматривали возможности 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"
}
]
}