среда, 10 июля 2013 г.

XSLT 2.0 -> 1.0

Известна проблема, связанная с реализацией XSLT 2.0 (и, соответственно, XPath 2.0) - как признаёт Дуг Тидуэлл (автор бестселлера по этой теме), фактически полноценных реализации всего две:
  • Altova
  • Saxon
- и обе платные. Все остальные - частичные. Что-что? Кто сказал, что Saxon - это типа OpenSource и, значит, по определению бесплатный? Платной является именно полноценная версия, в которой можно использовать Schema-типы данных. Бесплатные же реализации для XSLT есть только для версии 1.0 . При чём последних - как собак нерезаных! Они даже включены в наши с Вами браузеры. Действительно, можете это проверить - зайдите на страницу XML, для которой при помощи декларации
<?xml-stylesheet type="text/xsl" href="trans.xslt"?>
прописана таблица преобразования, совместимая с XSLT 1.0 - и наслаждайтесь результатом! (Тут, правда, следует учесть, что локальный файл не все браузеры корректно откроют - лучше разместить его на каком-то из ваших сайтов, либо поднять сервер локально и загрузить с него - применяет XSLT-стили к локальным файлам разве что FireFox последних версий...)

Так вот, в ситуации, когда XSLT 1.0 процессоров - хоть попой ешь, а XSLT 2.0 - в высшей степени скудно, Вашему покорному слуге недавно подумалось - а почему бы не использовать здесь примерно тот же паттерн, что я уже довольно давно и активно применяю по отношению к стеку технологий JavaScript/CSS/HTML, решая проблему кроссбраузерности без потери легковесности решений?

Тем более, что технология к этому располагает. XSLT-документы, фактически, представляют собой XML-документы, а потому к ним, в свою очередь, так же применим любой инструментарий работы с XML. А значит, можно написать XSLT-документ, который будет применяться к другому XSLT-документу, что бы преобразовать его. Зачем же может понадобиться преобразование одного XSLT-документа в другой? Для того, что бы в первом без особых заморочек использовать элементы XSLT 2.0 и XPath 2.0 запросы, а в результате преобразования получать пусть и более громоздкий, но корректный и работающий XSLT 1.0 документ.

Давайте прикинем, что нам для этого нужно?

Во-первых, нужно прописать в исходном XSLT 2.0 документе декларацию, ссылающуюся на документ преобразований уже для него. Т.е. начало нашей XSLT 2.0 таблички будет примерно таким:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="xslt2to1.xslt"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
В принципе, это, конечно, необязательно, но лучше это сделать, что бы не забыть, при помощи какого именно файла это преобразование будет осуществляться. Опять же - Altova XMLSpy последней, 13-й версии это дело игнорирует, не разрешая по кнопке "F10" трансформировать документ, который считает именно XSLT-документом, в отличие от того, как она позволяет поступать по отношению к обычным XML-документам, так что приходится осуществлять это преобразование, перейдя на вкладку преобразующего документа и там указывать исходный документ.

Во-вторых, нужно, собственно, создать преобразующий документ. Понятное дело, что этот документ крайне желательно так же иметь в виде XSLT 1.0, иначе полностью уйти от XSLT 2.0 движков не получится. Для начала давайте сделаем таблицу, которая будет полностью копировать исходный документ, а потом будем постепенно вносить в неё template`ы, что бы получить на выходе то, что надо:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Если применить эту таблицу к любому XML-документу - получим на выходе его полную копию! Хорошо, но нам же не нужна полная копия, так? Нам нужно преобразование. Для начала, нам нужно убрать, собственно, инструкцию процессору
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
, говорящую о том, что этот документ перед тем, как использоваться, должен быть преобразован согласно указанной XSLT-таблице стилей - выходной же документ, являющийся результатом преобразования, в свою очередь уже не нуждается в преобразовании и по-этому эту инструкцию нужно убрать.

Что бы сделать это, достаточно воспользоваться приёмом создания template`а, который имеет более специфическое XPath-выражение в атрибуте matсh, чем "node()" (поскольку селектор node() является максимально общим, практически любой XPath-запрос к тому же элементу будет более специфичным, так что сделать это не сложно ;) ). Тогда, согласно правилам XSLT, процессор для данного узла, либо атрибута выберет именно новый template, а не старый, именно из-за большей специфичности его XPath-запроса. Каким же должен быть этот template? Пустым, поскольку именно пустой template позволит убрать из конечного документа найденный узел. Т.е. убирается ненужная конструкция добавлением в таблицу преобразования следующего элемента:

<xsl:template match="processing-instruction()[local-name()='xml-stylesheet']"/>
Хорошо. Следующий шаг - изменение версии XSLT в документе, ведь мы преобразуем XSLT 1.0 в 2.0 . Это может быть сделано примерно так:
<xsl:template match="/xsl:*[local-name()='stylesheet' or local-name()='transform']/@version[.='2.0']">
    <xsl:attribute name="version">1.0</xsl:attribute>
</xsl:template>
Ну а дальше - непаханное поле, вполне достойное отдельного проекта на GitHub`е, который я собираюсь создать и потихоньку разрабатывать (когда найду на это время, конечно). В качестве примера можно рассмотреть реализацию довольно удобного атрибута "separator" в элементе value-of, который появляется в XSLT 2.0 и отсутствует в XSLT 1.0 . Этот атрибут задаёт символы, которые необходимо вставить между результатами выборки, проставленными в XPath-выражении атрибута "select". Например, выражение
<xsl:value-of select="'1'|'2'|'3'" separator=", "/>
выведет строчку:

"1, 2, 3"

Для того, что бы добиться подобного поведения в XSLT 1.0, нужно написать примерно такой вот громоздкий код:

<xsl:for-each select="'1'|'2'|'3'">
    <xsl:value-of select="."/>
    <xsl:if test="not(position()=last())">
        <xsl:text>, </xsl:text>
    </xsl:if>
</xsl:for-each>
Соответственно, преобразование может выглядеть примерно так:
<xsl:template match="xsl:value-of[@separator]">
    <xsl:element name="xsl:for-each">
        <xsl:attribute name="select">
            <xsl:value-of select="@select"/>
        </xsl:attribute>
        <xsl:element name="xsl:value-of">
            <xsl:attribute name="select">.</xsl:attribute>
        </xsl:element>
        <xsl:element name="xsl:if">
            <xsl:attribute name="test">not(position()=last())</xsl:attribute>
            <xsl:element name="xsl:text">
                <xsl:value-of select="@separator"/>
            </xsl:element>
        </xsl:element>
    </xsl:element>
</xsl:template>
Вот примерно как-то так...

P.S. Остался наиболее хитрый вопрос - о том, что делать с спецификой именно XPath 2.0 - запросов. Дело в том, что напрашивающиеся тут RegExp`ы в XSLT 1.0 (читай - XPath 1.0) не поддерживаются, что создаёт, мягко говоря, некоторые трудности. Что тут можно сделать? Либо отказаться от идеи использования в качестве процессора преобразования XSLT 1.0 процессор и по крайней мере для преобразования таблиц юзать процессор XSLT 2.0, либо использовать механизм расширений. Например, для Xalan`а пример работы можно подсмотреть тут.