Главная » XSLT » Реализация элементов расширения на языке Java

0

Задача

Требуется расширить функциональность XSLT за счет добавления нестан­дартных элементов.

Решение

Выше мы показали, как можно воспользоваться расширениями, уже реализован­ными в процессоре XSLT. А в этом разделе разработаем с нуля собственные элементы расширения. В отличие от функций расширения, для этого потребуется более близкое знакомство с деталями реализации конкретного процессора. Поскольку внутренняя архитектура у всех процессоров разная, большая часть кода окажется непереносимой.

Мы начнем с простого расширения, которое можно назвать, скорее, синтакси­ческим украшением, а не дополнительной функциональностью. При написании XSLT-кода часто возникает необходимость переключить контекст на другой узел. Идиоматический способ добиться этой цели дает команда xsl:for-each. Выг­лядит это странно, поскольку нам нужно не организовать цикл, а просто сделать контекстным единственный узел, определенный атрибутом select:

<xsl:for-each select="document(‘new.xml’)"> <!– Обработать новый документ –>

</xsl:for-each>

Мы реализуем элемент расширения xslx:set-context, который будет вести себя точно так же, как xsl:for-each, но только для первого узла из набора, опреде­ленного с помощью select (впрочем, обычно в наборе и бывает всего один узел).

Saxon требует, чтобы все элементы расширения, ассоциированные с конкрет­ным пространством имен, реализовывали интерфейс com.icl.saxon.style. ExtensionElementFactory. Фабрика отвечает за создание элементов расши­рения по локальному имени.

package com.ora.xsltckbk;

import com.icl.saxon.style.ExtensionElementFactory; import org.xml.sax.SAXException;

public class CkBkElementFactory implements ExtensionElementFactory {

public Class getExtensionClass(String localname) {

if (localname.equals("set-context")) return CkBkSetContext.class; if (localname.equals("templtext")) return CkBkTemplText.class; return null;

}

}

При использовании расширения в таблице стилей нужно указывать простран­ство имен, в котором вслед за завершающим символом / идет полностью ква­лифицированное имя фабрики. Кроме того, префикс этого пространства имен должен быть указан в атрибуте extension-element-prefixes элемента xsl:stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xslx="http://com.ora.xsltckbk.CkBkElementFactory" extension-element-prefixes="xslx">

<xsl:template match="/">

<xslx:set-context select="foo/bar">

<xsl:value-of select="."/> </xslx:set-context> </xsl:template>

</xsl:stylesheet>

Класс, реализующий элемент set-context, наследует классу com.icl.saxon.style.StyleElement и обязан предоставить методы prepareAttributes() и process(). Обычно реализуются также и другие ме­тоды, перечисленные в таблице 14.3.

Таблица 14.3. Важные методы класса styleElement из процессора Saxon

Реализовать элемент xslx:set-context оказалось легко, мы просто поза­имствовали код из входящего в Saxon класса XSLForEach, изменив его так, чтобы действие выполнялось только один раз:

public class CkBkSetContext extends com.icl.saxon.style.StyleElement {

Expression select = null;

public boolean isInstruction( ) { return true;

}

public boolean mayContainTemplateBody( ) { return true;

}

Затем мы проверяем наличие атрибута @select. Если он имеется, вызываем метод makeExpression, который из его строкового значения создает выражение XPath:

public void prepareAttributes( )

throws TransformerConfigurationException {

StandardNames sn = getStandardNames( ); AttributeCollection atts = getAttributeList( );

String selectAtt = null;

Таблица 14.3. Важные методы класса styleElement из процессора Saxon (окончание)

for (int a=0; a<atts.getLength( ); a++) { int nc = atts.getNameCode(a); int f = nc & 0xfffff;

if (f == sn.SELECT) {

selectAtt = atts.getValue(a); } else {

checkUnknownAttribute(nc);

}

}

if (selectAtt= =null) {

reportAbsence("select"); } else {

select = makeExpression(selectAtt);

}

}

public void validate( ) throws TransformerConfigurationException { checkWithinTemplate( );

}

Далее следует практически такой же код, как в элементе for-each из Saxon, только вместо того чтобы вызывать метод selection.hasMoreElements в цикле, мы проверяем наличие дочерних элементов только один раз, выбираем элемент, устанавливаем контекст и текущий узел, обрабатываем дочерние элементы и воз­вращаем результат в контекст:

public void process(Context context) throws TransformerException {

NodeEnumeration selection = select.enumerate(context, false); if (!(selection instanceof LastPositionFinder)) { selection = new LookaheadEnumerator(selection);

}

Context c = context.newContext( );

c.setLastPositionFinder((LastPositionFinder)selection); int position = 1;

if (selection.hasMoreElements( )) {

NodeInfo node = selection.nextElement( );

c.setPosition(position++);

c.setCurrentNode(node);

c.setContextNode(node);

processChildren(c);

context.setReturnValue(c.getReturnValue( ));

Следующий пример не так прост, поскольку он расширяет возможности XSLT, а не просто слегка модифицирует уже существующую функциональность.

Вы, конечно, понимаете, что раз уж я посвятил целую главу генерации кода, значит, эта тема меня интересует. Но, если XSLT почти оптимален в плане мани­пуляции XML-документами, то его средства вывода оставляют желать лучшего из-за многословности XML. Рассмотрим задачу генерации простого С++-кода на стандартном XSLT:

<classes> <class>

<name>MyClass1</name> </class>

<class>

<name>MyClass2</name> </class>

<class>

<name>MyClass3</name> <bases>

<base>MyClass1</base> <base>MyClass2</base> </bases> </class>

</classes>

Таблица стилей для преобразования этого документа в код на C++ могла бы выглядеть так:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <xsl:template match="class">

class <xsl:value-of select="name"/> <xsl:apply-templates select="bases"/>

{

public:

<xsl:value-of select="name"/>( ) ; ~<xsl:value-of select="name"/>( ) ;

<xsl:value-of select="name"/>(const <xsl:value-of select="name"/> &amp; other) ;

<xsl:value-of select="name"/>&amp; operator =(const <xsl:value-of select="name"/>&amp; other) ; } ;

</xsl:template>

<xsl:template match="bases"> <xsl:text>: public </xsl:text> <xsl:for-each select="base"> <xsl:value-of select="."/> <xsl:if test="position( ) != last( )">

<xsl:text>, public </xsl:text> </xsl:if> </xsl:for-each> </xsl:template>

<xsl:template match="text( )"/> </xsl:stylesheet>

Такую таблицу утомительно писать и трудно читать, так как код на C++ теря­ется в дебрях разметки.

Расширение xslx:templtext решает эту проблему путем создания альтерна­тивной реализации элемента xsl:text, который может содержать специальные символы экранирования, указывающие на необходимость особой обработки. Экра­нирование обозначается обратной косой чертой ( \ ) и может применяться в двух фор­мах. Очевидная альтернатива – использовать фигурные скобки { и }, имитирующие синтаксис вычисляемых выражений и XQuery. Но, поскольку эти символы часто употребляются в генераторах кода для других целей, я остановился на \.

Имея такой механизм, генератор кода можно записать следующим образом:

<xsl:stylesheet version="1.0"

xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns:xslx="http://com.ora.xsltckbk.CkBkElementFactory"

extension-element-prefixes="xslx">

<xsl:template match="class"> <xslx:templtext>

class \name\ <xsl:apply-templates select="bases"/>

{

public:

\name\( ) ; ~\name\( ) ;

\name\(const \name\&amp; other) ;

\name\&amp; operator =(const \name\&amp; other) ;

} ;

</xslx:templtext> </xsl:template>

<xsl:template match="bases">

<xslx:templtext>: public \base%’, public ‘\</xslx:templtext> </xsl:template>

<xsl:template match="text( )"/> </xsl:stylesheet>

Такой код значительно проще писать и читать. Конструкция применима в любом контексте, где нужно генерировать большой объем стандартного кода. Рев­нителям чистоты XSLT это расширение может не понравиться, потому что оно вводит в XSLT чуждый ему синтаксис, не имеющий отношения к манипуляциям с XML. Возражение справедливо, но, если взглянуть с практической точки зрения, то многие разработчики отказались бы от применения XSLT (в пользу Perl) для генерации стандартного кода только потому, что ему недостает краткого и не ме­шающего восприятию текста синтаксиса. Ладно, хватит ходить вокруг да около, пора уже привести код:

package com.ora.xsltckbk; import java.util.Vector ; import java.util.Enumeration ;

import com.icl.saxon.tree.AttributeCollection;

import com.icl.saxon.*;

import com.icl.saxon.expr.*;

import javax.xml.transform.*;

import com.icl.saxon.output.*;

import com.icl.saxon.trace.TraceListener;

import com.icl.saxon.om.Nodelnfo;

import com.icl.saxon.om.NodeEnumeration;

import com.icl.saxon.style.StyleElement;

import com.icl.saxon.style.StandardNames;

import com.icl.saxon.tree.AttributeCollection; import com.icl.saxon.tree.Nodelmpl;

Сначала объявим константы, используемые в простом конечном автомате, ко­торый разбирает текст с символами экранирования:

public class CkBkTemplText extends com.icl.saxon.style.StyleElement {

private static final  int SCANNING_STATE  = 0 ;

private static final  int FOUND1_STATE    = 1 ;

private static final  int EXPR_STATE      = 2 ;

private static final  int FOUND2_STATE    = 3 ;

private static final  int DELIMIT_STATE   = 4 ;

Затем определим четыре закрытых класса, реализующих миниязык, на котором написан текст внутри элемента xslx:templtext. Базовый класс CkBkTemplParam запоминает текст, предшествующий символу экранирования:

private class CkBkTemplParam {

public CkBkTemplParam(String prefix)

{

m_prefix = prefix ;

}

public void process(Context context) throws TransformerException {

if (!m_prefix.equals(""))

{

Outputter out = context.getOutputter( ); out.setEscaping(false); out.writeContent(m_prefix); out.setEscaping(true);

}

}

protected String m_prefix ;

}

Класс CkBkValueTemplParam наследует CkBkTemplParam и обрабатывает простые экранированные выражения вида \expr\. Чтобы упростить реализа­цию, предположим, что внутри элемента xslx:templtext экранирование вы­ходной информации отключено:

private class CkBkValueTemplParam extends CkBkTemplParam {

public CkBkValueTemplParam(String prefix, Expression value) {

super(prefix) ; m_value = value ;

}

public void process(Context context) throws TransformerException {

super.process(context) ;

Outputter out = context.getOutputter( ); out.setEscaping(false);

if (m_value != null)

{

m_value.outputStringValue(out, context);

}

out.setEscaping(true);

}

private Expression m_value ;

}

Класс CkBkTemplParam обрабатывает выражения вида \expr%delimit\, его код в основных чертах заимствован из класса XslForEach в Saxon:

private class CkBkListTemplParam extends CkBkTemplParam {

public CkBkListTemplParam(String prefix, Expression list, Expression delimit)

{

super(prefix) ; m_list = list ; m_delimit = delimit ;

}

public void process(Context context) throws TransformerException {

super.process(context) ;

if (m_list != null)

{

NodeEnumeration m_listEnum = m_list.enumerate(context, false);

Outputter out = context.getOutputter( ); out.setEscaping(false);

while(m_listEnum.hasMoreElements( ))

{

NodeInfo node = m_listEnum.nextElement( );

if (node != null) {

node.copyStringValue(out);

}

if (m_listEnum.hasMoreElements( ) && m_delimit != null)

{

m_delimit.outputStringValue(out, context);

}

}

out.setEscaping(true);

}

}

private Expression m_list = null; private Expression m_delimit = null ;

}

Последний закрытый класс CkBkStyleTemplParam используется для хра­нения элементов, вложенных в xslx:templtext, например xsl:apply- templates:

private class CkBkStyleTemplParam extends CkBkTemplParam {

public CkBkStyleTemplParam(StyleElement snode) {

m_snode = snode ;

}

public void process(Context context) throws TransformerException {

if (m_snode.validationError != null)

{

fallbackProcessing(m_snode, context);

}

else {

try

{

context.setStaticContext(m_snode.staticContext); m_snode.process(context);

}

catch (TransformerException err) {

throw snode.styleError(err);

}

}

}

}

Следующие три метода стандартны. Если вы позволяете использовать атри­бут disable-output-escaping для управления экранированием выходного текста, то его значение следует запомнить в методе prepareAttributes(). Об­разец исходного кода можно найти в файле XslText.java:

public boolean isInstruction( )

{

return true;

}

public boolean mayContainTemplateBody( ) {

return true;

}

public void prepareAttributes( ) throws TransformerConfigurationException {

StandardNames sn = getStandardNames( ); AttributeCollection atts = getAttributeList( );

for (int a=0; a<atts.getLength( ); a++)

{

int nc = atts.getNameCode(a); checkUnknownAttribute(nc);

}

}

Этап контроля – подходящее место для анализа содержимого элемента xslx:templtext на предмет наличия символов экранирования. Каждый тексто­вый узел передается анализатору. Стилистические дочерние элементы преобра­зуются в объекты типа CkBkStyleTemplParam. Член m_TemplParms – это век­тор, в котором хранятся результаты разбора:

public void validate( ) throws TransformerConfigurationException {

checkWithinTemplate( ); m_TemplParms = new Vector( ) ;

Nodelmpl node = (NodeImpl)getFirstChild( ); String value ;

while (node!=null)

{

if (node.getNodeType( ) == NodeInfo.TEXT)

{

parseTemplText(node.getStringValue( )) ;

}

else

if (node instanceof StyleElement)

{

StyleElement snode = (StyleElement) node;

m_TemplParms.addElement(new CkBkStyleTemplParam(snode)) ;

}

node = (NodeImpl)node.getNextSibling( );

}

}

Метод process обходит вектор m_TemplParms и для каждого элемента вы­зывает его метод process:

public void process(Context context) throws TransformerException {

Enumeration iter = m_TemplParms.elements( ) ;

while (iter.hasMoreElements( ))

{

CkBkTemplParam param = (CkBkTemplParam) iter.nextElement( ) ; param.process(context) ;

}

}

Следующие закрытые методы реализуют простой лексический анализатор на базе конечного автомата, который было бы проще написать при наличии аппарата регулярных выражений (который включен в JDK, начиная с версии 1.4.1). Два подряд идущих символа \\ интерпретируются как один литеральный символ \. Аналогично последовательность %% преобразуется в один символ %:

private void parseTemplText(String value)

{

// Этот конечный автомат разбирает текст, распознавая в нем параметры int ii = 0 ;

int len = value.length( ) ;

int state = SCANNING_STATE ; StringBuffer temp = new StringBuffer("") ; StringBuffer expr = new StringBuffer("") ;

while(ii < len)

{

char c = value.charAt(ii++) ; switch (state)

{

case SCANNING_STATE:

{

if (c == ‘\\’)

{

state = FOUND1_STATE ;

}

else {

temp.append(c) ;

}

}

break ;

case FOUND1_STATE:

{

if (c == ‘\\’)

{

temp.append(c) ;

state = SCANNING_STATE ;

}

else {

expr.append(c) ; state = EXPR_STATE ;

}

}

break ;

case EXPR_STATE:

{

if (c == ‘\\’)

{

state = FOUND2_STATE ;

}

else {

expr.append(c) ;

}

}

break ;

case FOUND2_STATE:

{

if (c = = ‘\\’)

{

state = EXPR_STATE ; expr.append(c) ;

}

else {

processParam(temp, expr) ; state = SCANNING_STATE ; temp = new StringBuffer("") ;

temp.append(c) ; expr = new StringBuffer("") ;

}

}

break ;

}

}

if (state == FOUND1_STATE || state == EXPR_STATE)

{

compileError("xslx:templtext dangling \\");

}

else

if (state == FOUND2_STATE) {

processParam(temp, expr) ;

}

else {

processParam(temp, new StringBuffer("")) ;

}

}

private void processParam(StringBuffer prefix, StringBuffer expr) {

if (expr.length( ) == 0)

{

m_TemplParms.addElement(new CkBkTemplParam(new String(prefix))) ;

}

else {

processParamExpr(prefix, expr) ;

}

}

private void processParamExpr(StringBuffer prefix, StringBuffer expr) {

int ii = 0 ;

int len = expr.length( ) ;

int state = SCANNING_STATE ;

StringBuffer list = new StringBuffer("") ;

StringBuffer delimit = new StringBuffer("") ;

while(ii < len)

{

char c = expr.charAt(ii++) ;

switch (state) {

case SCANNING_STATE:

{

if (c == ‘%’)

{

state = FOUND1_STATE ;

}

else {

list.append(c) ;

}

}

break ;

case FOUND1_STATE:

{

if (c == ‘%’)

{

list.append(c) ;

state = SCANNING_STATE ;

}

else {

delimit.append(c) ; state = DELIMIT_STATE ;

}

}

break ;

case DELIMIT_STATE:

{

if (c == ‘%’)

{

state = FOUND2_STATE ;

else {

delimit.append(c) ;

}

}

break ;

}

}

try {

if (state == FOUND1_STATE)

{

compileError("xslx:templtext trailing %");

}

else

if (state == FOUND2_STATE) {

compileError("xslx:templtext extra %");

}

else

if (state == SCANNING_STATE)

{

String prefixStr = new String(prefix) ;

Expression value = makeExpression(new String(list)) ;

m_TemplParms.addElement(

new CkBkValueTemplParam(prefixStr, value)) ;

}

else {

String prefixStr = new String(prefix) ;

Expression listExpr = makeExpression(new String(list)) ; Expression delimitExpr = makeExpression(new String(delimit)) ; m_TemplParms.addElement(

new CkBkListTemplParam(prefixStr, listExpr, delimitExpr)) ;

}

}

catch(Exception e) {

}

}

// Вектор CbkTemplParms, полученный в результате разбора текста private Vector m_TemplParms = null;

В реализацию элемента xslx:templtext можно внести несколько полезных дополнений. Например, распространить экранирование списка на несколько списков (/expr1%delim1%expr2%delim2/). Это примерно соответствует тако­му коду на XSLT:

<xsl:for-each select="expr1">

<xsl:variable name="pos" select="position( )"/> <xsl:value-of select="."/> <xsl:if test="$pos != last( )">

<xsl:value-of select="delim1"/> </xsl:if>

<xsl:value-of select="expr2[$pos]"/> <xsl:if test="$pos != last( )">

<xsl:value-of select="delim2"/> </xsl:if>

</xsl:for-each>

Эта возможность была бы полезна для подстановки в текст пары последователь­ных списков. Например, рассмотрим параметры написанной на C++ функции. Каж­дый из них представляет собой пару «имя-тип». XSLT-код дает лишь грубое прибли­жение к этой семантике, поскольку предполагается, что наборы узлов, заданные с помощью expr1 и expr2, содержат одинаковое количество элементов. Я полагаю, что в правильная реализация должна была бы подставлять в списки элементы, пока хотя бы в одном наборе остаются узлы, подавляя вывод разделителей для тех спис­ков, в которых узлов уже не осталось. Еще лучше, если бы выбором поведения можно было управлять с помощью атрибутов элемента xslx:templtext.

Обсуждение

Недостаток места не позволяет мне привести полную реализацию этих эле­ментов расширения для процессора Xalan. Но, памятуя о написанном во введе­нии, должно быть понятно, как это сделать.

См. также

Тем, кто интересуется расширением Saxon, было бы полезно прочитать ста­тью Майкла Кэя о внутренней архитектуре Saxon (http://www-106.ibm.com/ developerworks/library/x-xslt2).

Мангано Сэл  XSLT. Сборник рецептов. – М.: ДМК Пресс, СПБ.: БХВ-Петербург, 2008. – 864 с.: ил.

По теме:

  • Комментарии