Сегодняшняя заметка в блоге отчасти связана с серией статей об Apache NiFi, но также может считаться и отдельным произведением, поскольку работать с форматами данных JSON и XML приходится регулярно в системах любой сложности и любого профиля. Если нужна только часть со скриптом для конвертации, то можно смело игнорировать все, что касается NiFi.
Прошлые статьи об Apache NiFi доступны по ссылкам:
Чтобы преобразовать данные из одного формата в другой, Apache NiFi предоставляет широкий набор инструментов из коробки, включая, например, такие процессоры как ConvertRecord и такие сервисы как JsonTreeReader и XMLRecordSetWriter. NiFi позволяет единообразным образом конвертировать друг в друга CSV, XML, JSON и многие другие популярные форматы. Тем не менее решения на базе встроенных в NiFi решений могут показаться громоздкими и недостаточно гибкими, поскольку процесс конвертации весьма комплексный - необходимо создать схему, настроить несколько сервисов (как минимум два - для чтения и записи), настроить несколько процессоров. Возможность влиять на данный процесс при этом весьма низкая, а работает все стабильно только если никаких дополнительных операций с данными производить не требуется. Пример конвертации из JSON в XML стандартными средствами NiFi представлен здесь.
Человеку, владеющими навыками программирования, может показаться, что было бы гораздо удобнее воплотить весь описанный процесс в коде, не прибегая к долгой настройке множества сервисов и процессоров. К счастью, есть альтернативное решение. Для преобразования JSON в XML можно использовать возможности языка программирования Groovy, позволяющего легко и гибко формировать XML на основе полученного JSON. Именно пример конвертации данных с использованием Groovy скрипта мы и рассмотрим подробнее.
Допустим, у нас есть следующий демонстрационный JSON, содержащий данные разных типов:
{
"meta_info": {
"type": "example_object"
},
"objects": [
{
"main_info": {
"id": "1-57234",
"date": "2024-06-25"
},
"details": [
{
"num": "1_1",
"amount": 1500,
"name": "DEMO",
"detail_type": "type_1",
"boolean_field_example": false,
"custom_field_1": "First field",
"custom_field_2": "Second field",
},
{
"num": "1_2",
"amount": 100,
"name": "Another Demo",
"detail_type": "type_2",
"boolean_field_example": true,
"custom_field_1": "Third field",
"custom_field_2": "Forth field",
}
]
}
Мы хотим преобразовать его в XML, но с определенными нюансами. Например, переименовать поля, какое-то значение удвоить, у какого-то поля выбрать только часть строки и так далее. Groovy даст нам возможность все это сделать. Основными нашими помощниками будут:
- groovy.json.JsonSlurper - парсер JSON и его метод parseText, возвращающий структуру из списков (list) и пар ключ-значение (map), которая соответствуют полученному JSON
- groovy.xml.MarkupBuilder - вспомогательный класс для создания XML-разметки, который сформирует для нас строку в формате XML так, как мы сами ему зададим
В NiFi для Groovy-скриптов используется процессор ExecuteGroovyScript:
Внутри в Script Body добавим следующий скрипт:
import org.apache.commons.io.IOUtils
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import groovy.json.*
import groovy.xml.MarkupBuilder
def flowFile = session.get()
if (!flowFile) {
return
}
def formatMainInfoDate(obj) {
return Date.parse("yyyy.MM.dd", obj.main_info.date)
}
try {
flowFile = session.write(
flowFile,
{
inputStream, outputStream -> def text = IOUtils.toString(inputStream, StandardCharsets.UTF_8)
def writer = new StringWriter()
def parsed_json = new JsonSlurper().parseText(text)
def obj_data = parsed_json.objects[0]
def mB = new MarkupBuilder(writer)
mB.Result(Attribute: "XML conversion example") {
MainInfo {
Title("JSON to XML groovy guide")
EmptyField("")
NumberField(obj_data.main_info.id)
URL(flowFile.getAttribute("object.data.url"))
CustomDate(formatMainInfoDate(obj_data))
CurrentDate(new SimpleDateFormat("yyyy-MM-dd").format(new Date()))
CurrentTime(new SimpleDateFormat("HH:mm:ss").format(new Date()))
}
Details {
obj_data.details.each {
detail -> Detail {
DetailNr(detail.num.split('_')[1])
TypeInfo(detail.detail_type.replace("_", " "))
CustomField(detail.detail_type == "type_1" ? detail.custom_field_1 : detail.custom_field_2)
DoubleAmount(detail.amount * 2)
if (detail.name.length() >= 10) {
DetailName(detail.name)
} else {
DetailName("Too short")
}
BooleanField((detail.boolean_field_example) ? "T": "F")
}
}
}
}
}
outputStream.write(writer.toString().getBytes(StandardCharsets.UTF_8))
} as StreamCallback
)
flowFile = session.putAttribute(flowFile, "filename", flowFile.getAttribute('filename').tokenize('.')[0] + '_translated.xml')
session.transfer(flowFile, REL_SUCCESS)
} catch (Exception e) {
session.transfer(flowFile, REL_FAILURE)
}
Опишем, что и для чего здесь нужно.
Строки 1-5 представляют собой импорт необходимых пакетов и классов (ничего особенного).
Строки 7-10 специфичны для NiFi; они нужны для получения текущего файла потока (или окончанию работы с файлом, если его нет).
Строки 12-14 - типовая функция для форматирования даты, которая будет использована далее.
Строки 16-20 и 55-62 - каркас для работы с файлами потока NiFi, внутрь которого следует писать все остальное. В данной конструкции мы записываем в файл потока содержимое, получаемое после конвертации, а также маршрутизируем файл по одному из отношений (в зависимости от того, прошла конвертация успешно или возникли ошибки). В строке 58 также происходит изменение названия файла.
В строках 21-24 происходит инициализация всего необходимого для парсинга JSON и построения XML. Строка def obj_data = parsed_json.objects[0] означает, что через данную переменную мы получаем доступ к первому элементу списка objects, указанному в JSON'е выше.
В строках 26-52 происходит непосредственно построение результирующего XML на основе рассматриваемого JSON'а. Суть проста - иерархия тегов в XML будет соответствовать вложенности элементов, что мы передаем в Markup Builder. В нашем случае корневым тегом итогового XML будет тег Result (строка 26). Здесь же мы задаем и атрибут данного тега под названием "Attribute". Вложенными для Result будут элементы MainInfo с его дочерними тегами, а также список Details. Последнее заслуживает отдельного внимания. Groovy позволяет удобным образом работать со списками элементов, представленными в JSON. Через obj_data.details.each мы проходим по каждому элементу массива details и превращаем каждую "деталь" в отдельный XML тег с характерными для него вложенными элементами. Подобным образом происходит трансформация списков элементов между форматами.
Обратим внимание на широкие возможности формирования значений XML элементов. Groovy позволяет нам записать в XML элемент следующее:
- обычную фиксированную строку (строка 28);
- пустую строку (строка 29);
- значение взятое напрямую из JSON (строка 30);
- значение взятое из атрибута файла потока (строка 31);
- значение, которое возвращает функция (строка 32);
- текущую дату или время (строки 33, 34);
- строку, полученную в результате стандартных строковых преобразований, таких как split и replace (строки 39, 40);
- значение одного из двух полей в зависимости от значения какого-либо поля (строка 41);
- результат математической операции (строка 42);
- значение в зависимости от поля типа boolean (строка 48)
Кроме того, внутри MarkupBuilder'а возможно даже использование условных операторов (строки 43-47).
В результате работы мы имеем сконвертированные данные в формате XML. При этом скрипт предоставляет гибкие возможности для поддержки цикла преобразования данных, позволяя менять названия тегов, их порядок и их содержимое практически как угодно без каких-либо проблем и с минимальными ограничениями.
<Result Attribute='XML conversion example'>
<MainInfo>
<Title>JSON to XML groovy guide</Title>
<EmptyField></EmptyField>
<NumberField>1-57234</NumberField>
<URL>example.com</URL>
<CustomDate>2024.06.25</CustomDate>
<CurrentDate>2024-07-01</CurrentDate>
<CurrentTime>16:05:03</CurrentTime>
</MainInfo>
<Details>
<Detail>
<DetailNr>1</DetailNr>
<TypeInfo>type 1</TypeInfo>
<CustomField>First field</CustomField>
<DoubleAmount>3000</DoubleAmount>
<DetailName>Too short</DetailName>
<BooleanField>F</BooleanField>
</Detail>
<Detail>
<DetailNr>2</DetailNr>
<TypeInfo>type 2</TypeInfo>
<CustomField>Forth field</CustomField>
<DoubleAmount>200</DoubleAmount>
<DetailName>Another Demo</DetailName>
<BooleanField>T</BooleanField>
</Detail>
</Details>
</Result>