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:
We use Move-Corresponding, so we won´t need a Structure Mapping:
- 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)
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:
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
SAP Application Interface Framework Monitoring:
Business Partner created (Transaction BP):