В предприятии, для которого я работаю, документы печатаются в формате PDF через XSL-FO, примерно как в моём недавнем примере. Мне нужно было дополнить шаблон XSL и вывести определённую информацию для каждой позиции заказа от клиента, нашего заказа у оптовой торговли и выставленного счёта. Так как я не был силён в Formating Objects, я сначал накидал шаблон в HTML, примерно такой, как представленный в рамках этого поста, и позже перенёс его в FO.
В конечном итоге получилось, как на скриншоте ниже. Позиция в документе состоит из пяти столбцов: позиция (число), наименование товара, количество, цена за одну штуку и сумма цены позиции. Ниже начиная со столбца с наименованием идёт описание товара. Точно так же я вставил свою информацию, в примере я назвал эту информацию подробностями, которыми могут быть, например, серийные номера. Для некоторых товаров такая информация есть, для некоторых нет. Например, для второй позиции на скриншоте подробностей нет.

Скриншот: HTML-таблица после преобразования
XML-файл с данными, который я получал из экпорта из Apex (Shared Components -> Report Queries), состоял из двух информационных блоков, в первом из которых (ROWSET1) были данные о позициях, во втором (ROWSET2) данные о подробностях. Связаны эти данные были полем позиция (POSITION), котрое присутствует в обоих блоках. Каждая ряд данных из блока позиции может иметь одну или несколько соответствующих рядов в подробностях или не иметь их вовсе, каждый ряд данных в подробностях относится к одной из позиций.
Код XML-файла
<DOCUMENT> <DATA> <ROWSET1> <ROWSET1_ROW> <POSITION>1</POSITION> <NAME>Наименование товара 1</NAME> <DESCRIPTION>Описание товара 1</DESCRIPTION> <AMOUNT>3</AMOUNT> <PRICE>100.00</PRICE> <SUM>300.00</SUM> </ROWSET1_ROW> <ROWSET1_ROW> <POSITION>2</POSITION> <NAME>Наименование товара 2</NAME> <DESCRIPTION>Описание товара 2</DESCRIPTION> <AMOUNT>2</AMOUNT> <PRICE>300.00</PRICE> <SUM>600.00</SUM> </ROWSET1_ROW> <ROWSET1_ROW> <POSITION>3</POSITION> <NAME>Наименование товара 3</NAME> <DESCRIPTION>Описание товара 3</DESCRIPTION> <AMOUNT>1</AMOUNT> <PRICE>50.00</PRICE> <SUM>50.00</SUM> </ROWSET1_ROW> </ROWSET1> <ROWSET2> <ROWSET2_ROW> <POSITION>1</POSITION> <SERIAL>111111</SERIAL> </ROWSET2_ROW> <ROWSET2_ROW> <POSITION>1</POSITION> <SERIAL>222222</SERIAL> </ROWSET2_ROW> <ROWSET2_ROW> <POSITION>1</POSITION> <SERIAL>333333</SERIAL> </ROWSET2_ROW> <ROWSET2_ROW> <POSITION>3</POSITION> <SERIAL>111111</SERIAL> </ROWSET2_ROW> </ROWSET2> </DATA> </DOCUMENT>
Мой основной XSL-шаблон выдался достаточно простым. Все данные отображаются в HTML-документе, который содержит таблицу. Таблица содержит шаблон, который используется (apply-templates) для каждого ряда (ROWSET1_ROW) из блока данных с позициями (ROWSET1).
Код основного шаблона
<xsl:template match="DOCUMENT/DATA"> <html> <body> <table border="1"> <xsl:apply-templates select="ROWSET1/ROWSET1_ROW"/> </table> </body> </html> </xsl:template>
Для каждого ряда данных в ROWSET1 выводится ряд HTML-таблицы (элемент tr — table row), в котором выводится позиция, наименование товара и другие данные, о которых я писал выше. Рядом ниже следует описание товара. Обратите внимание на атрибутcolspan="100%"
, это значит, что клетка таблицы (элемент td) будет размером до конца ряда — первая клетка в ряду пуста, значит клетка с описанием будет размером в четыре клетки. Ниже запоминаем текущую позицию в переменной this_position и проверяем в if-блоке, есть ли к этой позиции подробности в ROWSET2. Если есть, то открываем шаблон (call-template) с названием details и передаём текущую позицию через параметр (with-param).
Честно сказать, я долго матюкался, так как в XSL нет нормальных переменных, ведь переменную нельзя изменить после объявления. К тому же здесь нет циклов, которые работают, как в других языках программирования. Чтобы узнать, существуют ли подробности, мне пришлось применить функцию count и выражение XPath, в котором я сравниваю текущую позицию с позициями в блоке ROWSET2.
Код шаблона позиций
<xsl:template match="ROWSET1_ROW"> <tr> <td><xsl:value-of select="POSITION"/></td> <td><xsl:value-of select="NAME"/></td> <td><xsl:value-of select="AMOUNT"/></td> <td><xsl:value-of select="PRICE"/></td> <td><xsl:value-of select="SUM"/></td> </tr> <tr> <td></td> <td colspan="100%"><xsl:value-of select="DESCRIPTION"/></td> </tr> <xsl:variable name="this_position" select="POSITION"/> <xsl:if test="count(//DOCUMENT/DATA/ROWSET2/ROWSET2_ROW[POSITION=$this_position]) > 0"> <xsl:call-template name="details"> <xsl:with-param name="id_position" select="POSITION" /> </xsl:call-template> </xsl:if> </xsl:template>
В последнем шаблоне я сравниваю переданный параметр с позициями в ROWSET2 и вывожу данные соответствующие параметру (for-each). Кстати, string($id_position) != ''
— это своеобразный XSL-вариант выражения != null в Java. Если объект не существует, то каст в строку будет пустой строкой. Я проверяю на всякий случай, была ли передана позиция и в случае, если нет, не вывожу ничего.
Код шаблона подробностей
<xsl:template name="details"> <xsl:param name="id_position" /> <xsl:if test="string($id_position) != ''"> <tr> <td></td> <td colspan="100%"> <xsl:variable name="this_count" select="count(//DOCUMENT/DATA/ROWSET2/ROWSET2_ROW[POSITION=$id_position])"/> <xsl:text>Подробности (</xsl:text><xsl:value-of select="$this_count"/>)<xsl:text>: </xsl:text> <xsl:for-each select="/DOCUMENT/DATA/ROWSET2/ROWSET2_ROW"> <xsl:variable name="this_position" select="POSITION"/> <xsl:if test="$this_position = $id_position"> <xsl:value-of select="SERIAL"/> <xsl:text>; </xsl:text> </xsl:if> </xsl:for-each> </td> </tr> </xsl:if> </xsl:template>
Вы можете скачать архив с примером, который содержит XSL-файл и XSL-файл. Распаковав архив и открыв файл index.xml браузером, в случае если трансформация прошла успешно, вы увидите не XML с данными, а HTML, как на первом и единственном скриншоте в этом посту.
Понравился пост? Поделись в соцсетях и подписывайся на аккаунты в Twitter и Facebook!