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 — генерация результата.
С точки зрения «чистого» понимания нам хватит трёх идей:
- XSLT — это тоже XML‑документ, в котором есть элементы xsl:*.
- Шаблон (xsl:template) выбирает часть входного XML (через match="..."), а внутри описывает, что писать в выход.
- Чтобы вытащить данные из входного 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: TransformerFactory → Transformer → transform(...)
Теперь перейдём к «механике запуска». В 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(...) отдельно) делает диагностику на порядок приятнее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ