XSLT преобразование внутренней таблицы в ABAP, имеющей поле типа «generic reference»

в 16:57, , рубрики: abap, ERP-системы, sap, Программирование, метки: ,

Пролог

О чем заметка? Как из внутренней таблицы, строка которой содержит ссылку на неизвестный (обобщенный) тип (REF TO DATA), которая, по факту, хранит ссылку на такую же таблицу, получить XML заданного формата. При этом, число уровней вложенности изначально неизвестно.
Зачем это нужно? Мне это понадобилось при выгрузке данных в различные форматы XML-файлов MS Office без использования OLE.
Для кого эта заметка? Для программистов на ABAP.
Необходимый уровень знания: знать, что такое reference type, generic type, XML; слышать, что существует такая вещь как XSLT.

Как же с этим бороться?

Задача

Есть дерево. Дерево хранится во внутренней таблице. Тип таблицы определен следующим образом:

TYPES
  : BEGIN OF tdeep_struct
    , name TYPE string
    , rf_child_list TYPE REF TO data  "та самая обобщенная сылка
  , END OF tdeep_struct

  , t_deep_struct TYPE STANDARD TABLE OF tdeep_struct
                    WITH NON-UNIQUE DEFAULT KEY
  .

Т.е. структура строки таблицы в поле rf_child_list содержит ссылку на обобщенный тип (generic type).

Из внутренней таблицы такой структуры, нужно получить XML следующего формата:

<List>
	<MyItem>
		<Name>Имя первой записи</Name>
    </MyItem>
	<MyItem>
		<Name>Имя следующей записи</Name>
    </MyItem>
</List>

При этом расположение узлов исходного дерева в итоговом XML должно соответствовать нисходящему обходу дерева (Т.е. начинаем просмотр записей внутренней таблицы сверху. Выводим содержимое первой записи, если у нее есть зависимые записи в rf_child_list – то начинаем выводить их с первой записи. Если у нее есть зависимые – то следом выводим их и т.д.).

Тестовый пример

Допустим, у нас во внутренней таблице сохранено дерево следующего вида(перечислены значения поля Name):

GrandParent1
	Child1
	Child2
		GrandChild1
GrandParent2

По условиям задачи мы должны получить из этих данных вот такой XML.

Итоговый XML для тестового примера
<List>
	<MyItem>
		<Name> GrandParent1</Name>
    </MyItem>
	<MyItem>
		<Name>Child1 </Name>
    </MyItem>
	<MyItem>
		<Name>Child2 </Name>
    </MyItem>
	<MyItem>
		<Name>GrandChild1 </Name> 
    </MyItem>
	<MyItem>
		<Name> GrandParent2</Name>
    </MyItem>
</List>

Решение

Начнем с простого: ABAP-код, что же может быть проще?

Определим и заполним тестовыми данными внутреннюю таблицу

DATA
  : gt_tree         TYPE t_deep_struct "таблица дерева
  , gtree            TYPE tdeep_struct   "рабочая область записи первого уровня
  , gchild           TYPE tdeep_struct   "рабочая область записи второго уровня
  , ggrandchild  TYPE tdeep_struct  "рабочая область записи третьего уровня
  .

FIELD-SYMBOLS
  : <gt_child_list>          TYPE t_deep_struct "внутренняя таблица записей второго уровня
  , <gt_grandchild_list> TYPE t_deep_struct "внутренняя таблица записей третьего уровня 
  .

START-OF-SELECTION.

  gtree-name = 'GrandParent1'.

  CREATE DATA gtree-rf_child_list TYPE t_deep_struct.

  ASSIGN gtree-rf_child_list->* TO <gt_child_list>.

  gchild-name = 'Child1'.
  APPEND gchild TO <gt_child_list>.

  gchild-name = 'Child2'.

  CREATE DATA gchild-rf_child_list TYPE t_deep_struct.
  ASSIGN gchild-rf_child_list->* TO <gt_grandchild_list>.
  ggrandchild-name = 'GrandChild1'.
  APPEND ggrandchild TO <gt_grandchild_list>.

  APPEND gchild TO <gt_child_list>.

  APPEND gtree TO gt_tree.

  CLEAR gtree.

  gtree-name = 'GrandParent2'.
  APPEND gtree TO gt_tree.  

Решая задачу я подумал, что надо бы использовать XSLT-преобразование. Так как тэг у этой заметки я поставил «XSLT для начинающих», то надо упомянуть, что же это такое. Если совсем на пальцах, то это такой язык, который позволяет из одного XML получить другой XML. При этом сам XSLT является подмножеством XML. Стоп! А причем тут внутренние таблицы, если он преобразует XML? А это такая особенность реализации XSLT в SAP. Посмотрим подробнее.

За выполнение трансформации в ABAP отвечает оператор CALL TRANSFORMATION. У него много параметром и много вариантов работы.
Входные данные требующие преобразования будем передавать, указав SOURCE имя_параметра = имя_внутренней_таблицы.
Результат можно получить указав RESULT XML объектXML/внутренняя таблица/строка/объект потока. Для простоты, получать результат будем в строку.

Результат трансформации отобразим на экране стандартными средствами объекта CL_XML_DOCUMENT.
Получаем вот такой код:

  CALL TRANSFORMATION zedu_test_xslt_deep_transform2
    SOURCE
      table = gt_tree
    RESULT XML gxml_str.

  go_xml_doc->parse_string( gxml_str  ).

  go_xml_doc->display( ).

Ах да… я же отвлекся! Вопрос-то был: и причем тут трансформация, если вообще-то она для преобразовании одного XML в другой?

Немного rtfm

Начнем с того, что в SAP трансформации используются для сериализации и десериализации данных. Т.е., как из какого-то объекта данных (структуры, внутренней таблицы, объекта) получить представление в виде последовательности битов и наоборот. У нас как раз задача сериализации: на входе вложенная структура (внутренняя таблица ABAP), а на выходе – XML(текстовое представление).
XSLT в SAP бывают 2-ух видов: обычный XSLT и simple transformation (ST). SAP показалось мало XSLT и он реализовал свое подмножество XSLT, которое назвал simple transformation. Нам оно не подходит, т.к. не умеет работать с полями generic type в входных параметрах.
Остается чистый XSLT.
SAP, при выполнении сериализации с применением XSLT, приводит входные параметры трансформации к виду asXML (canonical XML representation). В процессе этого преобразования, по определенным правилам, происходит конверсия типов ABAP, это касается и generic type (Явно получить каноническое представление какой либо переменной можно использовав стандартное для ABAP XSLT преобразование с зарезервированным именем ID ).
Картинка из хелпа ABAP, объясняющая преобразования данных при трансформациях
Нас интересует направление Serialization

Для моего примера каноническое XML представление выглядит следующим образом:

<?xml version="1.0" encoding="utf-8" ?> 
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
	<asx:values>
		<TABLE>
			<item>
				<NAME>GrandParent1</NAME> 
				<RF_CHILD_LIST href="#d1" /> 
			</item>
			<item>
				<NAME>GrandParent2</NAME> 
				<RF_CHILD_LIST /> 
			</item>
		</TABLE>
	</asx:values>
	<asx:heap 
        xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
        xmlns:abap="http://www.sap.com/abapxml/types/built-in" 
        xmlns:cls="http://www.sap.com/abapxml/classes/global" 
        xmlns:dic="http://www.sap.com/abapxml/types/dictionary">
		<prg:T_DEEP_STRUCT id="d1" 
                     xmlns:prg="http://www.sap.com/abapxml/types/program/ZEDU_TEST_XSLT_DEEP_TRANSFORM">
			<item>
				<NAME>Child1</NAME> 
				<RF_CHILD_LIST /> 
			</item>
			<item>
				<NAME>Child2</NAME> 
				<RF_CHILD_LIST href="#d2" /> 
			</item>
		</prg:T_DEEP_STRUCT>
		<prg:T_DEEP_STRUCT id="d2" 
                      xmlns:prg="http://www.sap.com/abapxml/types/program/ZEDU_TEST_XSLT_DEEP_TRANSFORM">
			<item>
				<NAME>GrandChild1</NAME> 
				<RF_CHILD_LIST /> 
			</item>
		</prg:T_DEEP_STRUCT>
	</asx:heap>
</asx:abap>

И как с этим жить дальше?

При получении канонического представления XML для внутренней таблицы, сама таблица преобразуется в элемент <TABLE>, внутри которого для каждой строки будет существовать элемент <item>. Каждое поле будет преобразовано в элемент с именем равным имени поля. Т.к. в мою структуру входит поле RF_CHILD_LIST, являющееся ссылкой на обобщенный тип DATA (TYPE REF TO DATA), то содержимое соответствующего элемента будет пустым, а в атрибуте элемента href будет храниться ссылка на содержимое, находящееся далее по документу.
Например, для первой строки значение <RF_CHILD_LIST href="#d1" />, где символ # — признак ссылки по правилам формирования ссылок XLink, а d1 – идентификатор ссылки. Само содержимое будет находиться в специальном разделе канонического XML-документа: asx:heap в элементе, имеющем атрибут id равный d1. В нашем примере <prg:T_DEEP_STRUCT id="d1" … >. Так как у нас рекурсивная структура, то дальнейшая вложенность элементов в каноническом представлении организована аналогично.
Т.е., как выглядит исходный XML документ для преобразования, мы разобрались.

А XSLT-то где?

Теперь нужно реализовать собственно XSLT-преобразование.
В SAP для создания трансформаций используется транзакция STRANS.
XSLT преобразование внутренней таблицы в ABAP, имеющей поле типа «generic reference»
Вводим название трансформации, нажимаем «Создать»(F5).
XSLT преобразование внутренней таблицы в ABAP, имеющей поле типа «generic reference»
Если нужно – изменяем название, указываем краткое описание и вид трансформации (в нашем случае – XSLT). Нажимаем Enter.

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

Использовать структурную печать в редакторе следует с осторожностью, т.к., в ряде случаев, редактор будет вставлять не нужные символы перевода строки (это может быть критично, если в итоге мы, например, хотим получить html документ).

При создании нового документа, система сразу же вставит шаблон пустого документа, т.е. мы его будем менять. Шаблон будет выглядеть следующим образом

<xsl:transform version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:sap="http://www.sap.com/sapxsl">

<xsl:strip-space elements="*"/>

<xsl:template match="/">
</xsl:template>

</xsl:transform>

Где:

  • xmlns:xsl означает, что мы будем использовать пространство имен xsl. Т.е. при использовании элементов их этого пространства имен, элементы будут иметь в начале префикс xsl: (например <xsl:strip-space elements="*"/>)
  • xmlns:sap означает, что мы будем использовать пространство имен sap
  • xsl:transform указание, что это у нас XSLT-преобразование
  • <xsl:strip-space elements="*"/> убирает лишние пробелы при преобразовании
  • <xsl:template match="/"> определяет шаблон преобразования, который применяется к корневому элементу исходного XML документа

XSLT-трансформация применяет преобразования указанные внутри шаблонов (xsl:template) к тем узлам исходного XML-документа, которые соответствуют выражению в атрибуте match. Значение атрибута match определяется с помощью специального языка XPath. В рамках этого языка, корневой элемент определяется как «/», доступ к элементу нашего исходного XML-документа TABLE будет определен как "/asx:abap/asx:values/TABLE" (т.е. перечисляются все узлы XML-документы от корневого (asx:abap) до нашего узла (TABLE)).

Для начала, добавляем корневой элемент нашего выходного XML-документа List. Шаблон примет вид:

  <xsl:template match="/">
    <List>
    </List>
  </xsl:template> 

Нам необходимо перебрать в цикле все элементы item внутри элемента TABLE нашего исходного XML документа, это делается с помощью команды <xsl:for-each select="/asx:abap/asx:values/TABLE/item"> , где в атрибуте select указывается XPath путь до элементов, по которым будет идти итерация. Внутри цикла будем выводить наш элемент MyItem, в котором выведем элемент Name со значением NAME соответствующего элемента исходного документа, по которому идет итерация. Таким образом, шаблон преобразования будет иметь теперь следующий вид:

  <xsl:template match="/"> 
    <List>
      <xsl:for-each select="/asx:abap/asx:values/TABLE/item">
        <MyItem>
          <Name><xsl:value-of select="NAME"/></Name>
        </MyItem>
      </xsl:for-each>
    </List>
  </xsl:template>

Значение из элемента исходного документа выводится командой <xsl:value-of select="NAME"/>, в атрибуте select которого указывается XPath-путь до элемента, из которого нужно взять значение. Обратим внимание, что путь в xsl:value-of указан несколько иначе, чем в xsl:for-each. Разница в том, что в xsl:for-each указан абсолютный путь, начиная от корневого элемента, а в xsl:value-of – относительный от вышестоящего элемента (т.е. пути в команде xsl:for-each).
Таким образом, реализовав данный шаблон, мы выведем только первый уровень рекурсивной структуры исходного документа.
Кроме того, при использовании такого шаблона у нас будут выводиться лишние пространства имен asx, sap, prg (XSLT их не отключает по умолчанию). Что бы исключить их из выходного XML, изменим xsl:transform следующим образом:

<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:sap="http://www.sap.com/sapxsl" 
    xmlns:asx="http://www.sap.com/abapxml" 
    xmlns:prg="http://www.sap.com/abapxml/types/program/ZEDU_TEST_XSLT_DEEP_TRANSFORM" 
    exclude-result-prefixes="asx sap prg" version="1.0">

Т.е. мы указали используемые пространства имен, а потом атрибутом exclude-result-prefixes перечислили те, которых не должно быть в выходном XML.
Теперь нужно добавить нисходящий обход тех самых данных, ссылка на которые лежит в элементе RF_CHILD_LIST. Для этого мы будем использовать еще один шаблон. Фактически шаблон – это как подпрограмма.
Предположим, что нам известен id соответствующий списку ссылок. Обозначим его как ChilListID. Это будет входной параметр шаблона.
Вот определение шаблона и его параметра:

  <xsl:template match="/asx:abap/asx:heap" name="OutChilds">
      <xsl:param name="ChilListID"/>
  </xsl:template>

Т.е. мы определили шаблон с именем OutChilds и параметром ChilListID. При этом, шаблон может обрабатывать только часть исходного XML-документа, которая соответствует поддереву /asx:abap/asx:heap.

Что нам нужно сделать в этом шаблоне? Перебрать все элементы item внутри элемента prg:T_DEEP_STRUCT с id равным входному параметру ChilListID. Делаем это с помощью уже знакомого нам xsl:for-each.
Сперва отыщем prg:T_DEEP_STRUCT с id равным входному параметру ChilListID:
<xsl:for-each select="prg:T_DEEP_STRUCT[@id=$ChilListID]">
Здесь XPath определяет, что итерация должна идти по тем элементам prg:T_DEEP_STRUCT, у которых аттрибут id равен параметру ChilListID.
Внутри данного цикла сделаем цикл по всем item: <xsl:for-each select="item">

Однако, можно сделать и иначе в рамках нашей задачи, без вложенных циклов, объединив их в один, XSLT такое позволяет. Выглядеть это будет вот так:
<xsl:for-each select="prg:T_DEEP_STRUCT[@id=$ChilListID]/item">

Выведем элемент MyItem и его Name. Получаем следующий шаблон обработки:

  <xsl:template match="/asx:abap/asx:heap" name="OutChilds">
    <xsl:param name="ChilListID"/>
      <xsl:for-each select="prg:T_DEEP_STRUCT[@id=$ChilListID]/item">
              <MyItem>
          <Name><xsl:value-of select="NAME"/></Name>
        </MyItem>
    </xsl:for-each>
  </xsl:template>

Мы вывели один уровень подчиненных узлов. Теперь нужно реализовать рекурсию: вызвать этот шаблон внутри него самого.
Вызов шаблона выполняется командой xsl:apply-templates, с указанием в атрибуте select к какой части дерева исходногоXML у нас будет применен этот шаблон.
Кроме того, не забываем, что нам еще нужно передать параметр ChilListID. Запись, которая ссылается на список дочерних записей, имеет в элементе RF_CHILD_LIST атрибут href, значение которого равно #id_списка_дочерних_записей. Т.е. нам нужно получить атрибут href и вырезать из него подстроку, начиная со второго символа. Это выполняется с помощью XPath следующим образом: substring(RF_CHILD_LIST/@href,2). Т.е. получается вот такой вызов шаблона:

      <xsl:apply-templates select="/asx:abap/asx:heap">
        <xsl:with-param 
           name="ChilListID" select="substring(RF_CHILD_LIST/@href,2)"/>
      </xsl:apply-templates>

В итоге, получаем шаблон обработки дочерних записей:

  <xsl:template match="/asx:abap/asx:heap" name="OutChilds">
    <xsl:param name="ChilListID"/>
    <xsl:for-each select="prg:T_DEEP_STRUCT[@id=$ChilListID]/item">
        <MyItem>
          <Name><xsl:value-of select="NAME"/></Name>
        </MyItem>

        <xsl:apply-templates select="/asx:abap/asx:heap">
          <xsl:with-param 
             name="ChilListID" select="substring(RF_CHILD_LIST/@href,2)"/>
        </xsl:apply-templates>
    </xsl:for-each>
  </xsl:template>

Теперь вставим, по аналогии, вызов этого шаблона в шаблон, работающий для корневого узла.

В итоге, полный текст преобразования примет следующий вид:

<xsl:transform 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns:sap="http://www.sap.com/sapxsl" 
  xmlns:asx="http://www.sap.com/abapxml" 
xmlns:prg="http://www.sap.com/abapxml/types/program/ZEDU_TEST_XSLT_DEEP_TRANSFORM" 
  exclude-result-prefixes="asx sap prg" version="1.0">

  <xsl:strip-space elements="*"/>

<!-- шаблон для поддерева начиная с корневого узла, т.е всего дерева -->
  <xsl:template match="/">
    <List>
<!--перебрать в цикле все записи верхнего уровня из исходного XML      -->
      <xsl:for-each select="/asx:abap/asx:values/TABLE/item">
        <MyItem>
<!--        вывести NAMR исходной записи-->
          <Name><xsl:value-of select="NAME"/></Name>
        </MyItem>
<!--        вызвать рекурсивный шаблон для вывода записей,
            на которые хранится ссылка в обрабатываемой записи -->
        <xsl:apply-templates select="/asx:abap/asx:heap">
          <xsl:with-param 
            name="ChilListID" select="substring(RF_CHILD_LIST/@href,2)"/>
        </xsl:apply-templates>
      </xsl:for-each>
    </List>
  </xsl:template>
<!--рекурсивный щаблон для вывода подчиненных записей-->
  <xsl:template match="/asx:abap/asx:heap" name="OutChilds">
<!--  определение параметра шаблона-->
    <xsl:param name="ChilListID"/>
<!--    найти список, id которого равен входному параметру, перебрать его записи-->
    <xsl:for-each select="prg:T_DEEP_STRUCT[@id=$ChilListID]/item">
        <MyItem>
          <Name><xsl:value-of select="NAME"/></Name>
        </MyItem>
<!--       вызвать самого себя рекурсивно-->
        <xsl:apply-templates select="/asx:abap/asx:heap">
          <xsl:with-param 
             name="ChilListID" select="substring(RF_CHILD_LIST/@href,2)"/>
        </xsl:apply-templates>
    </xsl:for-each>
  </xsl:template>
</xsl:transform>

Полный текст ABAP-программы

REPORT zedu_test_xslt_deep_transform.

*определим тип структуры имеющей поле типа "ссылка на обобщенный тип"
TYPES
  : BEGIN OF tdeep_struct
    , name TYPE string
    , rf_child_list TYPE REF TO data
  , END OF tdeep_struct

*табличный тип для структуры имеющей поле типа "ссылка на обобщенный тип"
  , t_deep_struct TYPE STANDARD TABLE OF tdeep_struct
                    WITH NON-UNIQUE DEFAULT KEY
  .

DATA
  : gt_tree      TYPE t_deep_struct "таблица дерева
  , gtree        TYPE tdeep_struct  "рабочая область верхнего уровня для таблицы дерева
  , gchild       TYPE tdeep_struct  "рабочая область второго уровня для таблицы дерева
  , ggrandchild  TYPE tdeep_struct  "рабочая область третьего уровня для таблицы дерева
  , gxml_str     TYPE string        "строка результирующего XML
  , go_xml_doc   TYPE REF TO cl_xml_document "объект для отображения результирующего XML
  .

FIELD-SYMBOLS
  : <gt_child_list>     TYPE t_deep_struct   "таблица записей второго уровня
  , <gt_grandchild_list> TYPE t_deep_struct  "таблица записей третьего уровня
  .

START-OF-SELECTION.
*заполнение таблицы тестовыми данными
  gtree-name = 'GrandParent1'.

  CREATE DATA gtree-rf_child_list TYPE t_deep_struct.

  ASSIGN gtree-rf_child_list->* TO <gt_child_list>.

  gchild-name = 'Child1'.
  APPEND gchild TO <gt_child_list>.

  gchild-name = 'Child2'.
  CREATE DATA gchild-rf_child_list TYPE t_deep_struct.
  ASSIGN gchild-rf_child_list->* TO <gt_grandchild_list>.

  ggrandchild-name = 'GrandChild1'.
  APPEND ggrandchild TO <gt_grandchild_list>.

  APPEND gchild TO <gt_child_list>.

  APPEND gtree TO gt_tree.

  CLEAR gtree.

  gtree-name = 'GrandParent2'.
  APPEND gtree TO gt_tree.

*вызов XSLT-трансформации
  CALL TRANSFORMATION zedu_test_xslt_deep_transform2
    SOURCE
      table = gt_tree
    RESULT XML gxml_str.

*покажем стандартными средствами полученный XML
  CREATE OBJECT go_xml_doc.
  go_xml_doc->parse_string( gxml_str  ).
  go_xml_doc->display( ).

Заключение

Правда ведь, не так уж и страшен XSLT для абапера?

Использованные материалы

  1. Справка ABAP
  2. MSDN: справочник по XSLT
  3. XPath tutorial
  4. Спецификация XSLT

Автор: PhysicalGraffiti

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js