ABAP Development

Publish SAP Application Interface Framework Interfaces as REST API using ABAP Swagger

Introduction

Some time ago I needed to publish SAP Application Interface Framework Interfaces as REST APIs for a Customer SAP S/4HANA System, providing a Swagger Documentation and Testing Page.

Prerequisites

  • Basic SAP Application Interface Framework Knowledge
  • ABAP Swagger Classes implemented

Implementation Steps

SAP Application Interface Framework Configuration

Interface Definition

We want to create Business Partners using the BAPI BAPI_BUPA_CREATE_FROM_DATA. Therefore I defined a Z-Structure with the same Structures as in the BAPI:

Interface Structure

We use Move-Corresponding, so we won´t need a Structure Mapping:

Interface Definition
  • Application and Persistence Engine = XML (Standard is Proxy)

Action

For my Testcase I created an SAP Application Interface Framework Interface which creates Business Partners using the BAPI BAPI_BUPA_CREATE_FROM_DATA

Function Module:

FUNCTION zXXXXX_bupa_create .
*"----------------------------------------------------------------------
*"*"Lokale Schnittstelle:
*"  IMPORTING
*"     REFERENCE(TESTRUN) TYPE  C
*"     REFERENCE(SENDING_SYSTEM) TYPE  /AIF/AIF_BUSINESS_SYSTEM_KEY
*"       OPTIONAL
*"  TABLES
*"      RETURN_TAB STRUCTURE  BAPIRET2
*"  CHANGING
*"     REFERENCE(DATA) TYPE  ZXXXXX_BUPA
*"     REFERENCE(CURR_LINE)
*"     REFERENCE(SUCCESS) TYPE  /AIF/SUCCESSFLAG
*"     REFERENCE(OLD_MESSAGES) TYPE  /AIF/BAL_T_MSG
*"----------------------------------------------------------------------

  DATA: lv_bpartner TYPE bu_partner,
        ls_central  TYPE bapibus1006_central.

  CALL FUNCTION 'BAPI_BUPA_CREATE_FROM_DATA'
    EXPORTING
      partnercategory         = data-header-partn_cat
      partnergroup            = data-header-partn_grp
      centraldata             = data-central_data
      centraldataorganization = data-central_data_org
    IMPORTING
      businesspartner         = lv_bpartner
    TABLES
      return                  = return_tab
    .

  IF lv_bpartner IS NOT INITIAL.

    CALL FUNCTION '/AIF/UTIL_ADD_MSG'
      EXPORTING
        msgty      = 'S'
        msgid      = 'ZXXXXX'
        msgno      = '000'
        msgv1      = lv_bpartner
      TABLES
        return_tab = return_tab
      .
    IF sy-subrc <> 0.
* Implement suitable error handling here
    ENDIF.

  ENDIF.

ENDFUNCTION.

Runtime Configuration Group

  • Defined a custom active Runtime Configuration Group in Transaction /AIF/PERS_CGR (Active means that the Message is processed in the same Work Process when called. This enables us to process SAP Application Interface Framework Interfaces synchronously)
Runtime Configuration Group

We need to reference this Runtime Configuration Group when calling the SAP Application Interface Framework Interface, otherwise SAP Application Interface Framework will process the Message with the Standard Configuration Group.

REST API Implementation

I created 3 classes for this interface:

  • Ressource Class (contains the actual Ressources, in this case the Interface Call)
  • Swagger Class (contains the Swagger configuration)
  • SICF Handler Class (maintained for the Service in SICF)

Ressource Class

One example Method for the Ressource Class: POST_BUPA_S (Synchronous Call of BUPA Interface)

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZAIFBP_CL_REST_RSRC_BUPA->POST_BUPA_S
* +-------------------------------------------------------------------------------------------------+
* | [--->] IS_DATA                        TYPE        ZXXXXX_BUPA
* | [<-()] RT_RETURN                      TYPE        /SCMB/LOC_TT_BAPIMSG
* +--------------------------------------------------------------------------------------</SIGNATURE>  

METHOD post_bupa_s.

    DATA: lv_msgguid  TYPE /aif/sxmssmguid,
          lv_msgguid2 TYPE  guid_32,
          lv_status   TYPE /aif/proc_status,
          ls_return   TYPE bapiret2,
          lt_return   TYPE bapiret2_tab.

    TRY.
        CALL METHOD /aif/cl_enabler_xml=>transfer_to_aif
          EXPORTING
            is_any_structure = is_data
            iv_queue_ns      = 'XXXXX'
            iv_queue_name    = '001'
          IMPORTING
            ev_msgguid       = lv_msgguid.
      CATCH cx_root INTO DATA(lo_exc) ##CATCH_ALL.
        CALL FUNCTION 'RS_EXCEPTION_TO_BAPIRET2'
          EXPORTING
            i_r_exception = lo_exc
          CHANGING
            c_t_bapiret2  = lt_return.
    ENDTRY.

    IF lt_return IS INITIAL.
      APPEND 'Message successfully transferred to SAP Application Interface Framework' TO rt_return.
    ENDIF.

    SELECT status FROM /aif/std_idx_tbl INTO lv_status WHERE msgguid = lv_msgguid. ENDSELECT.

    DATA(lv_message) = 'GUID: ' && lv_msgguid && ', Status: ' && lv_status.

    CALL FUNCTION '/AIF/UTIL_ADD_MSG'
      EXPORTING
        msgty      = 'S'
        msgid      = 'XXXXX'
        msgno      = '001'
        msgv1      = lv_message
      TABLES
        return_tab = lt_return.

    LOOP AT lt_return ASSIGNING FIELD-SYMBOL(<ls_return>).
      APPEND <ls_return>-message TO rt_return.
    ENDLOOP.

    lv_msgguid2 = lv_msgguid.

    lt_return = zaifgl_cl_utility=>get_log_messages(
      EXPORTING
        iv_message_guid = lv_msgguid2
        iv_namespace    = 'XXXXX'
        iv_interface    = 'BUPA'
    ).

    LOOP AT lt_return ASSIGNING <ls_return>.
      APPEND <ls_return>-message TO rt_return.
    ENDLOOP.

  ENDMETHOD.

The Status is retrieved from the SAP Application Interface Framework Standard Index Table. This should be adapted if a custom Index table has been customized.

The method zaifgl_cl_utility=>get_log_messages uses the Standard Function Modules BAL_DB_SEARCH, BAL_DB_LOAD, BAL_LOG_MSG_READ to get the Log Messages of an SAP Application Interface Framework Message:

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZAIFGL_CL_UTILITY=>GET_LOG_MESSAGES
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_MESSAGE_GUID                TYPE        GUID_32
* | [--->] IV_NAMESPACE                   TYPE        /AIF/NS
* | [--->] IV_INTERFACE                   TYPE        /AIF/IFNAME
* | [<-()] RT_RETURN                      TYPE        BAPIRET2_TAB
* +--------------------------------------------------------------------------------------</SIGNATURE>

METHOD get_log_messages.

    DATA: ls_filter      TYPE bal_s_lfil,
          lt_log_header  TYPE balhdr_t,
          lt_msg_handle  TYPE bal_t_msgh,
          ls_msg         TYPE bal_s_msg,
          ls_object      TYPE bal_s_obj,
          ls_subobj      TYPE bal_s_sub,
          ls_extno       TYPE bal_s_extn,
          lv_handle_path TYPE string VALUE '(SAPLSBAL_DB)<G>-T_LDAT',
          ls_msg_handle  TYPE balmsghndl.

    FIELD-SYMBOLS: <lt_log_handle> TYPE ANY TABLE,
                   <lt_messages>   TYPE ANY TABLE.

    ls_object-sign = 'I'.
    ls_object-option = 'EQ'.
    ls_object-low = '/AIF/LOG'.
    APPEND ls_object TO ls_filter-object.
    ls_subobj-sign = 'I'.
    ls_subobj-option = 'EQ'.
    ls_subobj-low = '/AIF/NO_SUB_LOG'.
    APPEND ls_subobj TO ls_filter-subobject.
    ls_subobj-sign = 'I'.
    ls_subobj-option = 'EQ'.
    CONCATENATE iv_namespace iv_interface INTO ls_subobj-low SEPARATED BY space.
    APPEND ls_subobj TO ls_filter-subobject.
    ls_extno-sign = 'I'.
    ls_extno-option = 'EQ'.
    ls_extno-low = iv_message_guid.
    APPEND ls_extno TO ls_filter-extnumber.


    CALL FUNCTION 'BAL_DB_SEARCH'
      EXPORTING
        i_s_log_filter     = ls_filter
      IMPORTING
        e_t_log_header     = lt_log_header
      EXCEPTIONS
        log_not_found      = 1
        no_filter_criteria = 2
        OTHERS             = 3.
    IF sy-subrc <> 0.
    ENDIF.

    CALL FUNCTION 'BAL_DB_LOAD'
      EXPORTING
        i_t_log_header     = lt_log_header
      IMPORTING
        e_t_msg_handle     = lt_msg_handle
      EXCEPTIONS
        no_logs_specified  = 1
        log_not_found      = 2
        log_already_loaded = 3
        OTHERS             = 4.
    IF sy-subrc <> 0.
    ENDIF.

    "If Messages are already loaded (SAP Application Interface Framework Runtime)
    IF lt_msg_handle IS INITIAL.
      ASSIGN (lv_handle_path) TO <lt_log_handle>.    
      IF <lt_log_handle> IS ASSIGNED.
        LOOP AT <lt_log_handle> ASSIGNING FIELD-SYMBOL(<ls_log_handle>).
          ASSIGN COMPONENT 'LOG_HANDLE' OF STRUCTURE <ls_log_handle> TO FIELD-SYMBOL(<lv_log_handle>).
          IF <lv_log_handle> IS ASSIGNED.
            ls_msg_handle-log_handle = <lv_log_handle>.
            ASSIGN COMPONENT 'MESSAGES-T_MHDR' OF STRUCTURE <ls_log_handle> TO <lt_messages>.
            IF <lt_messages> IS ASSIGNED.
              LOOP AT <lt_messages> ASSIGNING FIELD-SYMBOL(<ls_messages>). "#EC CI_NESTED
                ASSIGN COMPONENT 'MSGNUMBER' OF STRUCTURE <ls_messages> TO FIELD-SYMBOL(<lv_message>).
                IF <lv_message> IS ASSIGNED.
                  ls_msg_handle-msgnumber = <lv_message>.
                  INSERT ls_msg_handle INTO TABLE lt_msg_handle.
                ENDIF.
              ENDLOOP.
            ENDIF.
          ENDIF.
        ENDLOOP.
      ENDIF.
    ENDIF.

    LOOP AT lt_msg_handle ASSIGNING FIELD-SYMBOL(<ls_msg_handle>).
      CALL FUNCTION 'BAL_LOG_MSG_READ'
        EXPORTING
          i_s_msg_handle = <ls_msg_handle>
        IMPORTING
          e_s_msg        = ls_msg
        EXCEPTIONS
          log_not_found  = 1
          msg_not_found  = 2
          OTHERS         = 3.
      IF sy-subrc <> 0.
      ENDIF.

      IF ls_msg IS NOT INITIAL.
        CALL FUNCTION '/AIF/UTIL_ADD_MSG'
          EXPORTING
            msgty              = ls_msg-msgty
            msgid              = ls_msg-msgid
            msgno              = ls_msg-msgno
            msgv1              = ls_msg-msgv1
            msgv2              = ls_msg-msgv2
            "IMPORTING
            "RETURN                     = RT_RETURN
          TABLES
            return_tab         = rt_return
          EXCEPTIONS
            max_errors_reached = 1
            OTHERS             = 2.
        IF sy-subrc <> 0.
        ENDIF.
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

Swagger Class

Now in the Swagger Class we need to register the Service. in our example we will have the endpoint “/sap/zrest/bupa”

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZAIFBP_CL_REST_SWAG_BUPA->IF_HTTP_EXTENSION~HANDLE_REQUEST
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER                         TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>

 METHOD IF_HTTP_EXTENSION~HANDLE_REQUEST.

    DATA: lo_swag TYPE REF TO zcl_swag.

    CREATE OBJECT lo_swag
      EXPORTING
        io_server = server
        iv_base   = '/sap/zrest/bupa'
        iv_title  = 'BUPA'.
    lo_swag->register( me ).

    lo_swag->run( ).

  ENDMETHOD.

For every implemented Method in the Ressource Class we need to add a Line in the Meta Table of the Swagger Class (here we have 5 Methods defined)

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZAIFBP_CL_REST_SWAG_BUPA->ZIF_SWAG_HANDLER~META
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RT_META                        TYPE        ZAIFGL_TT_REST_META
* +--------------------------------------------------------------------------------------</SIGNATURE> 

 METHOD zif_swag_handler~meta.

    FIELD-SYMBOLS: <ls_meta> LIKE LINE OF rt_meta.


    APPEND INITIAL LINE TO rt_meta ASSIGNING <ls_meta>.
    <ls_meta>-summary   = 'Get Bupa'(001).
    <ls_meta>-url-regex = '/get_bupa$'.
    <ls_meta>-method    = zcl_swag=>gc_method-get.
    <ls_meta>-handler   = 'GET_BUPA'.
    <ls_meta>-response_settings-remove_data_object = abap_true.

    APPEND INITIAL LINE TO rt_meta ASSIGNING <ls_meta>.
    <ls_meta>-summary   = 'Get Status'(002).
    <ls_meta>-url-regex = '/get_status$'.
    <ls_meta>-method    = zcl_swag=>gc_method-get.
    <ls_meta>-handler   = 'GET_STATUS'.
    <ls_meta>-response_settings-remove_data_object = abap_true.

    APPEND INITIAL LINE TO rt_meta ASSIGNING <ls_meta>.
    <ls_meta>-summary   = 'Get Log Messages'(003).
    <ls_meta>-url-regex = '/get_log_messages$'.
    <ls_meta>-method    = zcl_swag=>gc_method-get.
    <ls_meta>-handler   = 'GET_LOG'.
    <ls_meta>-response_settings-remove_data_object = abap_true.

    APPEND INITIAL LINE TO rt_meta ASSIGNING <ls_meta>.
    <ls_meta>-summary   = 'Post Business Partner'(004).
    <ls_meta>-url-regex = '/post_bupa$'.
    <ls_meta>-method    = zcl_swag=>gc_method-post.
    <ls_meta>-handler   = 'POST_BUPA'.
    <ls_meta>-response_settings-remove_data_object = abap_true.

        APPEND INITIAL LINE TO rt_meta ASSIGNING <ls_meta>.
    <ls_meta>-summary   = 'Post Business Partner synchron'(004).
    <ls_meta>-url-regex = '/post_bupa_s$'.
    <ls_meta>-method    = zcl_swag=>gc_method-post.
    <ls_meta>-handler   = 'POST_BUPA_S'.
    <ls_meta>-response_settings-remove_data_object = abap_true.


  ENDMETHOD.

The Method of the Swagger Class (zaifbp_cl_rest_swag_bupa) will always call the corresponding Ressource Class (zaifbp_cl_rest_rsrc_bupa):

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZAIFBP_CL_REST_SWAG_BUPA->POST_BUPA_S
* +-------------------------------------------------------------------------------------------------+
* | [--->] IS_DATA                        TYPE        ZXXXXX_BUPA
* | [<-()] RT_RETURN                      TYPE        /SCMB/LOC_TT_BAPIMSG
* +--------------------------------------------------------------------------------------</SIGNATURE> 

 METHOD post_bupa_s.

    DATA: lo_bupa TYPE REF TO zaifbp_cl_rest_rsrc_bupa.

    CREATE OBJECT lo_bupa.

    rt_return = lo_bupa->post_bupa_s( is_data ).

  ENDMETHOD.

SICF Class

The SICF Class calls the Swagger Class if the Endpoint is correct:

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZAIFBP_CL_REST_SICF_BUPA->IF_HTTP_EXTENSION~HANDLE_REQUEST
* +-------------------------------------------------------------------------------------------------+
* | [--->] SERVER                         TYPE REF TO IF_HTTP_SERVER
* +--------------------------------------------------------------------------------------</SIGNATURE>
 
 METHOD if_http_extension~handle_request.

    DATA: lv_path TYPE string,
          lv_name TYPE string,
          li_http TYPE REF TO if_http_extension.


    lv_path = server->request->get_header_field( '~path' ).

    IF lv_path CP '/sap/zrest/bupa/*'.
      CREATE OBJECT li_http TYPE zaifbp_cl_rest_swag_bupa.
      li_http->handle_request( server ).
    ELSE.
      FIND REGEX '/sap/zrest/bupa/static/(.*)'
        IN lv_path
        SUBMATCHES lv_name ##NO_TEXT.

      IF lv_name IS INITIAL.
        lv_name = 'index.html' ##NO_TEXT.
      ENDIF.

      read_mime( ii_server = server
                 iv_url    = lv_name ).
    ENDIF.

  ENDMETHOD.

We also need the correct URL in Method “read_mime”

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Protected Method ZAIFBP_CL_REST_SICF_BUPA->READ_MIME
* +-------------------------------------------------------------------------------------------------+
* | [--->] II_SERVER                      TYPE REF TO IF_HTTP_SERVER
* | [--->] IV_URL                         TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>

  METHOD read_mime.

  DATA: li_api  TYPE REF TO if_mr_api,
          lv_data TYPE xstring,
          lv_mime TYPE string,
          lv_url  TYPE string.


    CONCATENATE '/SAP/PUBLIC/zrest/bupa/' iv_url INTO lv_url.

    li_api = cl_mime_repository_api=>if_mr_api~get_api( ).

    li_api->get(
      EXPORTING
        i_url       = lv_url
      IMPORTING
        e_content   = lv_data
        e_mime_type = lv_mime
      EXCEPTIONS
        not_found   = 1 ).
    IF sy-subrc = 1.
      ii_server->response->set_cdata( '404' ).
      ii_server->response->set_status( code = 404 reason = '404' ).
      RETURN.
    ENDIF.

    ii_server->response->set_compression( ).
    ii_server->response->set_content_type( lv_mime ).
    ii_server->response->set_data( lv_data ).

  ENDMETHOD.

Don´t forget to add the Class as Service Handler in SICF:

SICF configuration

Code Changes ABAP Swagger

Added Class and Method zaifgl_cl_json=>pretty_name

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZAIFGL_CL_JSON=>PRETTY_NAME
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_IN                          TYPE        STRING
* | [<-()] RV_OUT                         TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>

METHOD pretty_name.

    DATA: tokens TYPE TABLE OF char128.
    FIELD-SYMBOLS: <token> LIKE LINE OF tokens.

    rv_out = iv_in.

    TRANSLATE rv_out TO LOWER CASE.
    TRANSLATE rv_out USING `/_:_~_`.
    SPLIT rv_out AT `_` INTO TABLE tokens.
    DELETE tokens WHERE table_line IS INITIAL.
    LOOP AT tokens ASSIGNING <token> FROM 2.
      TRANSLATE <token>(1) TO UPPER CASE.
    ENDLOOP.

    CONCATENATE LINES OF tokens INTO rv_out.

  ENDMETHOD.

Method zcl_swag_map_type->map_structure
added Pretty Print for the Fields

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Protected Method ZCL_SWAG_MAP_TYPE->MAP_STRUCTURE
* +-------------------------------------------------------------------------------------------------+
* | [--->] IO_TYPEDESCR                   TYPE REF TO CL_ABAP_TYPEDESCR
* | [<-()] RV_TYPE                        TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>

*old
*    LOOP AT lt_components ASSIGNING <ls_component>.
*      lv_index = sy-tabix.
*
*      ASSERT NOT <ls_component>-name IS INITIAL.
*
*      lv_type = map_internal( <ls_component>-type ).
*      rv_type = rv_type && '"' && <ls_component>-name && '":{ ' && lv_type && ' }'.
*
*      IF lv_index <> lines( lt_components ).
*        rv_type = rv_type && ','.
*      ENDIF.
*    ENDLOOP.

    "new (with pretty print)
    DATA lv_name_pretty TYPE string.

    LOOP AT lt_components ASSIGNING <ls_component>.
      lv_index = sy-tabix.

      ASSERT NOT <ls_component>-name IS INITIAL.


      lv_name_pretty = zaifgl_cl_json=>pretty_name( iv_IN = <ls_component>-name ).

      lv_type = map_internal( <ls_component>-type ).
      rv_type = rv_type && '"' && lv_name_pretty && '":{ ' && lv_type && ' }'.

      IF lv_index <> lines( lt_components ).
        rv_type = rv_type && ','.
      ENDIF.
    ENDLOOP.

    "/new

Method zcl_swag_spec->response

only return the Type in Response

"old
      "lv_string = |"{ is_meta-meta-handler }_Response":\{"type": "object","properties": \{"DATA": \{{ lv_type }\}\}\}|.
      "new:
      lv_string = |"{ is_meta-meta-handler }_Response": \{{ lv_type }\}|.

Test

Calling the endpoint /sap/zrest/bupa/swagger.html

Test synchronous Posting of Business Partner

SAP Application Interface Framework Monitoring:

SAP Application Interface Framework Error Monitoring

Business Partner created (Transaction BP):