JavaRush /Курсы /Kotlin SELF /XSLT: идея преобразований и минимальный pipeline

XSLT: идея преобразований и минимальный pipeline

Kotlin SELF
50 уровень , 4 лекция
Открыта

1. Введение

Когда вы впервые видите XSLT, возникает естественная мысль: «Подождите… у нас же уже есть XPath. Зачем ещё один язык, да ещё похожий на XML?» Это нормальная реакция. XPath — это про выбрать данные. XSLT — про собрать результат по правилам. Представьте: XPath — это «найди нужные ингредиенты», а XSLT — «приготовь блюдо и красиво подай».

Если говорить чуть практичнее, XSLT встречается там, где нужно:

  • преобразовать один XML‑контракт в другой (например, «старый формат партнёра → наш формат»);
  • сформировать текстовый отчёт из XML (почти как шаблонизатор, но для XML);
  • оставить преобразование «на стороне интеграции», не переписывая код на Java/Kotlin.

И важный момент для Kotlin/JVM: нам не нужно искать экзотические библиотеки — на JVM исторически есть стандартные API для XSLT, и Kotlin спокойно вызывает их, потому что он отлично дружит с Java‑миром.

Мини-таблица XPath vs XSLT

Инструмент Главная идея Результат Типичный вопрос
XPath найти/выбрать узлы или значения «Где в документе user[@id='42']/name
XSLT преобразовать/собрать другой XML или текст «Как из <user> сделать <person> или строку отчёта?»

2. Ментальная модель XSLT: «вход + правила → выход»

Перед кодом полезно настроить голову на правильную картинку. XSLT — это набор правил (шаблонов), которые говорят: «если текущий узел вот такой, то в выходной документ надо записать вот это». Это похоже на when, только в мире XML‑дерева: вместо значений у вас узлы, вместо веток — шаблоны, вместо println — генерация результата.

С точки зрения «чистого» понимания нам хватит трёх идей:

  1. XSLT — это тоже XML‑документ, в котором есть элементы xsl:*.
  2. Шаблон (xsl:template) выбирает часть входного XML (через match="..."), а внутри описывает, что писать в выход.
  3. Чтобы вытащить данные из входного XML, чаще всего используют XPath‑выражения внутри XSLT (select="...").

То есть XPath в XSLT не исчезает — он становится «встроенным навигатором» по входному документу.

3. Самый маленький XSLT, который вообще имеет смысл

Давайте соберём минимальный, но живой XSLT. Мы возьмём входной XML и сделаем выход текстом, чтобы было проще увидеть результат глазами (как обычный println, только через преобразование).

Входной XML

<user id="42">
  <name>Ann</name>
</user>

XSLT

Мы хотим получить строку: User: Ann.

Чтобы XSLT был «правильным», ему нужен корневой элемент xsl:stylesheet, правильное пространство имён и хотя бы один шаблон.

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="/user">
    User: <xsl:value-of select="name"/>
  </xsl:template>
</xsl:stylesheet>

Здесь важно заметить две вещи. Во‑первых, match="/user" означает «этот шаблон применяется к корневому элементу <user>». Во‑вторых, select="name" — это XPath относительно <user>, то есть «взять дочерний элемент <name>».

4. Минимальный pipeline на JVM: TransformerFactoryTransformertransform(...)

Теперь перейдём к «механике запуска». В JDK есть пакет javax.xml.transform, который умеет компилировать XSLT и применять его. Код на Kotlin выглядит почти как Java, только без лишней церемонии.

Схема пайплайна такая:

flowchart LR
    A[XML Source] -->|transform| T[Transformer]
    S[XSLT Source] -->|compile| F[TransformerFactory]
    F --> T
    T --> B[Result: text/xml]

Нам нужны три типа сущностей:

  • Source для входного XML,
  • Source для XSLT,
  • Result для выхода.

Пример: «XML‑строка → текстовая строка»

Обратите внимание: пример короткий (и намеренно «в лоб»), чтобы было видно именно pipeline.

import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

fun main() {
    val xml = "<user id=\\"42\\"><name>Ann</name></user>"
    val xslt = """<xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="text"/>
        <xsl:template match="/user">User: <xsl:value-of select="name"/></xsl:template>
    </xsl:stylesheet>""".trimIndent()

    val transformer = TransformerFactory.newInstance()
        .newTransformer(StreamSource(StringReader(xslt)))

    val out = StringWriter()
    transformer.transform(StreamSource(StringReader(xml)), StreamResult(out))

    println(out.toString().trim()) // User: Ann
}

Здесь StreamSource(StringReader(...)) — это способ «скормить строку» как источник. StringWriter — это наш приёмник результата (как будто «печатаем в строку»).

5. Практика: XSLT для отчётов и конвертации контрактов

Чтобы примеры не выглядели как отдельные островки, давайте продолжим ту же линию, что была в лекциях про XML/DOM/XPath: у нас есть небольшой XML с данными, и мы хотим:

  • либо превратить его в человекочитаемый отчёт (текст),
  • либо превратить его в другой XML с более удобной структурой.

Представим, что мы в нашем учебном консольном приложении храним покупки (по сути — простейшие «траты») в XML:

<purchases>
  <purchase category="food" amount="12.50">Milk</purchase>
  <purchase category="food" amount="2.10">Bread</purchase>
  <purchase category="fun" amount="15.00">Movie</purchase>
</purchases>

В прошлых лекциях мы могли бы это распарсить DOM’ом и посчитать суммы. Сейчас мы делаем другой трюк: пусть XSLT сам соберёт текстовый отчёт.

XSLT «XML → текст»: простой отчёт списком

Важно не пытаться сделать «весь отчёт мира» в XSLT: это обзор. Нам достаточно научиться делать элементарные вещи: пройтись по элементам и вывести поля.

XSLT для отчёта

Здесь используется xsl:for-each — «пройтись по набору узлов». Внутри мы берём атрибуты через @category, @amount, а текст покупки — через ..

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="/purchases">
Purchases report:
<xsl:for-each select="purchase">
- <xsl:value-of select="."/> (<xsl:value-of select="@category"/>): $<xsl:value-of select="@amount"/>
</xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Обратите внимание на «бытовую» деталь: пробелы и переносы строк в XSLT для method="text" становятся частью результата. Это не баг, это фича. Но из‑за этого иногда приходится делать trim() на выходе в Kotlin — ровно так же, как мы делали trim() на textContent.

Запуск из Kotlin

import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

fun main() {
    val xml = """<purchases>
        <purchase category="food" amount="12.50">Milk</purchase>
        <purchase category="food" amount="2.10">Bread</purchase>
    </purchases>""".trimIndent()

    val xslt = """<xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="text"/>
        <xsl:template match="/purchases">
Purchases report:
<xsl:for-each select="purchase">
- <xsl:value-of select="."/> (<xsl:value-of select="@category"/>): $<xsl:value-of select="@amount"/>
</xsl:for-each>
        </xsl:template>
    </xsl:stylesheet>""".trimIndent()

    val t = TransformerFactory.newInstance().newTransformer(StreamSource(StringReader(xslt)))
    val out = StringWriter()
    t.transform(StreamSource(StringReader(xml)), StreamResult(out))

    println(out.toString().trim())
    // Purchases report:
    // - Milk (food): $12.50
    // - Bread (food): $2.10
}

Да, выглядит «как будто странный шаблонизатор из прошлого». Но именно в таком виде XSLT часто и живёт в интеграциях: небольшой файл‑шаблон и код, который его применяет.

XSLT «XML → XML»: меняем структуру данных под другой контракт

Теперь сделаем второй типичный кейс: превращаем один XML в другой. Это полезно, когда:

  • у вас входной XML неудобен для XPath/DOM анализа;
  • вы хотите привести данные к единому формату перед обработкой.

Например, пусть исходные покупки хранятся как:

<purchase category="food" amount="12.50">Milk</purchase>

А нам хочется получить:

<item>
  <title>Milk</title>
  <category>food</category>
  <amount>12.50</amount>
</item>

XSLT, который собирает новый XML

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>
  <xsl:template match="/purchases">
    <items>
      <xsl:for-each select="purchase">
        <item>
          <title><xsl:value-of select="."/></title>
          <category><xsl:value-of select="@category"/></category>
          <amount><xsl:value-of select="@amount"/></amount>
        </item>
      </xsl:for-each>
    </items>
  </xsl:template>
</xsl:stylesheet>

Запуск из Kotlin

import java.io.StringReader
import java.io.StringWriter
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

fun main() {
    val xml = """<purchases>
        <purchase category="food" amount="12.50">Milk</purchase>
    </purchases>""".trimIndent()

    val xslt = """<xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="xml" indent="yes"/>
        <xsl:template match="/purchases">
          <items><xsl:for-each select="purchase">
            <item>
              <title><xsl:value-of select="."/></title>
              <category><xsl:value-of select="@category"/></category>
              <amount><xsl:value-of select="@amount"/></amount>
            </item>
          </xsl:for-each></items>
        </xsl:template>
    </xsl:stylesheet>""".trimIndent()

    val out = StringWriter()
    TransformerFactory.newInstance()
        .newTransformer(StreamSource(StringReader(xslt)))
        .transform(StreamSource(StringReader(xml)), StreamResult(out))

    println(out.toString())
}

Теперь важная «связка» с прошлой лекцией: после XSLT‑преобразования вы можете взять результат (как строку XML), снова распарсить DOM’ом и использовать XPath уже по новой структуре. Иногда это реально упрощает логику, если оригинальный формат «кривоват».

XSLT из файлов: как это выглядит в нормальной жизни

До этого мы держали XSLT в строке. Это нормально для учебного примера, но в реальном проекте XSLT почти всегда живёт как отдельный файл рядом с приложением (или приходит как ресурс).

С точки зрения JVM‑API запуск тот же, просто StreamSource берётся из файла.

Пример: применить report.xslt к purchases.xml

import java.io.File
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamResult
import javax.xml.transform.stream.StreamSource

fun main() {
    val xmlFile = File("purchases.xml")
    val xsltFile = File("report.xslt")

    val transformer = TransformerFactory.newInstance()
        .newTransformer(StreamSource(xsltFile))

    transformer.transform(StreamSource(xmlFile), StreamResult(System.out))
    // Вывод уйдёт прямо в консоль
}

StreamResult(System.out) — очень удобная штука для консольных инструментов: результат XSLT сразу печатается, и вам не нужно собирать строку.

6. Ошибки и диагностика: что именно «сломалось» — XML или XSLT

На практике XSLT добавляет ещё один слой возможных проблем. Раньше у вас мог «сломаться» входной XML (не парсится, не well‑formed). Теперь может сломаться ещё и XSLT: например, неправильное пространство имён, синтаксическая ошибка или обращение к узлу, которого нет (иногда это просто даст пустой результат, иногда — ошибку, зависит от ситуации и движка).

Хорошая привычка: в коде различать этапы.

  • Этап 1: «скомпилировать» XSLT в Transformer
  • Этап 2: применить transform(...)

Мини‑пример с try/catch и понятным сообщением

import java.io.StringReader
import javax.xml.transform.TransformerFactory
import javax.xml.transform.stream.StreamSource

fun main() {
    val brokenXslt = "<xsl:stylesheet></xsl:stylesheet>" // нет xmlns:xsl

    try {
        TransformerFactory.newInstance()
            .newTransformer(StreamSource(StringReader(brokenXslt)))
        println("XSLT ok")
    } catch (e: Exception) {
        println("XSLT error: ${e.message}")
    }
}

Этот пример кажется «слишком простым», но он показывает ключевую мысль: ошибка XSLT — это не то же самое, что ошибка входного XML. И когда вы отлаживаете интеграцию, важно не писать универсальное «что-то пошло не так», а хотя бы понимать, на каком шаге.

7. Типичные ошибки при работе с XSLT

Ошибка №1: путать XPath и XSLT и ждать от evaluate(...), что он «сгенерирует результат».
XPath сам по себе ничего не «рисует» и не «строит». Он выбирает значения или узлы. Если вам нужно собрать новый XML или текстовый отчёт, XPath будет только частью решения, а сборка результата должна быть описана шаблонами XSLT.

Ошибка №2: забыть xmlns:xsl="http://www.w3.org/1999/XSL/Transform" и получить загадочные ошибки.
Это классика жанра. Визуально XSLT похож на XML, и кажется, что «ну почти правильно». Но без корректного пространства имён элементы xsl:* не будут распознаны как команды, и движок либо упадёт, либо сделает что-то бессмысленное.

Ошибка №3: ожидать, что форматирование не влияет на результат.
Если вы делаете method="text", переносы строк в XSLT буквально станут частью результата. А если вы делаете method="xml", отступы и переносы будут зависеть от настроек и движка (например, indent="yes" может работать по‑разному). Поэтому часто приходится дополнительно нормализовать строку (trim()) или аккуратно контролировать шаблон.

Ошибка №4: собирать XSLT как огромную Kotlin‑строку без необходимости.
Строка с тройными кавычками — отличный учебный вариант, но в реальной жизни поддерживать «полотно XSLT внутри Kotlin‑кода» сложно: кавычки, отступы, экранирование — и вот уже отладка превращается в квест. Гораздо спокойнее хранить XSLT в отдельном .xslt файле и читать его как ресурс/файл.

Ошибка №5: не разделять этап «XSLT компилируется» и этап «преобразование выполняется».
Когда всё обёрнуто в один try/catch, вы теряете понимание, что именно сломалось: файл XSLT битый или входной XML не подходит. Даже простое разнесение на два шага (newTransformer(...) отдельно, transform(...) отдельно) делает диагностику на порядок приятнее.

1
Задача
Kotlin SELF, 50 уровень, 4 лекция
Недоступна
Консольный конвертер
Консольный конвертер
1
Задача
Kotlin SELF, 50 уровень, 4 лекция
Недоступна
Печать бейджа
Печать бейджа
1
Задача
Kotlin SELF, 50 уровень, 4 лекция
Недоступна
Текстовый отчёт
Текстовый отчёт
1
Задача
Kotlin SELF, 50 уровень, 4 лекция
Недоступна
Миграция тегов
Миграция тегов
1
Опрос
XML практикум, 50 уровень, 4 лекция
Недоступен
XML практикум
XML практикум
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ