xsl:text>Everybody
needs
space
Результатом этого преобразования будет текст
Everybody needs space
Несложно понять, насколько мощным средством являются расширения. Они фактически позволяют реализовать в преобразованиях функциональность традиционных языков программирования. Иными словами, почти все то, что можно сделать в обычных языках программирования, можно сделать и в преобразованиях.
Как это часто бывает, за дополнительные возможности приходится платить. Использование механизма расширений предъявляет определенные требования и накладывает некоторые ограничения.
□ Реализация механизма расширений в текущей версии языка целиком и полностью зависит от производителей процессоров. Вследствие этого интерфейсы расширения различных XSLT-процессоров могут отличаться даже для одного языка программирования. Это в итоге ведет к несовместимости расширений и непереносимости XSLT-решений между различными процессорами.
□ Возможность использования того или иного языка для написания расширений зависит от наличия интерфейса XSLT-процессора для этого языка. Не следует ожидать, что любой процессор сможет работать с расширениями, написанными на любом языке, то есть, иначе говоря, расширения привязывают преобразования к строго определенному процессору или, в лучшем случае, группе процессоров.
□ В то время как сам XSLT не имеет сторонних эффектов, расширения этого принципа придерживаться не обязаны. Вследствие этого преобразования, в которых есть расширения с побочными эффектами могут из-за различных методов обработки входящего документа генерировать на разных процессорах разный результат.
Итак, вопрос, использовать расширения или нет — это вопрос "функциональность против переносимости", и, хотя его решение будет всегда зависеть от конкретной задачи, существуют также и довольно общие критерии оценки, которые мы приведем в следующей таблице (табл. 10.1).
Таблица 10.1. Использование расширений: критерии за и против
Использовать расширения стоит, если: Использовать расширения не стоит, если: преобразования будут выполняться на заранее известном процессоре или группе процессоров; целевой процессор неизвестен. Преобразования должны быть переносимы, насколько это возможно; в XSLT нет средств для выполнения требуемой задачи, либо они очень неэффективны; в XSLT имеются средства для выполнения требуемой задачи; преобразование должно обладать побочными эффектами; преобразование может обойтись без побочных эффектов; целевой процессор предоставляет интерфейс для хорошо известного разработчику языка программирования интерфейс для нужного языка программирования в целевом процессоре отсутствует
Подводя итог, образно выражаясь, скажем, что расширения — это пушка, стрелять из которой по воробьям рекомендуется только, когда не остается ничего другого, или воробьи достаточно велики.
К сожалению, не представляется возможным описать в одной главе интерфейсы расширения даже наиболее распространенных XSLT-процессоров. Вместо этого мы постараемся изложить основные принципы создания расширения, а также приведем несколько общих примеров, которые смогут послужить основой для создания частных решений.
Основным языком реализации расширений, приводимых в этой главе, будет Java. Пожалуй, Java является единственным языком, интерфейсы расширений для которого достаточно стандартизированы, чтобы можно было говорить об общих подходах. Однако, если читатель не знаком с этим языком — ничего страшного, ведь основное внимание в этой главе уделяется использованию расширений в XSLT, а не написанию их в других языках и Java-код приводится только для того, чтобы сделать примеры рабочими.
Функции расширения
Прежде чем описывать использование функций расширения, вспомним, как мы вызывали в преобразованиях обычные функции, например, функцию
concat
:
Атрибут
select
содержит XPath-выражение concat('para', 'bellum')
, которое с точки зрения синтаксиса XPath является вызовом функции и соответствует продукции FunctionCall
:[XP16] FunctionCall ::= FunctionName
'(' ( Argument ( ',' Argument ) * ) ? ')'
Аргументами функции являются выражения, а имя может быть любым корректным XML-именем (за исключением
node
, comment
, processing-instruction
и text
, которые используются для проверки типа узла):[XP17] Argument ::= Expr
[XP35] FunctionName ::= QName - NodeType
В плане синтаксиса функции расширения ничем не отличаются от стандартных функций: они отвечают тем же самым грамматическим правилам. Единственное различие состоит в том, что функции стандартных библиотек XPath и XSLT принадлежат нулевому пространству имен, в то время как пространство имен функций расширения должно обязательным образом быть ненулевым. Иными словами, вызовы функций, имеющие вид
имя(аргумент, аргумент, ...)
, будут считаться вызовами функций базовых библиотек, а вызовы вида префикс:имя(аргумент, аргумент, ...)
будут считаться вызовами функций расширения.ПримерВыражение
round(0.6)
является вызовом функции базовой библиотеки XPath, в то время как выражение
math:round(0.6)
является вызовом функции расширения.
Практически во всех процессорах пространство имен функции расширения является звеном, которое связывает ее с конкретной реализацией.
Пример
Элемент
xsl:value-of
вычисляет выражение math:round(0.6)
, которое является вызовом функции расширения. Само имя функции состоит из локальной части round и префикса math
, которому соответствует URI java:java.lang.Math
. В большинстве XSLT-процессоров вызов такого рода будет означать обращение к статической функции round
класса java.lang.Math
.Простейшим случаем использования расширений в XSLT-процессорах, написанных на Java, является обращение к стандартным функциям пакетов Java.
ПримерПредположим, что входящий документ описывает координаты множества точек, а преобразование создает SVG-документ, содержащий линии, которые их последовательно соединяют.
ПримечаниеSVG — это XML-язык для описания масштабируемой векторной графики (от англ. scalable vector graphics). SVG позволяет простым XML-синтаксисом описывать векторную графику. SVG-документы могут показываться в браузерах при помощи таких компонент, как Adobe SVG Viewer или Batik от Apache XML Project.
Листинг 10.3. Входящий документ
<точки width="200" height="200">
<точка x="-50" y="-50"/>
<точка x=" 50" y="-50"/>
<точка x=" 50" y=" 50"/>
<точка x="-50" y=" 50"/>
точки>
Листинг 10.4. Преобразование
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2000/svg">
indent="yes"
doctype-public="-//W3C//DTD SVG 1.0//EN"
doctype-system="http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"/>
Simple line-based figure
x1="{@x + 100}"
y1="{@y + 100}"
x2="{following-sibling::точка[1]/@x + 100}"
y2="{following-sibling::точка[1]/@y + 100}">
select="preceding-sibling::точка[last()]/@x + 100"/>
select="preceding-sibling::точка[last()]/@y + 100"/>
Результатом этого преобразования является следующий SVG-документ.
Листинг 10.5. Выходящий SVG-документ
PUBLIC "-//W3C//DTD SVG 1.0//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
Simple line-based figure
На рис. 10.1 приведен пример визуального представления этого документа.
Рис. 10.1. Визуальное представление полученного SVG-документа
Предположим теперь, что нам нужно не просто создать по данному множеству точек набор соединяющих их линий, но еще и произвести некоторые геометрические преобразования, например поворот на заданный в градусах угол α.
Формулы преобразования координат при повороте чрезвычайно просты:
x = x'∙cos(α) −y∙sin(α),
у = x'∙sin(α) + x'∙cos(α),
где x' и y' — старые координаты точки, x и y — новые координаты точки, а α — угол поворота. Единственная загвоздка состоит в том, что функций
sin
и cos
в базовой библиотеке XPath нет.Самым простым выходом в такой ситуации является использование расширений. Например, в случае XSLT-процессора, который может использовать Java-расширения (Saxon, Xalan, Oracle XSLT Processor и так далее) надо будет лишь только объявить пространство имен вида:
xmlns:math="java:java.lang.Math"
и использовать функции
math:sin
и math:cos
.Листинг 10.6. Преобразование, осуществляющее поворот
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2000/svg"
xmlns:math="java:java.lang.Math">
indent="yes"
doctype-public="-//W3C//DTD SVG 1.0//EN"
doctype-system="http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"/>
Simple line-based figure
x1="{$x1 * math:cos($alpha-radian) -
$y1 * math:sin($alpha-radian) + 100}"
y1="{$x1 * math:sin($alpha-radian) +
$y1 * math:cos($alpha-radian) + 100}"
x2="{$x2 * math:cos($alpha-radian) -
$y2 * math:sin($alpha-radian) + 100}"
y2="{$x2 * math:sin($alpha-radian) +
$y2 * math:cos($alpha-radian) + 100}"/>
Результатом этого преобразования будет следующий документ.
Листинг 10.7. Результирующий SVG-документ
PUBLIC "-//W3C//DTD SVG 1.0//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
xmlns="http://www.w3.org/2000/svg"
xmlns:math="java:java.lang.Math"
width="200"
height="200">
Simple line-based figure
x1="81.68060041188197" y1="31.70359014757173"
x2="168.29640985242827" y2="81.68060041188197"/>
x1="168.29640985242827" y1="81.68060041188197"
x2="118.31939958811803" y2="168.29640985242827"/>
x1="118.31939958811803" y1="168.29640985242827"
x2="31.70359014757173" y2="118.31939958811803"/>
x1="31.70359014757173" y1="118.31939958811803"
x2="81.68060041188197" y2="31.70359014757173"/>
Визуальное представление этого документа демонстрирует рис. 10.2, где представлен поворот, выполненный на 30°:
Рис. 10.2. Визуальное представление полученного SVG-документа
Анализируя полученный документ, мы можем заметить объявление пространства имен с префиксом
math
, которое было в него включено:
xmlns="http://www.w3.org/2000/svg"
xmlns:math="java:java.lang.Math"
width="200"
height="200">
...
Это тот самый случай, когда объявление пространства имен используется в самом преобразовании, но является лишним в выходящем документе. Для того чтобы избавиться от него, нужно просто включить префикс
math
в атрибут exclude-result-prefixes
элемента xsl:stylesheet
.
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2000/svg"
xmlns:math="java:java.lang.Math"
exclude-result-prefixes="math">
...
Поскольку мы все равно используем в этом преобразовании расширения, мы можем написать свой собственный класс, который будет выполнять вычисление новых координат точки, исключив таким образом из преобразования все математические операции.
Листинг 10.8. Класс, вычисляющий координаты точки после поворотаpackage de.fzi.xslt;
public class rot {
public static double X(double x, double y, double degree) {
double radian = Math.PI * degree / 180;
return x * Math.cos(radian) - y * Math.sin(radian);
}
public static double Y(double x, double y, double degree) {
double radian = Math.PI * degree / 180;
return x * Math.sin(radian) + y * Math.cos(radian);
}
}
Для того чтобы использовать методы этого класса в качестве функций расширения, немного изменим объявления в элементе
xsl:stylesheet
:
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2000/svg"
xmlns:rot="java:de.fzi.xslt.rot"
exclude-result-prefixes="rot">
Создание элемента
line
теперь может быть записано в виде:
x1="{rot:X($x1, $y1, $alpha) + 100}"
y1="{rot:Y($x1, $y1, $alpha) + 100}"
x2="{rot:X($x2, $y2, $alpha) + 100}"
y2="{rot:Y($x2, $y2, $alpha) + 100}"/>
Как мы отмечали выше, интерфейсы использования функций расширения весьма различаются между разными процессорами даже в случае такого переносимого языка, как Java. Отличия могут быть и в форме вызовов функций, и в форме объявлений пространств имен. Например, в процессоре Saxon пространство имен для класса
de.fzi.xslt.rot
может быть объявлено как:xmlns:rot="java:de.fzi.xslt.rot"
в Xalan — как:
xmlns:rot="xalan://de.fzi.xslt.rot"
в Oracle XSLT Processor — как:
xmlns:rot="http://www.oracle.com/XSL/Transform/java/de.fzi.xslt.rot"
При этом сами вызовы во всех трех случаях будут одинаковыми:
rot:X($x, $y, $angle)
для метода X или
rot:Y($x, $y, $angle)
для метода Y.
Функция function-available
При использовании функций расширения всегда есть вероятность того, что это расширение в силу каких-либо причин поддерживаться данным процессором не будет. Чаще всего это случается, во-первых, когда процессор просто физически не в состоянии вызвать эту функцию (например, процессор, написанный на C++, вряд ли будет содержать средства для выполнения Java-кода), во-вторых, когда расширение недоступно (например, процессор не в состоянии найти указанный Java-класс или динамическую библиотеку), и в-третьих, когда пространство имен объявлено неверно (например, с URI
java:de.fzi.xslt.rot
вместо xalan://de.fzi.xslt.rot
). Результатом обращения к неподдерживаемому расширению будет, естественно, ошибка.XSLT позволяет избежать подобного рода ошибок путем предварительной проверки наличия заданной функции расширения. Для этой цели служит стандартная функция
function-available
(от англ. function is available — функция доступна)boolean function-available(string)
Функция
function-available
принимает на вход строку, представляющую имя функции и возвращает true
, если эта функция может быть вызвана и false
— если нет.Строковый аргумент этой функции представляет расширенное имя функции, он должен соответствовать продукции
QName
, то есть иметь вид имя
или префикс:имя
. В первом случае function-available
проверяет, реализована ли в данном процессоре стандартная функция с таким именем, например function-available('concat')
скорее всего, возвратит true
.В случае, если аргумент
function-available
имеет вид префикс:имя
, функция function-available
проверяет доступность указанной функции расширения. Например, для того, чтобы проверить, может ли в данном контексте быть вызвана функция rot:X
, необходимо вычислить выражениеfunction-available('rot:X')
В данном случае
true
будет означать, что функция rot:X
может быть вызвана, false
— что функция в силу каких-то причин недоступна.Функция
function-available
может помочь в создании преобразований, которые используют расширения, но при этом в некоторой степени сохраняют переносимость между различными процессорами. Достаточно написать несколько вариантов вызова функции расширения для каждого из процессоров, на которых преобразование должно работать, а затем использовать вариант с доступной данному процессору функцией расширения.ПримерДля того чтобы обеспечить работоспособность расширения, реализованного классом
de.fzi.xslt.rot
в наиболее распространенных XSLT-процессорах, написанных на Java (как-то: Saxon, Xalan и Oracle XSLT Processor), прежде всего необходимо объявить соответствующие пространства имен:
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/2000/svg"
xmlns:saxon="java:de.fzi.xslt.rot"
xmlns:xalan="xalan://de.fzi.xslt.rot"
xmlns:oracle="http://www.oracle.com/XSL/Transform/java/de.fzi.xslt.rot"
exclude-result-prefixes="saxon xalan oracle">
...
Префикс
saxon
соответствует интерфейсу расширений в XSLT-процессоре Saxon, префикс xalan
— процессору Xalan и префикс oracle
— Oracle XSLT Processor.Теперь осталось только найти поддерживаемый вариант расширения и произвести соответствующий вызов.
Листинг 10.9
x1="{saxon:X($x1, $y1, $alpha) + 100}"
y1="{saxon:Y($x1, $y1, $alpha) + 100}"
x2="{saxon:X($x2, $y2, $alpha) + 100}"
y2="{saxon:Y($x2, $y2, $alpha) + 100}"/>
x1="{xalan:X($x1, $y1, $alpha) + 100}"
y1="{xalan:Y($x1, $y1, $alpha) + 100}"
x2="{xalan:X($x2, $y2, $alpha) + 100}"
y2="{xalan:Y($x2, $y2, $alpha) + 100}"/>
x1="{oracle:X($x1, $y1, $alpha) + 100}"
y1="{oracle:Y($x1, $y1, $alpha) + 100}"
x2="{oracle:X($x2, $y2, $alpha) + 100}"
y2="{oracle:Y($x2, $y2, $alpha) + 100}"/>
Necessary extension function is not available.
Supported processors are:
Saxon, Xalan, Oracle XSLT Processor.
В случае, если хотя бы одна из функций
saxon:X
, xalan:X
, oracle:X
будет доступна при обработке, она будет использована процессором для создания атрибутов элемента line
. В противном случае, процессор прервет выполнение преобразования и выведет указанное в элементе xsl:message
сообщение.Нельзя не согласиться с тем, что приведенный выше способ не отличается элегантностью. Реализовывать свой вариант для каждого существующего процессора может быть довольно трудоемкой задачей — но такова уж плата за возможности расширений.
Функция расширения nodeset
Одной из самых полезных функций расширения, которая, как правило, уже штатно реализована во многих процессорах (то есть, не требует дополнительного программирования) является функция
nodeset
. Эта функция позволяет в обход прямого запрета спецификации конвертировать результирующий фрагмент дерева во множество узлов.Предположим, что мы создаем в переменной
rtf
результирующий фрагмент дерева следующего вида:
- 1
- 2
- 3
При попытке вычислить выражение вида
$rtf/item[2]
процессор в соответствии со спецификацией должен вывести ошибку, поскольку в этом фильтрующем выражении (см. продукцию[XP20] FilterExpr
) переменная rtf
должна содержать множество узлов, а не фрагмент дерева.Текущая спецификация языка XPath совершенно явно говорит о том, что ни один тип данных не может быть преобразован во множество узлов. Функция
nodeset
действует в обход этого запрещения: она принимает на вход результирующий фрагмент дерева и возвращает множество, состоящее из корневого узла этого фрагмента.В разных процессорах эта функция имеет различный синтаксис: она может носить имя
nodeset
или node-set
, или nodeSet
, однако семантика ее во всех случаях одинакова:nodeset nodeset(result-tree-fragment)
Функция принимает на вход единственный аргумент, являющийся фрагментом дерева и возвращает множество узлов, состоящее из его корня.
ПримерПредположим, что мы обрабатываем входящий документ, содержащий трехбуквенные коды языков.
Листинг 10.10. Входящий документ
- ENG
- FRE
- GER
- GRE
- ITA
- NOR
- POR
- SPA
Фрагмент шаблона, обрабатывающий этот список, может выглядеть следующим образом:
Если в преобразовании нам понадобится доопределить входящий список кодами
RUS
и UKR
, не исправляя входящий документ, можно поступить следующим образом:□ создать в переменной фрагмент дерева, содержащий элементы
item
входящего документа плюс элементы item
, доопределяющие этот список;□ преобразовать дерево в список узлов;
□ обрабатывать список узлов точно так же, как мы бы обрабатывали сам входящий документ.
Преобразование, реализующее эти три шага, приведено ниже.
Листинг 10.11. Преобразование
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xalan"
exclude-result-prefixes="xalan">
- RUS
- UKR
Результат этого преобразования приведен на следующем листинге.
Листинг 10.12. Выходящий документ
Вне всякого сомнения, функция
nodeset
является одним из наиболее востребованных в XSLT расширений, ведь возможность не только создавать, но и манипулировать уже созданными древовидными структурами является чрезвычайно полезной.В качестве одного из примеров применения функции
nodeset
можно привести реализацию с ее помощью многошаговых преобразований.В качестве примера рассмотрим схему трансформации, изображенную на рис. 10.3, в которой документ А сначала нужно обработать преобразованием 1, затем полученный результат (документ В) обработать преобразованием 2. Конечным результатом цепочки преобразований в данном случае является документ С.
Рис. 10.3. Двухшаговое преобразование
При выполнении преобразования процессор применяет шаблоны ко множеству узлов входящего документа и выстраивает результирующее дерево. Таким образом, для того, чтобы повторно применить шаблоны к уже обработанному документу (читай: к полученному дереву), нужно просто иметь возможность преобразовывать дерево во множество узлов.
ПримерПредставим себе два простых преобразования,
first.xsl
и second.xsl
, первое из которых заменяет во входящем документе элементы а
на элементы b
, а второе — элементы b
на элементы с
.Листинг 10.13. Преобразование first.xsl
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
Листинг 10.14. Преобразование second.xsl
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
Для того чтобы последовательно применить два этих преобразования к некоторому входящему документу
a.xml
, мы можем, например, дважды вызвать процессор:java org.apache.xalan.xslt.Process -IN a.xml -XSL first.xsl -OUT b.xml
java org.apache.xalan.xslt.Process -IN b.xml -XSL second.xsl -OUT c.xml
В результате этих вызовов XSLT-процессор Xalan сначала применит преобразование
first.xsl
к документу a.xml
и сохранит результат в файле b.xml
, а затем обработает полученный документ b.xml
при помощи преобразования second.xml
и сохранит результат в файле c.xml
.В качестве альтернативы, например, для тех случаев, когда пакетная обработка невозможна, мы можем создать преобразование, последовательно применяющее шаблоны преобразований
first.xsl
и second.xsl
к входящему документу. Для этого:□ назначим шаблонам преобразования
first.xsl
режим first
, а шаблонам преобразования second.xsl
— режим second
;□ в основном шаблоне применим шаблоны режима
first
к узлам входящего документа, сохранив результат в переменной b
;□ приведем результирующее дерево, содержащееся в переменной
b
ко множеству узлов;□ обработаем полученное множество узлов шаблонами режима
second
.Следующий листинг демонстрирует предложенный подход.
Листинг 10.5. Преобразование first-then-second.xsl
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xalan="http://xml.apache.org/xalan"
exclude-result-prefixes="xalan">
a:
b:
c:
Ход этого преобразования лучше всего прокомментирует полученный результат.
Листинг 10.16. Входящий документ<а>
1
2
а>
Листинг 10.17. Выходящий документ
<а>
1
2
а>
1
2
<с>
1
2
с>
Элементы расширения