Алексей Валиков - Технология XSLT
<items>
<item source="a" name="A"/>
<item source="b" name="B"/>
<item source="a" name="C"/>
<item source="c" name="D"/>
<item source="b" name="E"/>
<item source="b" name="F"/>
<item source="c" name="G"/>
<item source="a" name="H"/>
</items>
нужно было получить документ вида.
Листинг 11.2. Требуемый результат<sources>
<source name="а">
<item source="a" name="A"/>
<item source="a" name="C"/>
<item source="a" name="H"/>
</source>
<source name="b">
<item source="b" name="B"/>
<item source="b" name="E"/>
<item source="b" name="F"/>
</source>
<source name="c">
<item source="c" name="D"/>
<item source="c" name="G"/>
</source>
</sources>
Легко понять, почему такая задача называется задачей группировки: требуется сгруппировать элементы item по значениям одного из своих атрибутов.
Напомним вкратце решение, которое было тогда предложено. При обработке первого объекта каждой группы мы создавали элемент source, в который включали все элементы item, принадлежащие этой группе. Для определения первого элемента мы использовали выражение
preceding-sibling::item[@source=current()/@source]
которое возвращало непустое множество только тогда, когда элемент не был первым в группе.
В этом разделе мы приведем гораздо более эффективное и остроумное решение задачи группировки, впервые предложенное Стивом Мюнхом (Steve Muench), техническим гуру из Oracle Corporation. Оно основывается на двух посылках.
□ Мы можем выбрать множество узлов по их свойствам при помощи ключей.
□ Мы можем установить, является ли узел первым узлом множества в порядке просмотра документа при помощи функции generate-id.
С первым пунктом все, пожалуй, ясно — выбор множества узлов по определенному критерию — это самое прямое предназначение ключей. Второй же пункт оставляет легкое недоумение: функция generate-id вроде бы предназначена только для генерации уникальных значений.
Для того чтобы развеять все сомнения, напомним, как ведет себя эта функция, если аргументом является множество узлов. В этом случае generate-id возвращает уникальный идентификатор первого в порядке просмотра документа узла переданного ей множества. Значит для того, чтобы проверить, является ли некий узел первым узлом группы, достаточно сравнить его уникальный идентификатор со значением выражения generate-id($group), где $group — множество узлов этой группы.
С учетом приведенных выше возможностей группирующее преобразование переписывается удивительно элегантным образом.
Листинг 11.3. Группирующее преобразование<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="src" match="item" use="@source"/>
<xsl:template match="items">
<sources>
<xsl:apply-templates
select="item[generate-id(.)=generate-id(key('src', @source))]"/>
</sources>
</xsl:template>
<xsl:template match="item">
<source name="{@source}">
<xsl:copy-of select="key('src', @source)"/>
</source>
</xsl:template>
</xsl:stylesheet>
Результат выполнения этого преобразования уже был приведен в листинге 11.2.
Перечисление узлов
Функции name и local-name предоставляют возможности для работы с документом, имена элементов и атрибутов в котором заранее неизвестны. Например, если шаблон определен как:
<xsl:template match="*[starts-with(local-name(), 'чеб')]">
...
</xsl:template>
то обрабатываться им будут все элементы, локальные части имен которых начинаются на "чеб" (например, "чебуреки", "Чебоксары", "чебурашка").
Следующее преобразование демонстрирует, как при помощи функции local-name и ключей сосчитать количество элементов и атрибутов документа с различными именами.
Листинг 11.4. Входящий документ<foo bar="1">
<bar foo="2"/>
<bar bar="3"/>
<foo foo="4">
<bar bar="5"/>
<bar foo="6"/>
</foo>
</foo>
Листинг 11.5. Преобразование<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- Выводим информацию в текстовом виде -->
<xsl:output method="text"/>
<!--
| Создаем ключ, отображающий узлы атрибутов и элементов
| в их локальные части имен.
+-->
<xsl:key name="node" match="*" use="local-name()"/>
<xsl:key name="node" match="@*" use="local-name()"/>
<xsl:template match="*|@*">
<xsl:variable name="name" select="local-name()"/>
<!--
| Если узел является первым узлом группы (первым встретившимся
| узлом документа с данным именем), выводим информацию о
| количестве узлов в группе (количество узлов с таким же именем).
+-->
<xsl:if test="generate-id(.) = generate-id(key('node', $name))">
<xsl:text>Node '</xsl:text>
<xsl:value-of select="local-name()"/>
<xsl:text>' found </xsl:text>
<xsl:value-of select="count(key('node', $name))"/>
<xsl:text> times.
</xsl:text>
</xsl:if>
<!-- Рекурсивно обрабатываем дочерний элемент и атрибуты -->
<xsl:apply-templates select="*|@*"/>
</xsl:template>
</xsl:stylesheet>
Листинг 11.6. Выходящий документNode 'foo' found 5 times.
Node 'bar' found 7 times.
Именованный шаблон как функция
Сложно переоценить возможности механизмов расширений языка XSLT. Они позволяют сочетать простоту и гибкость обработки XML-документов при помощи элементов XSLT и выражений XPath. Практически любая функция, которая отсутствует в XSLT, может быть написана на подходящем языке программирования и подключена к процессору.
Но как уже отмечалось ранее, функции расширения ограничивают переносимость преобразований. Во-первых, функции расширения одного процессора совсем необязательно будут присутствовать в другом процессоре — скорее наоборот. Во-вторых, не приходится надеяться, что пользовательские модули, написанные на одном языке или с использованием одного интерфейса, смогут использоваться любым процессором. Поэтому часто перед разработчиком стоит проблема решить определенную задачу, используя только стандартные функции и элементы XSLT.
В этом разделе мы рассмотрим возможность использования именованных шаблонов в качестве функций, которые принимают на вход несколько параметров и возвращают некоторое вычисленное значение.
Использование именованных шаблонов как функций обуславливается следующими тезисами.
□ Именованный шаблон можно вызывать вне зависимости от того, какая часть документа обрабатывается в данный момент.
□ Именованному шаблону можно передавать параметры.
□ Результат выполнения именованного шаблона можно присваивать переменной.
Вызов именованного шаблона выполняется элементом xsl:call-template, в атрибуте name которого указывается имя вызываемого шаблона. Такой вызов не зависит от того, какая часть документа обрабатывается в данный момент и может производиться по необходимости.
Параметры именованному шаблону передаются точно так же, как и обычному — при помощи элементов xsl:with-param, которые могут быть включены в вызывающий элемент xsl:call-template. Примером вызова именованного шаблона с параметрами может быть конструкция вида
<xsl:call-template name="foo">
<xsl:with-param name="x" select="1"/>
<xsl:with-param name="y" select="2"/>
</xsl:call-template>
которая вызывает шаблон с именем foo и передает ему параметр x со значением, равным 1 и параметр y со значением, равным 2.
Вызов именованного шаблона может также производиться при инициализации переменной — внутри элемента xsl:variable. В этом случае с переменной связывается результирующий фрагмент дерева, возвращаемый именованным шаблоном.
ПримерВ качестве примера приведем простой шаблон, который вычисляет квадрат переданного ему параметра x:
<xsl:template name="sqr">
<xsl:param name="x"/>
<xsl:value-of select="$x * $x"/>
</xsl:template>
Для того чтобы присвоить переменной у квадрат числа 6 мы можем записать следующее:
<xsl:variable name="y">
<xsl:call-template name="sqr">