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

Ми хочемо отримати рядок: Користувач: 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">
    Користувач: <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, тільки без зайвої церемонності.

Схема pipeline така:

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">Користувач: <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()) // Користувач: 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">
Звіт про покупки:
<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">
Звіт про покупки:
<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())
    // Звіт про покупки:
    // - 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 гаразд")
    } catch (e: Exception) {
        println("Помилка XSLT: ${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
Опитування
XML практикум, рівень 50, лекція 4
Недоступний
XML практикум
XML практикум
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ