Содержание


Создание документов в формате PDF при помощи XStream и XSL-FO

Использование технологий XStream и XSL-FO для динамической генерации документов

Во многих бизнес-приложениях встречается задача создания документов PDF на основе данных, содержащихся в Java-объектах модели. Такие документы лучше всего рассматривать в качестве альтернативного представления бизнес-данных. Подобные представления должны быть легко модифицируемыми и слабо связанными с уровнем модели. В данной статье демонстрируется решение этой проблемы при помощи таких технологий, как XML, XStream и форматирующие объекты XSL (XSL-FO). Вначале мы кратко рассмотрим каждую из этих технологий, а затем интегрируем их в единое демонстрационное приложение.

Итак, мы начнем с введения в используемые технологии.

Сериализация данных при помощи XStream

XStream – это простая, но мощная библиотека, позволяющая сериализовать объекты из XML и десериализовать их обратно в XML. Она отличается гибкостью, простотой, производительностью, экономной работой с памятью, низкими накладными расходами, а также хорошим управлением генерируемыми XML-документами. Еще одна ее привлекательная черта – поддержка графов объектов с высокой степенью вложенности, таких как каталог объектов, представляющих CD-диски, каждый из которых содержит ряд объектов-записей, имеющих, в свою очередь, свои атрибуты. Технология XStream не требует корректировки существующих бизнес-классов, если у вас нет желания использовать поддерживаемые ею Java-аннотации.

Преобразование данных при помощи XSLT

Следующим компонентом предлагаемого решения является XSLT – язык преобразований структурированных документов XML в различные форматы, в том числе XML и HTML. XSLT – это мощный и сложный язык, основанный на XML, включающий в себя ряд стандартных функций, в частности, функции для обработки строк и форматирования. Для запрашивания и выборки наборов вершин XML в XSLT активно используется XPath.

Преобразования документов XML, как правило, описываются в виде набора правил в страницах стилей XSLT. Страницы стилей состоят из шаблонов, которые определяют, как именно следует преобразовывать определенные элементы XML. Шаблоны можно применять к элементам с определенным именем, находящимся в определенном контексте (например, контекст /Book/Title отличается от /Catalog/Title) или с определенными атрибутами (например, /Book/@Title).

Форматирование документов при помощи XSL-FO

XSL-FO – это просто схема XML для представления данных. Она похожа на HTML и CSS, однако информация о стилях не вынесена в отдельный файл CSS, а хранится непосредственно в документах FO. Применение сериализации XML в сочетании с XSL-FO дает целый ряд преимуществ.

  • Разделение ответственности. Слой представления данных (форматирование и разметка) отделяется от слоя модели (бизнес-объектов Java) благодаря сериализации XML и XSL-FO.
  • Слабая связь между данными и представлением. Таблицы стилей XSL-FO, используемые для форматирования и определения макетов документов, не привязаны к конкретным бизнес-объектам: они обрабатывают исключительно их XML-представление. Подобное разделение между данными и представлением позволяет легко изменять формат документов PDF без внесения каких-либо изменений в объекты Java.
  • Гибкость представления. Таблицы стилей XSL-FO отличаются исключительной гибкостью в том, что касается управления форматированием и макетом. Подобно CSS, стандарт XSL-FO включает ряд форматирующих элементов для управления такими параметрами, как тип шрифта, размер шрифта, отступы и оформление текста. Кроме того, XSL-FO предоставляет следующие возможности:
    • наборы атрибутов XSLT, которые позволяют многократно использовать одни и те же инструкции форматирования внутри документа;
    • поддержка сложного разбиения на страницы, позволяющая выбирать нужную компоновку при выполнении ряда условий (например, можно использовать разные компоновки для первой, последней и остальных страниц);
    • возможность добавления сносок к документу;
    • определение колонтитулов страниц (они тоже могут различаться для первой, последней и остальных страниц);
    • оптимизация верстки, в том числе поддержка инструкций "keep with next" (не отрывать абзац от следующего) и минимизация пробелов;
    • наследование стилей (наследование параметров форматирования и стиля);
    • оглавление и закладки (XSL-FO позволяет генерировать оглавление для всего документа PDF);
    • поддержка таблиц, содержащих заголовочную, нижнюю и основную секции;
    • поддержка различных направлений записи текста, в том числе слева направо, справа налево, сверху вниз и снизу вверх.

Работа с XSL-FO заключается в применении XSLT для преобразования документа XML в документ FO, который затем обрабатывается процессором FO для генерации итогового представления (в данном случае документа PDF). Весь процесс преобразования при помощи XSL-FO показан на рисунке 1.

XSL-FO transformation process for a XML document (generate an FO document and finally a PDF document)
XSL-FO transformation process for a XML document (generate an FO document and finally a PDF document)

Архитектура предлагаемого решения

Основная цель предлагаемого решения заключается в реализации гибкой схемы создания документов PDF на основе произвольных объектов Java. При этом компонент, отвечающий непосредственно за генерацию PDF, должен быть максимально изолирован от Java-классов модели. В данном решении роль такого "изолятора" играет промежуточное представление объектов в XML, генерируемое при помощи XStream и далее преобразуемое в формат FO.

На рисунке 2 показано, как из описанных выше основных компонентов (бизнес-объекты Java, XStream) строится законченное решение для генерации документов в формате PDF, представляющих заказы на покупку.

Solution architecture to generate the XML document and then the PDF document
Solution architecture to generate the XML document and then the PDF document

Вначале необходимо создать XML-представление данных путем сериализации бизнес-объекта типа IPurchaseOrder (заказ на покупку), который содержит экземпляры Java-классов IOrderItem и IAddress. Для максимальной гибкости в данном решении используются аннотации Java, которые отображают классы на соответствующие элементы и атрибуты документа XML, представляющего заказ на покупку.

На следующем этапе заказ на покупку, представленный в виде XML-файла, преобразуется в документ FO, который затем должен поступить на вход процессора FO. Данное преобразование выполняется при помощи страницы стилей XSLT. Полученный документ FO передается на обработку библиотеке Apache FOP – открытой реализации процессора FO, которая генерирует документ PDF.

Итоговый документ PDF должен выглядеть, как показано на рисунке 3. Для этого необходимо создать соответствующую страницу стилей XSL-FO.

Screenshot of  sample Purchase Order PDF document
Screenshot of sample Purchase Order PDF document

Диаграммы классов

Описываемое решение включает пять интерфейсов и реализующих их классов. Краткое описание каждого из интерфейсов приведено в таблице 1.

Таблица 1. Интерфейсы и их реализации
Имя интерфейса/классаОписание
IXmlSerializableИнтерфейс для классов, сериализуемых в XML.
IAddressИнтерфейс бизнес-объекта, представляющего адрес.
IPurchaseOrderИнтерфейс бизнес-объекта, представляющего заказ на покупку.
IOrderItemИнтерфейс бизнес-объекта, представляющего позицию заказа.
IPdfGeneratorИнтерфейс генератора, преобразующего любой бизнес-объект, который реализует интерфейс IXmlSerializable, в формат PDF.

На рисунке 4 показана диаграмма этих интерфейсов на универсальном языке моделирования (Unified Modeling Language - UML).

UML diagram of interfaces in Table 1
UML diagram of interfaces in Table 1

Наше решение состоит из реализаций показанных выше интерфейсов и класса Tester. Диаграмма этих бизнес-классов показана на рисунке 5.

UML diagram of the business objects
UML diagram of the business objects

Схема XML для представления заказов на покупку

В таблице 2 описаны ключевые элементы XML-схемы для представления заказов на покупку.

Таблица 2. Компоненты схемы представления заказов на покупку
Ключевой элементОписание
OrderDateСодержит дату заказа на покупку.
CompanyAddressСодержит адресные данные компании, в том числе ее название, улицу, город, штат и почтовый индекс.
CustomerAddressСодержит адресные данные покупателя, в том числе название компании, улицу, город, штат и почтовый индекс.
ItemsСодержит список заказанных товаров, для каждого из которых указывается идентификатор, название и количество заказанных единиц.

Генерация XML-представления заказа на покупку при помощи XStream

Первый этап генерации XML-документа, представляющего заказ на покупку, заключается в сериализации бизнес-объекта типа PurchaseOrder в XML.

В данном случае управление генерацией XML через XStream осуществляется при помощи аннотаций. Аннотации являются предпочтительным способом конфигурирования формата XML, генерируемого XStream, поскольку они, во-первых, просты в использовании, а во-вторых, позволяют описывать отображение в XML на уровне классов, тем самым повышая надежность кода сериализации. Без аннотаций любые изменения бизнес-классов автоматически приводили бы к необходимости корректировки кода сериализации, в то время как использование аннотаций делает код чище, так как описание сериализации не отделяется от определения классов и полей. Таким образом, при добавлении нового поля в класс вы можете сразу в том же файле определить, как это поле должно сериализоваться в XML.

Замечание. Использование аннотаций возможно только в Java версии 1.5 и выше. В данной статье они применяются для управления генерируемым форматом XML.

Обратите внимание на класс PurchaseOrder в листинге 1, который реализует интерфейсы IPurchaseOrder и IXmlSerializable . Ключевую роль при сериализации в XML играет метод toXml, определенный в интерфейсе IXmlSerializable. Данная реализация содержит минимально необходимый объем кода, вызывающий сериализатор XStream с включенной поддержкой аннотаций. Для каждого внутреннего поля XStream ищет аннотации, описывающие способ сериализации соответствующего класса. Если таковые не найдены, то используются соглашения сериализации по умолчанию.

Листинг 1. Класс PurchaseOrder
class@XStreamAlias("PurchaseOrder")
public class PurchaseOrder implements IPurchaseOrder, IXmlSerializable {
    // закрытые переменные-члены
    /** номер заказа. */
    @XStreamAlias("OrderId")
    @XStreamAsAttribute
    private String m_orderId = null;

    /** дата заказа. */
    @XStreamAlias("OrderDate")
    private Date m_orderDate = null;

    /** адрес компании. */
    @XStreamAlias("CompanyAddress")
    private IAddress m_companyAddr = null;

    /** адрес клиента. */
    @XStreamAlias("CustomerAddress")
    private IAddress m_customerAdress = null;

    /** товары. */
    @XStreamAlias("Items")
    private ArrayList<IOrderItem> m_items = null;

    ...

    // Метод интерфейса IXmlSerializable
    public String toXml() {
        XStream xstream = new XStream();
        xstream.autodetectAnnotations(true);
        return xstream.toXML(this);
    }
}

Аннотации @XStreamAlias указывают XStream, какие элементы и атрибуты XML следует использовать в процессе сериализации и десериализации. В свою очередь аннотация XStreamAsAttribute означает, что данное поле должно сериализоваться в виде атрибута, а не отдельного элемента XML.

Далее следует создать классы Address и OrderItem (их исходный код содержится в архиве к статье). Им необязательно реализовывать интерфейс IXmlSerializable, поскольку они не будут сериализоваться отдельно от класса PurchaseOrder. Реализация этих классов достаточно очевидна и состоит в основном из частных переменных экземпляра и открытых get/set-методов. При этом частные переменные экземпляра снабжены аннотациями для управления сериализацией через XStream.

Вызов метода toXml приводит к генерации документа XML, аналогичного показанному в листинге 2.

Листинг 2. Представление заказа на покупку в XML
<PurchaseOrder OrderId="PO-123-456789">
    <OrderDate>2009-06-14 13:05:02.251 EDT</OrderDate>
    <CompanyAddress>
        <CompanyName>ACME Company</CompanyName>
        <StreetAddress>123 Main Street</StreetAddress>
        <City>Orlando</City>
        <State>FL</State>
        <ZipCode>32801</ZipCode>
    </CompanyAddress>
    <CustomerAddress>
        <CompanyName>A++</CompanyName>
        <StreetAddress>123 8th Avenue</StreetAddress>
        <City>Orlando</City>
        <State>FL</State>
        <ZipCode>32801</ZipCode>
    </CustomerAddress>
    <Items>
        <Item ItemId="A1B2C3" ItemName="Widget" Quantity="100" ItemCost="100.5"/>
        <Item ItemId="C3B2A1" ItemName="Micro-Widget" Quantity="1000" ItemCost="10.75"/>
    </Items>
</PurchaseOrder>

Генерация представления заказа на покупку в виде документа FO

На следующем шаге выполняется преобразование заказа на покупку из XML (см. листинг 2) в документ FO. При написании страниц XSLT часто бывает полезно создать прототип того, как должен выглядеть результат преобразования. Этот подход работает особенно удачно в случае использования XSL-FO, поскольку он позволяет проверить форматирование и макет путем тестовой обработки прототипа документа FO процессором Apache FOP. После проверки корректности макета вам останется только создать страницу XSL-FO для преобразования исходных документов XML в документы FO.

Создание документа FO

Любой документ FO имеет следующие два основных раздела.

  • Определение макета страницы. В этом разделе описывается макет страницы, в том числе поля и содержимое (например, верхний и нижний колонтитулы, основной текст).
  • Последовательность страниц (содержимое и данные). В этом разделе находится содержимое страниц документа, включая верхние и нижние колонтитулы и основную часть страницы. Для каждой последовательности страниц должен быть определен макет.

Вначале рассмотрим модель макета. Основные компоненты страниц документа FO, такие как Region-Before, Region-After, Region-Body, Region-Start и Region-End, показаны на рисунке 6.

FO page layout model
FO page layout model

Макет основной области страницы находится в элементе Region-Body (см. рисунок 6). Элементы Region-Before и Region-After, как правило, используются для верхнего и нижнего колонтитулов. Кроме того, часть содержимого документа может размещаться в правой и левой областях (элементы Region-Start и Region-End).

Эти области могут включать следующие типы содержимого.

  • Содержимое со статическим размещением, которое может находиться только в статических областях, например в верхнем или нижнем колонтитуле.
  • Содержимое с динамическим размещением, привязанное к макету страницы, которое может размещаться на нескольких страницах. К этому типу относится содержимое основной области документа FO.

Макет страницы включает в себя определение размера страницы и отступов. При этом в пределах одного документа может быть несколько макетов страниц. В листинге 3 показано описание простого макета страницы размером 8.5 x 11 дюймов.

Листинг 3. Пример макета страницы
<fo:layout-master-set>
    <fo:simple-page-master 
        margin-right="1in" 
        margin-left="1in" 
        margin-bottom="0.5in" 
        margin-top="1in" 
        page-width="8.5in" 
        page-height="11in" 
        master-name="standardletter">

        <fo:region-body 
            margin-bottom="1in" 
            margin-top="1in"/>		
        <fo:region-after extent="0.5in"/>
    </fo:simple-page-master>
</fo:layout-master-set>

Стандарт FO включает следующие элементы контента, которые могут быть добавлены к элементам со статическим или динамическим содержимым.

  • block - контейнер, аналогичный тегу <DIV> в HTML. Каждый элемент block может содержать текст или другие элементы FO.
  • inline - контейнер, аналогичный HTML-тегу <SPAN>.
  • table - таблица, аналогичная тегу <TABLE> в HTML.
  • list - список, похожий на HTML-теги  <UL> и <OL>.
  • external-graphic - изображение или другой графический объект; аналогичен тегу <IMG> в HTML.
  • character - единичный символ; как правило, используется в случае необходимости специального форматирования отдельных символов.
  • basic-link - ссылка в документе, аналогичная тегу <A> в HTML.

Создание страницы XSLT для преобразования заказа на покупку

Преобразование документа XML, представляющего заказ на покупку, начинается с корневого узла PurchaseOrder. К нему применяется шаблон, показанный в листинге 4, который генерирует элементы layout-master-set и page-sequence.

Листинг 4. Шаблон PurchaseOrder
<xsl:template match="PurchaseOrder">
    <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
        <!-- Определение макета страницы, в том числе полей -->
        <fo:layout-master-set>			
            <!-- Размер страницы и полей -->
            <fo:simple-page-master 
                master-name="all"
                page-height="11in"
                page-width="8.5in" 
                margin-top="0.25in"
                margin-bottom="0.25in"
                margin-left="1in" 
                margin-right="1in">
            <!—Макет основной области -->
            <fo:region-body margin-top="2in" 
                margin-bottom="1in" />
            <!—Макет верхнего колонтитула -->
            <fo:region-before extent="2in" />
            <!—Макет нижнего колонтитула -->
            <fo:region-after extent="1in" />
            </fo:simple-page-master>
        </fo:layout-master-set>

        <!-- Создание последовательности страниц-->
        <fo:page-sequence master-reference="all">
            <!-- Создание верхнего колонтитула -->
            <fo:static-content flow-name="xsl-region-before">
                <!-- Вывод адреса компании в верхнем колонтитуле -->
                <xsl:apply-templates 
                    select="CompanyAddress" />
                <fo:block font-size="18pt" 
                    font-family="sans-serif"
                    line-height="1.5em" 
                    background-color="black" 
                    color="white"
                    text-align="center"
                    >PURCHASE ORDER</fo:block>
            </fo:static-content>
            <!-- Создание нижнего колонтитула -->
            <fo:static-content flow-name="xsl-region-after">
                <!-- Добавление номера страницы к нижнему колонтитулу -->
                <fo:block text-align="end" 
                font-size="10pt" 
                font-family="serif">
                    Page <fo:page-number />
                </fo:block>
            </fo:static-content>

            <!-- Вывод основного содержимого документа -->
            <fo:flow flow-name="xsl-region-body">
                <!-- Вывод информации о заказе (дата, номер)-->
                <xsl:call-template name=
                "DisplayOrderInformation" />

                <!-- Вывод адреса покупателя -->
                <xsl:apply-templates select=
                "CustomerAddress" />

                <!-- Вывод товаров-->
                <xsl:apply-templates select="Items" />
            </fo:flow>
        </fo:page-sequence>
    </fo:root>
</xsl:template>

На следующем шаге генерируется верхний колонтитул страницы, содержащий логотип и адрес компании. При формировании статического содержимого секции Region-Before вызывается отдельный шаблон, применяющийся ко всем элементам CompanyAddress. Он создает таблицу из двух колонок, в первую из которых помещается логотип компании, а во вторую – ее адрес. Шаблон преобразования элементов CompanyAddress приведен в листинге 5.

Листинг 5. Шаблон CompanyAddress
<xsl:template match="CompanyAddress">
<fo:table width="100%">
    <fo:table-column column-width="40%" />
    <fo:table-column column-width="60%" />
    <fo:table-body>
        <fo:table-row>
            <fo:table-cell>
            <fo:block>
            <fo:external-graphic
            src="url(D:\workspace\DwArticle5\resources\CompanyLogo.jpg)" />
            </fo:block>
            </fo:table-cell>
            <fo:table-cell>
            <fo:block>
            <fo:block text-align="right">
            <xsl:value-of select="./StreetAddress" />
            </fo:block>
            <fo:block text-align="right">
            <xsl:value-of select="concat(./City, ' ', 
                ./State, ' ', 
                ./ZipCode)" />
            </fo:block>
            </fo:block>
            </fo:table-cell>
        </fo:table-row>
    </fo:table-body>
</fo:table>
</xsl:template>

Далее в документе выводится информация о заказе, в частности, его идентификатор и дата. Для этой цели служит шаблон с именем DisplayOrderInformation, вызываемый в следующей строке:

<xsl:call-template name="DisplayOrderInformation" />

Предыдущие шаблоны были безымянными и применялись инструкцией <xsl:apply-templates/>. Именованные шаблоны полезны для реализации процедур, возвращающих значения, либо в тех случаях, когда необходимо обрабатывать не все дочерние элементы, а специально выбранный набор вершин. Кроме того, их можно с успехом применять в рекурсивных сценариях обработки. В нашем случае такой шаблон используется для вывода информации о заказе исключительно в учебных целях; существуют и другие, более оптимальные варианты. Текст нашего именованного шаблона приведен в листинге 6.

Листинг 6. Шаблон DisplayOrderInformation
<xsl:template name="DisplayOrderInformation">
    <fo:block text-align="right">
        <fo:inline font-weight="bold">Id:</fo:inline>
        <xsl:value-of select="./@OrderId" />
    </fo:block>
    <fo:block text-align="right">
        <fo:inline font-weight="bold">Order Date:</fo:inline>
        <xsl:value-of select="./OrderDate" />
    </fo:block>
</xsl:template>

Данный шаблон преобразует элементы Items и Item в таблицу, каждая строка которой соответствует элементу Item. Кроме того, строки таблицы нумеруются, для каждого товара подсчитывается его общая стоимость, а внизу добавляется строка с итоговой суммой всего заказа. Для нумерации используется инструкция <xsl:number/>, генерирующая последовательность чисел. Общая стоимость одного товара вычисляется путем умножения числа заказанных единиц товара на стоимость единицы. В данном примере показаны два рекурсивных способа вычисления итоговой суммы всего заказа: при помощи рекурсивного именованного шаблона и с использованием режима применения шаблона и рекурсии. В обоих случаях итоговая сумма заказа вычисляется путем суммирования общей стоимости по всем заказанным товарам.

В листинге 7 показаны все шаблоны, отвечающие за генерацию таблицы, которая содержит все товары, перечисленные в заказе.

Листинг 7. Шаблоны Items в Item
<!-- Шаблон для элемента Items-->	
<!-- Вывод таблицы -->	
<xsl:template match="Items">
    <fo:block font-weight="bold" background-color="black" color="white"
    padding="2pt">ITEMS</fo:block>

    <fo:table width="100%">
    <!-- Указание ширины колонок таблицы -->
    <fo:table-column column-width="10%" />
    <fo:table-column column-width="15%" />
    <fo:table-column column-width="30%" />
    <fo:table-column column-width="15%" />
    <fo:table-column column-width="15%" />
    <fo:table-column column-width="15%" />
    <!-- Заголовочная строка таблицы -->	
    <fo:table-header>
    <fo:table-row>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold"
    >#</fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold"
    >Item ID</fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold"
    >Description</fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold"
    >Quantity</fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold"
    >Item Cost</fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold"
    >Total Cost</fo:block>
    </fo:table-cell>
    </fo:table-row>
    </fo:table-header>
    <fo:table-body>
    <xsl:apply-templates />
    <!-- Итоговая строка таблицы, охватывающая все колонки.
         Общая сумма выводится жирным шрифтом.-->		
    <fo:table-row>
    <fo:table-cell border="solid black 1px"
    number-columns-spanned="5">
    <fo:block font-weight="bold" 
    text-align="right">Total
    </fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block font-weight="bold">
    <!-- Значение данной переменной выводится ниже -->
        <xsl:variable name="total">
    <!-- Следующую строку следует раскомментировать 
         при использовании режимов применения и рекурсии. -->
    <!--							     
    <xsl:apply-templates select="Item[1]"
            mode="calculateTotal" />-->
    <!-- Инструкцию call-template следует закомментировать, если 
         не используется вызов именованного шаблона. -->
            <xsl:call-template name="calculateTotal">
                <xsl:with-param name="nodes" select="Item" />
            </xsl:call-template> 
        </xsl:variable>
    <!-- Вывод значения переменной "total" -->					
        <xsl:value-of select="format-number($total, '$#,##0.00')"/>
    </fo:block>
    </fo:table-cell>
    </fo:table-row>
    </fo:table-body>
    </fo:table>
</xsl:template>

<!-- Шаблон для элементов Item-->	
<!-- Выводит тег TR для каждого элемента Item -->	
<xsl:template match="Item">
<fo:table-row>
    <fo:table-cell border="solid black 1px">
    <fo:block>
    <xsl:number/>
    </fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block>
    <xsl:value-of select="@ItemId" />
    </fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block>
    <xsl:value-of select="@ItemName" />
    </fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block>
    <xsl:value-of select="@Quantity" />
    </fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <fo:block>
    <xsl:value-of select="format-number(@ItemCost, '$#,##0.00')" />
    </fo:block>
    </fo:table-cell>
    <fo:table-cell border="solid black 1px">
    <xsl:variable name="total" select="@Quantity * @ItemCost" />
    <fo:block>
    <xsl:value-of select="format-number($total, '$#,##0.00')" />
    </fo:block>
    </fo:table-cell>
    </fo:table-row>
</xsl:template>

Как уже говорилось, в данном примере представлены два основных способа подсчета общей суммы заказа. Первый из них, основанный на использовании именованного шаблона, приведен в листинге 8. Шаблон calculateTotal вызывается рекурсивно для всех элементов Item. Принцип работы шаблона поясняется во вложенных комментариях.

Листинг 8. Именованный шаблон для вычисления общей суммы заказа
<xsl:template name="calculateTotal">
<xsl:param name="nodes" select="/.." />
<xsl:param name="subtotal" select="0" />
<xsl:variable name="total"
 select="$subtotal + ($nodes[1]/@ItemCost * $nodes[1]/@Quantity)" />
<xsl:choose>
    <!-- Проверка наличия необработанных элементов Item.
         Если таких нет, обработка прекращается, и возвращается результат. -->
    <xsl:when test="not($nodes[2])">
        <xsl:value-of select="$total" />
    </xsl:when>
    <!-- Если необработанные вершины Item еще остались,
         рекурсивно вызывается этот же шаблон. -->
    <xsl:otherwise>
        <xsl:call-template name="calculateTotal">
            <xsl:with-param name="nodes"
            select="$nodes[position() > 1]" />
            <xsl:with-param name="subtotal" select="$total" />
        </xsl:call-template>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>

Второй вариант заключается в рекурсивных вызовах шаблонов с разными режимами применения. Шаблон, показанный в листинге 9, применяется ко всем элементам Item в случае, если режим равен calculateTotal. При этом необходимо создать пустой шаблон для тех дочерних вершин, которые должны игнорироваться при применении шаблона в этом режиме. Как и его именованный аналог, данный шаблон применяется рекурсивно до тех пор, пока существует хотя бы один необработанный элемент Item. Принцип его работы также описывается в комментариях.

Листинг 9. Шаблон для элементов Item, применяемый в режиме calculateTotal
<xsl:template match="Item" mode="calculateTotal">
<xsl:param name="subtotal" select="0" />
<xsl:variable name="total"
 select="$subtotal + (@ItemCost * @Quantity)" />
<xsl:choose>
<!-- Проверка наличия необработанных элементов Item.
         Если таких нет, обработка прекращается, и возвращается результат. -->
    <xsl:when test="not(following-sibling::Item)">
        <xsl:value-of select="$total" />
    </xsl:when>
    <xsl:otherwise>
<!-- Если необработанные вершины Item еще остались,
         рекурсивно применяется этот же шаблон. -->
        <xsl:apply-templates select="following-sibling::Item[1]"
            mode="calculateTotal">
            <xsl:with-param name="subtotal" select="$total" />
        </xsl:apply-templates>
    </xsl:otherwise>
</xsl:choose>
</xsl:template>

Генерация документа PDF

Далее остается последний шаг — создание документа PDF на основе XML, сгенерированного XStream, при помощи только что созданной страницы XSL. Для этого служит класс PdfGenerator, который применяет преобразование XSL к документу XML, используя стандартный класс Transformer, и передает полученный документ FO на вход процессора Apache FOP.

Листинг 10. Класс PdfGenerator
public class PdfGenerator implements IPdfGenerator {

    public OutputStream generate(IXmlSerializable object, String stylesheetPath, 
                    OutputStream pdfContent) {    
        try {
            // Указание источника для XML
            String xml = object.toXml();
            StreamSource xmlSource = 
                        new StreamSource(new ByteArrayInputStream(xml.getBytes()));
      
            // Указание источника для страницы стилей
            File xslFile = new File(stylesheetPath);
            FileInputStream xslFileStream = new FileInputStream(xslFile);
            StreamSource xslSource = new StreamSource(xslFileStream);

            // Получение XSL-процессора
            TransformerFactory tfactory = TransformerFactory.newInstance();
            Transformer transformer = tfactory.newTransformer(xslSource);

            // Настройка процессора FOP
            FopFactory fopFactory = FopFactory.newInstance();
            FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
            foUserAgent.setProducer(this.getClass().getName());
            Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent,
                                    pdfContent);
                  
            // Выполнение преобразования
            Result res = new SAXResult(fop.getDefaultHandler());
            transformer.transform(xmlSource, res);
        } catch (FileNotFoundException e) { e.printStackTrace(); 
        } catch (TransformerException e) {  e.printStackTrace();
        } catch (FOPException e) { e.printStackTrace();
        }
            
        return pdfContent;
    }
}

Рекомендации по использованию XStream и XSL-FO

При работе с XStream и XSL-FO следует придерживаться ряда рекомендаций.

  • Используйте Java-аннотации для конфигурирования XStream, поскольку они повышают гибкость управления и способствуют слабой связанности уровней приложения.
  • Используйте режимы применения шаблонов, а не именованные шаблоны, поскольку преобразование наборов вершин лучше укладывается в философию XSLT, чем вызов процедур.
  • Не опасайтесь рекурсивного применения шаблонов. Попытки избежать рекурсии очень часто приводят к ненужному усложнению страниц XSL.
  • Тщательно продумывайте схему XML. Непродуманные схемы, сделанные на скорую руку, часто бывают сложны в поддержке и дальнейшем развитии. В частности, следует внимательно подходить к вопросу выбора между атрибутами и элементами. Например, если вместо элемента Publisher (издатель) использовать одноименный атрибут, то в дальнейшем будет сложно добавить дополнительную информацию об издателе в документ XML.

Заключение

Прочитав эту статью, вы увидели, насколько легко можно генерировать документы PDF на основе бизнес-объектов Java при помощи XStream и XSL-FO. Принцип разделения ответственности позволяет отделить уровень представления данных от самих бизнес-объектов, в результате чего можно корректировать представление (т. е. документы PDF), не внося никаких изменений в Java-классы. Для этой цели служат страницы стилей XSL, в которых описываются все требования к представлению данных. Кроме того, такое решение позволяет использовать несколько страниц XSL для генерации представлений разного типа, например разных документов, на основе одних и тех же бизнес-объектов Java.


Ресурсы для скачивания


Похожие темы

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML
ArticleID=779648
ArticleTitle=Создание документов в формате PDF при помощи XStream и XSL-FO
publish-date=12072011