As was pointed out in a recent web log, one nice thing about ABAP is its built-in support for dynamic programming techniques, particularly program generation. However, a common problem one faces in this context is how to actually construct the program's source code. The straight-forward solution is to build the code line by line concatenating text literals and string variables for the static respectively changing parts. While this approach might be feasible in simple cases it rapidly turns cumbersome when the code to be generated becomes more complex. A standard solution for this kind of problem is to use a template engine, and as it turns out, ABAP's XSLT processor fits the bill.
Suppose we need to map fields between two arbitrary (flat) structures and the mapping to be used is provided to us in an internal table:
TYPE-POOLS abap.
TYPES: BEGIN OF field_mapping,
dst TYPE abap_compname,
src TYPE abap_compname,
END OF field_mapping,
field_mappings TYPE STANDARD TABLE OF field_mapping WITH KEY dst.
A particular mapping is implemented by a mapper object, that can be accessed through a generic (global) interface:
INTERFACE zif_mapper.
METHODS: map
IMPORTING
is_src TYPE any
EXPORTING
es_dst TYPE any.
ENDINTERFACE.
Minimizing dependencies, we use a factory to create the mapper objects:
CLASS zcl_mapper_factory DEFINITION ABSTRACT.
PUBLIC SECTION.
METHODS: create ABSTRACT
IMPORTING
is_src TYPE any
is_dst TYPE any
it_map TYPE field_mappings
RETURNING
value(rr_mapper) TYPE REF TO zif_mapper.
ENDCLASS.
There are numerous ways to realize a service like this in ABAP, one (perhaps not the most elegant) is to use dynamic code generation. Of course, the overhead of creating an implementation of interface zif_mapper at runtime will only be justified if the mapping is used thousands of times subsequently. However, for our chosen approach the concrete factory may be implemented as follows:
CLASS zcl_mapper_factory_code_gen DEFINITION INHERITING FROM zcl_mapper_factory.
PUBLIC SECTION.
METHODS: create REDEFINITION.
PROTECTED SECTION.
TYPES: text TYPE STANDARD TABLE OF string WITH DEFAULT KEY.
METHODS: get_source_code
IMPORTING
ir_srcdescr TYPE REF TO cl_abap_structdescr
ir_dstdescr TYPE REF TO cl_abap_structdescr
it_map TYPE field_mappings
RETURNING
value(rt_source_code) TYPE text.
ENDCLASS.
CLASS zcl_mapper_factory_code_gen IMPLEMENTATION.
METHOD create.
DATA lr_srcdescr TYPE REF TO cl_abap_structdescr.
DATA lr_dstdescr TYPE REF TO cl_abap_structdescr.
DATA lt_source_code TYPE text.
DATA lv_program TYPE string.
DATA lv_msg TYPE string.
DATA lv_lin TYPE string.
DATA lv_wrd TYPE string.
DATA lv_off TYPE string.
lr_srcdescr ?= cl_abap_typedescr=>describe_by_data( is_src ).
lr_dstdescr ?= cl_abap_typedescr=>describe_by_data( is_dst ).
lt_source_code = get_source_code(
ir_srcdescr = lr_srcdescr
ir_dstdescr = lr_dstdescr
it_map = it_map ).
GENERATE SUBROUTINE POOL lt_source_code NAME lv_program
MESSAGE lv_msg
LINE lv_lin
WORD lv_wrd
OFFSET lv_off.
IF sy-subrc <> 0.
CONCATENATE 'Error in line' lv_lin ', word' lv_wrd 'at offset' lv_off ':' lv_msg
INTO lv_msg
SEPARATED BY ' '.
MESSAGE lv_msg TYPE 'E'.
ENDIF.
PERFORM instantiate
IN PROGRAM (lv_program)
CHANGING rr_mapper.
ENDMETHOD.
METHOD get_source_code.
DATA lv_line TYPE string.
FIELD-SYMBOLS <mapping> TYPE field_mapping.
APPEND 'program.' TO rt_source_code.
APPEND 'class mapper definition.' TO rt_source_code.
APPEND ' public section.' TO rt_source_code.
APPEND ' interfaces zif_mapper.' TO rt_source_code.
APPEND 'endclass.' TO rt_source_code.
APPEND 'class mapper implementation.' TO rt_source_code.
APPEND ' method zif_mapper~map.' TO rt_source_code.
lv_line = ir_srcdescr->get_relative_name( ).
CONCATENATE ' field-symbols <src> type' lv_line '.'
INTO lv_line
SEPARATED BY ' '.
APPEND lv_line TO rt_source_code.
lv_line = ir_dstdescr->get_relative_name( ).
CONCATENATE ' field-symbols <dst> type' lv_line '.'
INTO lv_line
SEPARATED BY ' '.
APPEND lv_line TO rt_source_code.
APPEND ' assign is_src to <src>.' TO rt_source_code.
APPEND ' assign es_dst to <dst>.' TO rt_source_code.
LOOP AT it_map ASSIGNING <mapping>.
CONCATENATE ' <dst>-' <mapping>-dst ' = <src>-' <mapping>-src '.'
INTO lv_line.
APPEND lv_line TO rt_source_code.
ENDLOOP.
APPEND ' endmethod.' TO rt_source_code.
APPEND 'endclass.' TO rt_source_code.
APPEND 'form instantiate changing cr_mapper.' TO rt_source_code.
APPEND ' create object cr_mapper type mapper.' TO rt_source_code.
APPEND 'endform.' TO rt_source_code.
ENDMETHOD.
ENDCLASS.
In class zcl_mapper_factory_code_gen the actual construction of the source code is moved to the dedicated method get_source_code, leaving the tasks of creating a dynamic subroutine pool and instantiating the mapper object to method create (at the price of introducing an implicit dependency on the generated form routine instantiate that acts as entry point for the code inside subroutine pool).
For didactic reasons we first implemented get_source_code the straight-forward way, i.e. by stringing together the source code line by line. Although the result is arguably still readable, it is easy to see what mess we would end up with if we had a more complex problem to solve.
Let's contrast this with the alternative implementation we get when we switch to using ABAP's XSLT processor as template engine. Method get_source_code then turns into little more than a generic driver routine that calls the XSL transformation which does the real work:
METHOD get_source_code.
DATA lv_src_type TYPE string.
DATA lv_dst_type TYPE string.
DATA lv_program TYPE string.
lv_src_type = ir_srcdescr->get_relative_name( ).
lv_dst_type = ir_dstdescr->get_relative_name( ).
CALL TRANSFORMATION zxslt_mapper
PARAMETERS src_type = lv_src_type
dst_type = lv_dst_type
SOURCE map = it_map
RESULT XML lv_program.
SPLIT lv_program
AT cl_abap_char_utilities=>cr_lf
INTO TABLE rt_source_code.
ENDMETHOD.
Here we use a variant of CALL TRANSFORMATION that allows us to take advantage of ABAP's capability to automatically transform data objects into their canonical XML representation. This representation of the internal table it_map acts as the XML source document that our XSLT program will have to transform into the desired ABAP source code. To figure out what the XML source tree looks like for an example mapping we can exchange the call to our own, yet to be written XSLT program zxslt_mapper with a call to the built-in id transformation and look at its output:
<?xml version="1.0" encoding="iso-8859-1" ?>
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
<asx:values>
<MAP>
<item>
<DST>EINS</DST>
<SRC>ONE</SRC>
</item>
<item>
<DST>ZWEI</DST>
<SRC>TWO</SRC>
</item>
<item>
<DST>DREI</DST>
<SRC>THREE</SRC>
</item>
</MAP>
</asx:values>
</asx:abap>
It is profitable to download this XML document to our local workstation since it can then act as test case during the development of our own XSL transformation. Using the workbench's test mode we can modify our XSLT program until the output of the transformation is what we need. The final result of this iterative process is the XSLT program zxslt_mapper shown below:
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sap="http://www.sap.com/sapxsl"
>
<xsl:output method="text" omit-xml-declaration="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="SRC_TYPE"/>
<xsl:param name="DST_TYPE"/>
<xsl:template match="/">
program.
class lcl_mapper definition.
public section.
interfaces zif_mapper.
endclass.
class lcl_mapper implementation.
method zif_mapper~map.
field-symbols <src> type <xsl:value-of select="$SRC_TYPE"/>.
field-symbols <dst> type <xsl:value-of select="$DST_TYPE"/>.
assign is_src to <src>.
assign es_dst to <dst>.
<xsl:apply-templates select="//MAP/item"/>
endmethod.
endclass.
form instantiate changing cr_mapper.
create object cr_mapper type lcl_mapper.
endform.
</xsl:template>
<xsl:template match="item">
<dst>-<xsl:value-of select="DST"/> = <src>-<xsl:value-of select="SRC"/>.
</xsl:template>
</xsl:transform>
The XSL transformation is quite straight forward, however a few points merit mentioning:
Hopefully this simple example already demonstrated how ABAP's XSLT processor can be employed as template engine in the context of code generation. For productive use (performance and the system limit on the number of subroutine pools becoming an issue) the technique probably has to be complemented by a more complex framework that (transparently) caches the generated programs in the database.
Apart from the simple example presented here, the technique was also explored and validated in other scenarios as well, e.g. code generation for business rules defined in a domain specific language (DSL) using XML notation.
As long as we limit ourselves to one-to-one mappings, another implementation making creative use of the new dynamic type facility is also feasible. The basic idea here is to create an intermediary structure type acting as switchboard between the source and target structures. It is created structurally identical to the former (and therefore can be filled from it via move), but uses component names from the latter for its fields, as defined by the mapping, thus allowing a move-corresponding between the two. An implementation based on this idea is shown below:
CLASS zcl_mapper_dyn_type DEFINITION.
PUBLIC SECTION.
INTERFACES: zif_mapper.
METHODS: constructor
IMPORTING
ir_filter TYPE REF TO data.
PRIVATE SECTION.
DATA: lr_filter TYPE REF TO data.
ENDCLASS.
CLASS zcl_mapper_factory_dyn_type DEFINITION INHERITING FROM
zcl_mapper_factory.
PUBLIC SECTION.
METHODS: create REDEFINITION.
PROTECTED SECTION.
METHODS: replace
IMPORTING
it_map TYPE field_mappings
iv_src TYPE string
RETURNING
value(rv_dst) TYPE string.
DATA: mv_count(3) TYPE n.
ENDCLASS.
CLASS zcl_mapper_dyn_type IMPLEMENTATION.
METHOD constructor.
lr_filter = ir_filter.
ENDMETHOD.
METHOD zif_mapper~map.
FIELD-SYMBOLS <filter> TYPE ANY.
ASSIGN lr_filter->* TO <filter>.
MOVE is_src TO <filter>.
MOVE-CORRESPONDING <filter> TO es_dst.
ENDMETHOD.
ENDCLASS.
CLASS zcl_mapper_factory_dyn_type IMPLEMENTATION.
METHOD create.
DATA lr_src_type TYPE REF TO cl_abap_structdescr.
DATA lt_src_components TYPE cl_abap_structdescr=>component_table.
DATA lr_filter_type TYPE REF TO cl_abap_structdescr.
DATA lr_filter TYPE REF TO data.
FIELD-SYMBOLS <component> TYPE abap_componentdescr.
lr_src_type ?= cl_abap_typedescr=>describe_by_data( is_src ).
lt_src_components = lr_src_type->get_components( ).
LOOP AT lt_src_components ASSIGNING <component>.
<component>-name = replace(
it_map = it_map
iv_src = <component>-name ).
ENDLOOP.
lr_filter_type = cl_abap_structdescr=>create( lt_src_components ).
CREATE DATA lr_filter TYPE HANDLE lr_filter_type.
CREATE OBJECT rr_mapper TYPE zcl_mapper_dyn_type
EXPORTING
ir_filter = lr_filter.
ENDMETHOD.
METHOD replace.
DATA ls_mapping TYPE field_mapping.
READ TABLE it_map WITH KEY src = iv_src INTO ls_mapping.
IF sy-subrc = 0.
rv_dst = ls_mapping-dst.
ELSE.
CONCATENATE '_' mv_count INTO rv_dst.
ADD 1 TO mv_count.
ENDIF.
ENDMETHOD.
ENDCLASS.