Montag, 12. September 2011

Ein JSON-Builder in ABAP

Zu dem in einem früheren Post beschriebenen JSON-Parser gibt es ein Gegenstück – den JSON-Builder. Er kann gegebene ABAP-Daten in das JSON-Datenformat wandeln. Die ABAP-Daten sind dabei – wie der Zieltyp des JSON-Parsers – vom Typ ZUT_DATA und ermöglichen beliebig tiefe Schachtelung. Hier der Typ von ZUT_DATA, wenn man ihn nicht, wie es richtig ist, im DDIC, sondern als lokalen Datentyp definieren würde:
types:
begin of zut_data,
type type c,
value type ref to data,
end of zut_data.

Der Typschlüssel type kann dabei die Wert S für String, N für Zahl, B für Boolesch, a für Array und h für Hash annehmen.

Es gäbe verschiedene Strategien für die Implementierung eines solchen Builders. Man könnte alles in ABAP ausprogrammieren oder sich eine andere der in ABAP verfügbaren Sprachen zunutze machen – in diesem Fall bietet sich der JavaScript-Interpreter cl_java_script oder der XSLT-Prozessor an (in ABAP mit der Anweisung call transformation integriert).

Ich habe mich in diesem Fall gefühlsmässig für XSLT entschieden: Die erforderliche Rekursion durch tiefe Datenstrukturen wird durch XSLT gut unterstützt, auch gilt der in ABAP eingebaute XSLT-Prozessor als schnell. Über den JavaScript-Prozessor kenne ich keine Performance-Angaben, jedoch habe ich Forenbeiträge gelesen, in denen der JavaScript-Prozessor als sehr ressourcenintensiv beschrieben wurde. Eine reine ABAP-Implementierung schliesslich wäre ebenso gut möglich gewesen. Ob die Stringverarbeitung von ABAP für grosse Datenobjekte mit der XSLT-Verarbeitung konkurrieren kann, wäre eine interessante Frage. Hier will ich aber den Weg über eine XSLT-Transformation zabap2json beschreiben.

Bei Verwendung einer Transformation ist die zentrale build-Methode, die einen zut_data-Parameter entgegennimmt und einen String im JSON-Format zurückgibt, eine reine Delegation:
method build.
call transformation zabap2json
options data_refs = 'heap-or-create'
source data_root = is_data
result json = ev_json.
replace regex '^\s+' in ev_json with space.
replace regex '\s+$' in ev_json with space.
endmethod.

Wichtig ist hier allerdings der Zusatz data_refs = 'heap-or-create'. Hiermit weisen wir den Prozessor an, alle nicht selbständigen, also auf den Stack oder auf globalen Daten verweisende Referenzen so zu behandeln wie selbständige, also mit create data auf dem Heap angelegte Datenobjekte. Würden wir diese Option nicht angeben, so würden die nicht selbständigen Datenreferenzen nicht ins ABAPXML-Format serialisiert und könnten daher nicht von der XSLT-Transformation bearbeitet werden.

In diesem Fall haben wir einen Aufruf von call transformation, der ABAP-Daten entgegennimmt und auch wieder an den Aufrufer zurückgibt. XML tritt nur als Zwischenformat auf - als Quell- und Zielformat der Transformation.

Wie sieht nun die Transformation zabap2json aus? Die ersten Templates steigen zunächst bis zum Basistemplate showData der Rekursion ab und spezifizieren, dass der Ergebnisstring in ein <JSON>-Element eingepackt werden soll – unter diesem Namen kann das Ergebnis dann in ABAP als String abgeholt werden:
  <xsl:template match="/">
<xsl:apply-templates select="/asx:abap/asx:values"/>
</xsl:template>

<xsl:template match="DATA_ROOT">
<xsl:call-template name="getData"/>
</xsl:template>

<xsl:template name="getData">
<asx:abap>
<asx:values>
<JSON>
<xsl:call-template name="showData">
<xsl:with-param name="data" select="."/>
</xsl:call-template>
</JSON>
</asx:values>
</asx:abap>
</xsl:template>

Das zentrale Template showData muss nun den Typschlüssel auswerten und an ein dazu passendes Template delegieren. Da es in XSLT keine Möglichkeit gibt, Templates dynamisch aufzurufen, muss der passende Templateaufruf in einem <xsl:choose>-Block programmiert werden:

  <xsl:template name="showData">
<xsl:param name="data"/>
<xsl:variable name="type" select="$data/TYPE"/>
<xsl:for-each select="$data/DATA">
<xsl:choose>
<xsl:when test="$type = 'N'">
<xsl:call-template name="simpleValue"/>
</xsl:when>
<xsl:when test="$type = 'S'">
<xsl:call-template name="stringValue"/>
</xsl:when>
<xsl:when test="$type = 'B'">
<xsl:call-template name="booleanValue"/>
</xsl:when>
<xsl:when test="$type = 'h'">
<xsl:call-template name="showHash"/>
</xsl:when>
<xsl:when test="$type = 'a'">
<xsl:call-template name="showArray"/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>

Das <for-each> wird natürlich nur einmal durchlaufen und setzt dabei den current node für die Templates.

Die Regel für den Hash zeigt, dass das Template showData bei der Auflösung tiefer Datenstrukturen parametrisiert aufgerufen wird. Auch sieht man am select-Attribut der <for-each>-Schleife über die Hash-Elemente, wie der Zugriff auf eine Datenreferenz im Heap zu bewerkstelligen ist: Die ID im Heap entspricht dem Wert des Attributs href in der Referenz ab dem 2. Zeichen (XLink- bzw. XPointer-konform wird einem href-Verweis auf ein Element mit einer ID im aktuellen Dokument ein # vorangestellt):

  <xsl:template name="showHash" match="ZUT_DATA" mode="h">
<xsl:variable name="id" select="substring(@href,2)"/>
{
<xsl:for-each select="/asx:abap/asx:heap/*[@id = $id]/*">
<xsl:if test="position() > 1">,</xsl:if>
<xsl:call-template name="showHashRow"/>
</xsl:for-each>
}
</xsl:template>

<xsl:template name="showHashRow">
"<xsl:value-of select="KEY"/>":
<xsl:call-template name="showData">
<xsl:with-param name="data" select="."/>
</xsl:call-template>
</xsl:template>


Das Template für einen simpleValue ist so gestaltet, dass es mit Direktwerten ebenso wie mit Referenzen umgehen kann:

  <xsl:template name="simpleValue">
<xsl:choose>
<xsl:when test="@href">
<xsl:call-template name="getReference">
<xsl:with-param name="href" select="./@href"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>


Umständlich wird leider nur das Template zur Darstellung eines Strings. Es müssen ja alle " im String druch \" und alle einfachen Backslashs \ durch doppelte \\ maskiert werden. Da es keine XPath-Funktion zum Ersetzen von Strings gibt, ist diese Funktion separat dazuzuprogrammieren. Gott sei Dank gibt es das Internet, in dem sich Funktionen wie XSLT string-replace auffinden lassen - danke Erik!
  <xsl:template name="stringValue">
"<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="simpleValue"/>
</xsl:with-param>
<xsl:with-param name="replace">\</xsl:with-param>
<xsl:with-param name="by">\\</xsl:with-param>
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace">"</xsl:with-param>
<xsl:with-param name="by">\"</xsl:with-param>
</xsl:call-template>"
</xsl:template>

<!-- Template dankend übernommen
aus http://geekswithblogs.net/Erik/archive/2008/04/01/120915.aspx -->
<xsl:template name="string-replace-all">
<xsl:param name="text" />
<xsl:param name="replace" />
<xsl:param name="by"/>
<xsl:choose>
<xsl:when test="contains($text, $replace)">
<xsl:value-of select="substring-before($text,$replace)" />
<xsl:value-of select="$by" />
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text"
select="substring-after($text,$replace)" />
<xsl:with-param name="replace" select="$replace" />
<xsl:with-param name="by" select="$by" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>

Wer sich für die Transformation näher interessiert, kann den vollständigen Quelltext bei Pastebin betrachten oder herunterladen.

Nachtrag am 31.3.2012 Ich habe ergänzend zur Klasse noch einige Macros geschrieben, die den ABAP-seitigen Aufbau komplexer Datenstrukturen in eine Variable vom Typ ZUT_DATA erleichtern. Das folgende Beispiel zeigt die Verwendung:
* --- Beispiel zur Erzeugung einer komplexen ZUT_DATA-Struktur
form build_hash changing cs_data type zut_data.


data: lt_constants type zut_hash_tab,
lt_first_numbers type zut_array_tab.

_json_data. " Hilfsvariablen

_add_to_array lt_first_numbers:
number 1,
number 2,
number 3.

* Einige Elemente in den Hash aufnehmen
_add_to_hash lt_constants :
array `first_numbers` lt_first_numbers,
number `pi` `3.141592`,
boolean `wahr` 'X',
boolean `falsch` ' '.

_set_data cs_data hash lt_constants.


endform. "build_hash

Legt man das resultierende Datenobjekt dem zcl_json_builder vor, so erzeugt er den folgenden String (den ich aus Lesbarkeitsgründen noch manuell formatiert habe):
{
"first_numbers":[1 ,2 ,3 ],
"pi":3.141592,
"wahr":true,
"falsch":false
}

Die Macro-Funktionen sind im Include-Programm Z_MACRO_BUILD_JSON definiert.

Nachtrag am 12.3.2013

Durch die Integration von JSON in den ABAP Kernel werden sowohl der hier vorgestellte JSON-Builder als auch der JSON-Parser ab SAP-Basisrelease 7.02 obsolet.

Im einfachsten Fall - wenn man das aus den ABAP-Daten abgeleitete "kanonische JSON-Format" akzeptieren kann - kommt man mit einem Zwei- bis Dreizeiler zur Transformation von ABAP-Daten in JSON-Daten und umgekehrt aus. Hierbei liegt die Anweisung call transformation id zugrunde. Um ein abweichendes JSON-Format herzustellen, kann man statt id eine selbstdefinierte XSLT-Transformation verwenden, wie in meinem Blog Developing a REST API in ABAP beschrieben.

Manchmal muss aber die Zielstruktur Stück für Stück aufgebaut werden - dann ist eine Klasse, die ähnlich wie der hier beschriebene ZCL_JSON_BUILDER arbeitet, schon nützlich. Einen JSON-Builder, der von der JSON-Integration in den Kernel profitiert, habe ich in Form der Klasse ZCL_JSON_OUTPUT implementiert und im SCN-Blog Playing the JSONata the iXML way näher beschrieben.

9 Kommentare :

Ray hat gesagt…
Dieser Kommentar wurde vom Autor entfernt.
Ray hat gesagt…

Hallo Herr Plantiko,

ich habe mit der XSLT Transformation, speziell beim serialisieren von Standard Tabellen ein Problem festgestellt.

Der JSON Array ["eins", "zwei"] wird mit ihrem JSON Parser als Referenz auf eine Standardtabelle deserialisiert, aber die XSLT-Transformation bricht mit einem Kurzdump ab.

Info aus st22: Die ABAP Daten sind nicht serialiserbar.

Haben sie dasselbe Problem beim serialisieren von Standardtabellen.

Mit Hashtabellen funktioniert die XSLT Transformation.

Gruss Ray Burks

Rüdiger Plantiko hat gesagt…

Hallo Ray,

die Klasse ZCL_JSON_BUILDER ist bei mir durch Unit Tests abgesichert. Vielleicht stimmt etwas mit Deinen Aufruftypen nicht?

Vergleiche den Aufruf mit http://bsp.mits.ch/code/clas/zcl_json_builder

Die Klasse enthält in der Section "ABAP Unit Tests" auch meine Unit Tests, auch mit Tabellen.

Kannst Du mal schauen, ob diese Tests, sinngemäss auf Deinen Code angepasst, bei Dir auch durchlaufen? Hast Du beim Aufruf der Transformation den Zusatz OPTIONS DATA_REFS wie beschrieben angegeben?

Die Unit Tests laufen hier auf SAP_BASIS 702 und SAP_BASIS 700 ohne Probleme.

Gruss,
Rüdiger

Rüdiger Plantiko hat gesagt…

Hallo Ray,

sehe gerade, dass Du im falschen Blog gepostet hast. Ein Builder ist etwas ganz anderes als ein Parser. So ziemlich das Gegenteil... :-)

Aber auch im Parser, Klasse http://bsp.mits.ch/code/clas/zcl_json_parser gibt es Unit Tests, auch mit Standard- und Hashtabellen. Bitte mal ausprobieren.

Gruss,
Rüdiger

Ray hat gesagt…

Hallo Rüdiger,

ich gehe mal zum du über. Erstmal danke für die Infos.

Weisst wie es ist, wenn man im dunkeln ist und plötzlich ein Licht aufgeht.

Ich habe garnicht gewusst, dass es eine Klasse ZCL_JSON_BUILDER existiert.

Ich habe nur die build-Methode in den Parser integriert und das Ergebnis der parse-Methode wieder über die build-Methode serialisieren wollen.

Die Unit Tests für den Parser habe ich schon angeschaut, funzt...

Die UnitTests des Builders beantworten alle meine offenen Fragen. Das probiere gleich heute abend mal aus.

Danke und Gruss Ray

Rüdiger Plantiko hat gesagt…

Hallo Ray,

schön, dass Dir die Unit Tests helfen, den Code des JSON-Builders zu verstehen und obendrein auch noch im Grossen und Ganzen das wiedergeben, was Du machen willst.

Noch etwas, wozu Unit Tests taugen: als Entwicklerdoku!

Gruss,
Rüdiger

Andre Beck hat gesagt…

Hallo,

könnten sie den Code des JSON Builder komplett via Pastebin veröffentlichen, damit ich Copy&Paste machen kann?

Ich muss nun aus SAP Daten mittels JSON Array an eine Adresse schicken und bin nicht wirklich Experte was das Transformationszeug angeht.

Ich danke Ihnen vorab recht herzlich.
Spitzen Beispiele!!!

Grüße
A. Beck

Rüdiger Plantiko hat gesagt…

Hallo André,

klar. Hier ist der vollständige Sourcecode der Klasse, generiert mit dem undokumentierten Funktionscode SHOW_CLIF im Class Builder.

http://pastebin.com/fXSM808n

Dazu benötigst Du noch die XSLT-Transformation selbst. Mehr sollte eigentlich nicht nötig sein.

AB hat gesagt…

Hello Rüdiger, I know yours is an obsolete approach since SAP supports native conversion in newer releases, still I'm at a customer with an old version (7.0 kernel) so no native transformation or even /UI2/CL_JSON are available. I'm looking for alternatives and your classes could come in handy (already tried ZJSON which has problems with large amounts of data, and ZCL_MDP_JSON which dumps in some methods).

I'm trying to load and use your code, already downloaded:

https://pastebin.com/2RXxL0Du (parser)
https://pastebin.com/fXSM808n (builder)
https://pastebin.com/FCkhLVQp (XSLT)
http://bsp.mits.ch/code/fugr/zutil (FM Z_GET_FIELDNAMES)

Still, I'm missing definitions of dictionary objects, namely:

ZUT_DATA
ZUT_HASH_ELEMENT
ZUT_HASH_TAB

(I don't know if further checks will show something more missing)

Some comment clarifies about ZUT_DATA:

types : begin of zu_data,
type type c length 1 ,
data type ref to data ,
end of zu_data.

Could you please provide the rest?

Thank you!