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

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. Lars Hvam wrote a Blog Post about ABAP Swagger (here), so I tried to incorporate SAP Application Interface Framework into this Approach.

  • Basic SAP Application Interface Framework Knowledge
  • ABAP Swagger Classes implemented: See Blog Post

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%20Structure

Interface Structure

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

Interface%20Definition

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%20Configuration%20Group

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%20configuration

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 }\}|.

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

Test%20synchronous%20Posting%20of%20Business%20Partner

Test synchronous Posting of Business Partner

SAP Application Interface Framework Monitoring:

AIF%20Error%20Monitoring

SAP Application Interface Framework Error Monitoring

Business Partner created (Transaction BP):

In this Blog Post we can see how SAP Application Interface Framework Interface can be published asynchronously or synchronously as a REST Service including a Swagger UI Page generated from ABAP. Once implemented, the Classes can be cloned and adjusted to another Interface easily.

If you have any questions, let me know in the comments. Follow me for more SAP Application Interface Framework Content in the Future.