null

Конвертация JSON в XML с Groovy (для NiFi и не только)

Сегодняшняя заметка в блоге отчасти связана с серией статей об 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>