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
Ми хочемо отримати рядок: Користувач: 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: TransformerFactory → Transformer → transform(...)
Тепер перейдемо до «механіки запуску». У 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(...) окремо) робить діагностику на порядок приємнішою.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ