Montag, 17. August 2009

Arbeiten mit Templates

Unter Templates, also Schablonen oder Vorlagen, sind im weiteren Sinne beliebige Muster zu verstehen, die man laden und für eigene Zwecke anpassen kann. Ein Objekt wird also nicht identisch wiederverwendet, nicht 1:1 übernommen, sondern kopiert (zum Beispiel von der Datenbank / dem Shared Memory in den Speicher der Sitzung) und angepasst. Praktisch alle Präsentationsaufgaben laufen auf eine Verarbeitung von Templates hinaus. Aber auch auf Click verfügbare Codemuster im Editor sind Templates. Auch die automatische Codegenerierung basiert häufig auf Templates.

Einige Beispiele von Templates, wie sie für das Präsentieren von Daten verwendet werden, habe ich in meinem Blog Präsentieren ist Arbeiten mit Schablonen behandelt: Die in den Perl-Sprachkern eingebaute Interpolation, die Schablonenverarbeitung mit XSLT, Formulare und Views. In diesem Blog will ich die Verwendung von Schablonen am Beispiel von Views und Taglib-Templates noch detaillierter erläutern.

Wie geht die Verarbeitung eines BSP-Views zur Laufzeit eigentlich konkret vor sich?

Der "Road Runner", wie das BSP-Framework anfangs hiess, ist eine der wenigen Stellen im SAP-System, in dem Klassengenerierung verwendet wird [1],[2]. Zunächst legt der Entwickler im View-Editor das HTML-Design seiner Ausgabe fest, wobei sich meist statische und dynamische Anteile abwechseln. Hier ein Beispielview mit drei typischen Kategorien von Bestandteilen:

<%@page language="abap"%>
<div class="header">
<%=otr($TMP/BERICHT_FUER_FILIALE)%>
<%=t001w-werks%>
</div>

Wir haben hier einerseits Bestandteile, die aus Sicht des BSP-Frameworks lediglich Konstanten darstellen: das einleitende HTML-Element <div> und dessen Abschluss. Es gibt auch einen Text, der von der Sprache abhängt: Den OTR-Kurztext $TMP/BERICHT_FUER_FILIALE. Schliesslich gibt es ein ABAP-Datenobjekt, das zur Laufzeit mittels move in die Antwort gestellt wird: Das Feld t001w-werks.[3]

Der BSP-Compiler generiert aus diesem Entwurf eine Viewklasse – eine Unterklasse von CL_BSP_PAGE. Die Unterklassen bekommen einen Namen, der aus dem Präfix CL_O2 und einer 25stelligen, automatisch generierten ID besteht. Das folgende Bild zeigt die Hierarchie – wobei noch zu beachten ist, dass die automatisch generierten Klassen nicht in der Workbench (SE80) betrachtet werden können, da ihnen kein Paket zugeordnet ist - wohl aber ist die Anzeige direkt im Class Builder (SE24) möglich.



Obiger Beispielview wird vom BSP-Framework in folgenden Code gewandelt:
method _ONLAYOUT.

* generated by BSP converter version: 200502181202
* generated by BSP compiler version: 1.60
*

DATA %_O2_UTL_000 TYPE STRING. "#EC *
DATA: %_O2X TYPE STRING. "#EC *

_M_PAGE_CONTEXT->M_OUT->PRINT_STRING(
VALUE = _M_HTML_POOL OFFSET = 0 LENGTH = 20 ).
CLEAR %_O2_UTL_000.
SYSTEM-CALL OTR GET_TEXT_BY_ALIAS LANGUAGE SY-LANGU
ID '$TMP/BERICHT_FUER_FILIALE' NR '0001' TEXT INTO %_O2_UTL_000.
_M_PAGE_CONTEXT->M_OUT->PRINT_STRING( VALUE = %_O2_UTL_000 ).
%_O2X = t001w-werks.
_M_PAGE_CONTEXT->M_OUT->PRINT_STRING( VALUE = %_O2X ).
_M_PAGE_CONTEXT->M_OUT->PRINT_STRING(
VALUE = _M_HTML_POOL OFFSET = 20 LENGTH = 8 ).

Das globale Attribut _m_html_pool ist ein String, der sämtliche aus Sicht des BSP-Frameworks konstanten Teile des Views enthält. Mit der Methode print_string werden die durch Offset und Länge identifizierten Substrings des Pools in den Ausgabestrom geschrieben (repräsentiert durch den BSP-Writer m_out). Der OTR-Text und das Feld t001w-werks können dagegen nur dynamisch ermittelt werden, da sie bei verschiedenen Aufrufen verschiedene Inhalte haben können.

Das also ist die Designidee der BSP-Architekten für die Templateverarbeitung: Die festen und variablen Bestandteile werden durch eine fest programmierte Reihenfolge von Aufrufen der Methode print_string() zusammengemischt, wobei die einzelnen festen Bestandteile aus einem für den View unveränderlichen String, dem HTML-Pool ausgeschnitten werden. Dieser wird letztlich in einer Datenbanktabelle (o2pagrt) verwaltet, jedoch aus Performancegründen auch im sitzungsübergreifenden shared buffer zwischengespeichert. Die Anweisungen zur Ermittlung der dynamischen Bestandteile werden an Ort und Stelle in die Folge der print_string()-Aufrufe hineingeneriert.

Ich will hier ein alternatives Vorgehen zur Verarbeitung von Templates skizzieren, wie ich es für BSP-Extensions in einem neuen Projekt verwendet habe. Motivation war hierbei, HTML-Code nicht mehr als String im ABAP-Code hinzuschreiben, sondern immer in BSP-Views zu pflegen. Kleinere Templates, die selbst im Rahmen eines BSP-Views aufgerufen werden, können aus Effizienzgründen nicht mit einem internen HTTP-Request beschafft werden. Stattdessen sollen sie als Templateobjekte in einem Gebiet des Shared Objects Memory vorgehalten werden, und die Substitution dynamischer Werte soll zur Laufzeit mit Hilfe von Stringoperationen erfolgen.

Ein Template ist eine Folge von statischen und dynamischen Bestandteilen. Mein Ansatz ist, diese Bestandteile der Reihe nach in eine stringtab, eine interne Tabelle vom elementaren Zeilentyp string einzufüllen. Die dynamischen Bestandteile bekommen dabei einen Bezeichner. Solche Bezeichner können im View - also dem Ort, wo der HTML-Code gepflegt wird - mit einer speziellen Syntax eingegeben werden: Sie sollen durch umschliessende "Lattenzäune" gekennzeichnet werden wie in folgendem Beispiel (wobei wir einmal die Sprachabhängigkeit ignorieren):
Bitte überweisen Sie #AMOUNT# #CURRENCY# umgehend auf das Konto #IBAN#.
Wenn das Template von einem BSP-Element angefordert wird, das zuständige Shared Object jedoch bemerkt, dass es nicht mehr oder noch nicht im Speicher verfügbar ist, beschafft es sich den Inhalt dieses Views mit Hilfe eines internen HTTP-Requests, generiert daraus ein Datenobjekt mit einer für die schnelle Substitution optimalen Datenstruktur und merkt sich dieses im sitzungsübergreifenden Hauptspeicher.

Aus obigem View-Code wird dabei folgende stringtab generiert (die einzelnen Strings schliesse ich, um auch die enthaltenen Leerzeichen kenntlich zu machen, in Hochkommata ein):
'Bitte überweisen Sie '
'AMOUNT'
'CURRENCY'
' umgehend auf das Konto '
'IBAN'
Sie sehen natürlich sofort, dass das Template durch diese stringtab noch nicht vollständig codiert ist. Denn aus der stringtab allein lässt sich ja nicht die Information ablesen, welche Teile fest und welche dynamisch sind. Der String IBAN könnte ja auch ein fester Text sein.

Zu der Stringtab kommt daher noch eine Hash-Tabelle hinzu, die für jeden im Template vorkommenden symbolischen Namen den Zeilenindex der Stringtab angibt, in die der dynamische Wert zu setzen ist. In obigem Beispiel also
AMOUNT   -> 2
CURRENCY -> 3
IBAN -> 5
Wenn wir noch einen Schlüsselteil hinzunehmen, unter dem pro Template noch verschiedene Festwerte auseinandergesteuert werden können (ich verwende hier nur die Anmeldesprache) ergibt sich folgende tiefe Struktur für ein Template:



Ein Template-Objekt ist nun ein Shared Object, das eine Reihe derartiger Templates in einer globalen öffentlichen (aber nur lesbaren) Hashtabelle enthält. Darüberhinaus enthält das Template-Objekt auch Methoden, wie aus dem Template der aktuelle String zu gewinnen ist. Im einfachsten Fall reduziert sich das auf ein Vorgehen wie in der nachfolgend aufgelisteten Methode substitute_by_parameters( ). Beim Aufruf erhält diese Methode eine Tabelle it_param_act von Aktualwerten der Parameter - sowie den Schlüssel is_key, unter dem das Template in der globalen Hashtabelle aufzufinden ist. Zurückgegeben wird der interpolierte HTML-Code in der Stringvariablen ev_html, die am Schluss der Methode aus der stringtab durch Konkatenation zusammengesetzt wird:

method substitute_by_parameters.

data: ls_template type zmss_taglib_template,
lv_substitions type i.

field-symbols: <ls_param_act> type ihttpnvp,
<ls_param> type zmss_taglib_template_param,
<lv_data> type string.

* Template aus dem Gedächtnis einlesen, ggf. per Request neu ermitteln
call method read_template
exporting
is_key = is_key
importing
es_template = ls_template.

* Parametersubstitution NAME durch VALUE
call method substitute_normal
exporting
it_param_act = it_param_act
changing
ct_param_template = ls_template-params
ct_data = ls_template-data.

if ls_template-params is not initial.
* Addon-Methode für spezielle Parameter
call method substitute_special
exporting
it_param_act = it_param_act
changing
ct_param_template = ls_template-params
ct_data = ls_template-data.
endif.

* Ergebnis aufbauen
clear ev_html.
loop at ls_template-data assigning <lv_data>.
concatenate ev_html <lv_data> into ev_html respecting blanks.
endloop.

endmethod.

Durch Redefinition der verwendeten Methoden in Subklassen lässt sich dieses Standardverhalten beliebig modifizieren. Während insbesondere die Methode substitute_special() für die Redefinition vorgesehen ist, enthält die Methode substitute_normal() den Geradeausfall des Ersetzens. Jeder ersetzte Parameter wird dabei aus dem Hash ls_template-params herausgelöscht. Nur dann also, wenn nach Ausführung von substitute_normal() noch unaufgelöste Parameter verbleiben, wird die Methode substitute_special( ) aufgerufen.
method substitute_normal.

field-symbols: <ls_param_act> type ihttpnvp,
<lv_data> type string,
<ls_param> type zmss_taglib_template_param.

loop at it_param_act assigning <ls_param_act>.

* Parameter, die nicht im Template vorkommen, sind erlaubt
* und werden ignoriert
read table ct_param_template assigning <ls_param>
with table key name = <ls_param_act>-name.
check sy-subrc eq 0.

read table ct_data assigning <lv_data>
index <ls_param>-tabix.

* Die Zeilenindices von ls_template-params verweisen immer
* auf gültige Zeilen des Templates!
assert sy-subrc eq 0.

* Zeile in stringtab mit aktuellem Wert überschreiben
<lv_data> = <ls_param_act>-value.

* Abgearbeiteten Parameter aus Hashtabelle entfernen
delete table ct_param_template from <ls_param>.

endloop.

endmethod.

Mit Templateobjekten dieser Art ist es möglich, HTML-Code vollständig aus dem ABAP-Quelltext zu verbannen. Er kann stattdessen dort gepflegt werden, wo er natürlicherweise hingehört: Im BSP-View. Aufgrund der effizienten Implementierung mittels Shared Objects braucht man dabei keine Performanceeinbussen zu befürchten.


[1] Die automatische Klassengenerierung wird nicht nur für BSP-Views, sondern auch für BSP-Elemente, für den Bedingungseditor des PostProcessingFrameworks (PPF) und bei der Definition von Shared Objects eingesetzt. Weitere Verwendungen sind mir nicht bekannt.
[2] Die automatische Generierung von Programmcode, zum Beispiel Klassen, aus einem abstrakteren Designdokument, ist eine Technik, die das Potential hat, unsere Arbeitsweise grundlegend zu verändern und auf die ich in Zukunft noch zurückkommen werde.
[3] Hierbei ist vorausgesetzt, dass der View ein Attribut t001w vom strukturierten Datentyp t001w hat. Das sind Details, die für diese Diskussion keine Rolle spielen.

Keine Kommentare :