Skip to Content

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.

A simple example

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 &lt;src&gt; type <xsl:value-of select="$SRC_TYPE"/>.

    field-symbols &lt;dst&gt; type <xsl:value-of select="$DST_TYPE"/>.

    assign is_src to &lt;src&gt;.

    assign es_dst to &lt;dst&gt;.

    <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">

    &lt;dst&gt;-<xsl:value-of select="DST"/> = &lt;src&gt;-<xsl:value-of select="SRC"/>.

  </xsl:template>



</xsl:transform>

    

   

The XSL transformation is quite straight forward, however a few points merit mentioning:

  • The primary purpose of XSLT is to transform one XML document (i.e. its DOM tree) into another, but it can be put to other uses as well. With the instruction <xsl:output type=”text” omit-xml-declaration=”yes”/> we tell the XSLT processor that our output will not necessarily be well-formed XML, and disable the output escaping of special characters like > that would occur otherwise. In addition, we suppress the inclusion of the mandatory <?xml version=”1.0″ encoding=”iso-8859-1″ ?> header line in the output.       
  • Whitespace handling in XSLT is a little tricky. The instruction <xsl:strip-space elements=”*”/> strips all extraneous whitespace (i.e. every text node that consists entirely of whitespace characters)  from the source tree. The same is true for the XSLT itself. If we specifically need to insert whitespace characters into the output we can enclose them in <xsl:text> elements which are exempt from this stripping.
  • We use top-level parameters to pass the type names of the structures to be mapped to our XSL transformation. This is an explicit way of providing well-defined pieces of information to the XSLT program.  

  

Concluding remarks

  

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.   

  

Addendum 

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.

To report this post you need to login first.

4 Comments

You must be Logged on to comment or reply to a post.

  1. Tobias Trapp

    Hi Achim,<br/><br/>this is in excellent blog. I have only one comment: For code generation the sap:concat statement is really useful to get readable code. In the above blog you could use:<br/><br/><sap:concat bol=”|” eol=”#” ><br/>  |class lcl_mapper definition.#<br/>  |  public section.#<br/>  |    interfaces zif_mapper.#<br/>  |endclass.#<br/></sap:concat><br/><br/>I used | as begin of line and # as end of line.<br/><br/>In fact this is how SAP generated code in NW4. Just watch out for the XSLT programs in the NW4 ABAP Test Drive.<br/><br/>Cheers,<br/>Tobias

    (0) 
    1. Markus Theilen
      Hi!

      Readable ABAP source code can also easily be produced by calling the PrettyPrinter function with the source string. The trick is to call “both” functions. I will post a code snippet tomorrow (not a work at the moment, so no access to the dev machine).

      (0) 
      1. Markus Theilen

        Hi again!The trick is, that one function only does the conversion of keywords to upper case (“HIKEY”) while the other one makes the correct indentions. All variables and parameters are tables of strings.

        (0) 
  2. Markus Theilen
    Hi!

    Thank you for this nice and informative blog. Using XSLT is a nice way to generate source code. I used this way in Design by Contract experiments to transform pre and post conditions in XML style into assert statements and was surprised how “easy” it is to use XSLT in ABAP. 🙂

    In another blog you mentioned the problem to generate global classes. Searching through the SEO* FuBas, I found a few pieces where theses FuBas are used. I will check them again tomorrow and post a few hints, if you like.

    Greetings, Markus

    (0) 

Leave a Reply