Wednesday 28 December 2022

How To Create A Rest Api With SAP Abap And Apply MVC1 Routing

In this blog post, I would like show how to create Rest api and how to apply MVC1 routing to handle different request simply from a controller class.

For that, first we will create handler and controller class for rest structure. Then we will add mvc1 controller class and model class to process business logic.

And finally we will create a service to handle rest requests.

At the end of the post, there are web browser, postman and abap consuming examples for the same rest api.

Let’s start.

Create following structures;

◉ ZREST_S_RESP_STATE
◉ ZREST_S_RESPONSE
◉ ZREST_S_REQUEST

◉ ZREST_S_RESP_STATE

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

◉ ZREST_S_RESPONSE

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

◉ ZREST_S_REQUEST

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

Now we will create classes.

Call Stack for a call

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

Classes

- ZREST_CL_DEFS
- ZREST_CL_MODEL
- ZREST_CL_REQ_CONTROLLER
- ZREST_CL_REQ_HTTP_HANDLER
- ZREST_CL_HTTP_HANDLER

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

CLASS zrest_cl_defs DEFINITION
  PUBLIC
  CREATE PUBLIC .

  PUBLIC SECTION.

    CONSTANTS c_state_success TYPE char1 VALUE 'S' ##NO_TEXT.
    CONSTANTS c_state_warning TYPE char1 VALUE 'W' ##NO_TEXT.
    CONSTANTS c_state_error TYPE char1 VALUE 'E' ##NO_TEXT.

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS ZREST_CL_DEFS IMPLEMENTATION.
ENDCLASS.
CLASS zrest_cl_model DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    METHODS get_datetime
      EXPORTING
        !response_body TYPE zrest_s_response-body
        !state         TYPE zrest_s_response-state .

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS ZREST_CL_MODEL IMPLEMENTATION.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZREST_CL_MODEL->GET_DATETIME
* +-------------------------------------------------------------------------------------------------+
* | [<---] RESPONSE_BODY                  TYPE        ZREST_S_RESPONSE-BODY
* | [<---] STATE                          TYPE        ZREST_S_RESPONSE-STATE
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD get_datetime.
    DATA: exref TYPE REF TO cx_root.
    TRY .

        TYPES : BEGIN OF ty_res,
                  datetime TYPE tzntimestp,
                END OF ty_res.

        DATA: res TYPE ty_res.
        res-datetime = sy-datum && sy-uzeit.

        response_body = /ui2/cl_json=>serialize( EXPORTING data = res ).

        state-state = zrest_cl_defs=>c_state_success.

      CATCH cx_root INTO exref.
        state-state = zrest_cl_defs=>c_state_error.
        state-state_text = exref->get_text( ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.
CLASS zrest_cl_req_controller DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    METHODS process_request
      IMPORTING
        !req_json      TYPE string
      EXPORTING
        !response_json TYPE string
        !response_stc  TYPE zrest_s_response .

  PROTECTED SECTION.
  PRIVATE SECTION.

    CONSTANTS:
    c_req_get_datetime TYPE zrest_e_req_id VALUE '1001'.

    METHODS conv_stc_to_json
      IMPORTING
                response_stc  TYPE zrest_s_response
      RETURNING VALUE(result) TYPE string.

ENDCLASS.

CLASS ZREST_CL_REQ_CONTROLLER IMPLEMENTATION.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZREST_CL_REQ_CONTROLLER->CONV_STC_TO_JSON
* +-------------------------------------------------------------------------------------------------+
* | [--->] RESPONSE_STC                   TYPE        ZREST_S_RESPONSE
* | [<-()] RESULT                         TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD conv_stc_to_json.
    DATA: exref TYPE REF TO cx_root.
    TRY .
        result = /ui2/cl_json=>serialize( EXPORTING data = response_stc ).
      CATCH cx_root.
        result = exref->get_text( ).
    ENDTRY.
  ENDMETHOD.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZREST_CL_REQ_CONTROLLER->PROCESS_REQUEST
* +-------------------------------------------------------------------------------------------------+
* | [--->] REQ_JSON                       TYPE        STRING
* | [<---] RESPONSE_JSON                  TYPE        STRING
* | [<---] RESPONSE_STC                   TYPE        ZREST_S_RESPONSE
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD process_request.
    DATA: exref TYPE REF TO cx_root.
    TRY .
        DATA req_stc TYPE zrest_s_request.
        /ui2/cl_json=>deserialize(
          EXPORTING
            json             = req_json
          CHANGING
            data             = req_stc
        ).

        IF req_stc-id IS NOT INITIAL.

          DATA(model) = NEW zrest_cl_model( ).

          IF req_stc-id EQ c_req_get_datetime.

            model->get_datetime(
              IMPORTING
                response_body = response_stc-body
                state         = response_stc-state
            ).

          ELSE.
            response_stc-state-state = zrest_cl_defs=>c_state_warning.
            MESSAGE s001(zrest_msg) INTO response_stc-state-state_text.
          ENDIF.

        ELSE.

          "Fill dummy content as sample
          req_stc-id = 999999.
          req_stc-body = 'Some Json Content'.
          response_stc-body = /ui2/cl_json=>serialize( EXPORTING data = req_stc ).

          response_stc-state-state = zrest_cl_defs=>c_state_warning.
          MESSAGE s002(zrest_msg) INTO response_stc-state-state_text.
        ENDIF.

        response_json = conv_stc_to_json( response_stc = response_stc ).

      CATCH cx_root.
        response_stc-state-state = zrest_cl_defs=>c_state_error.
        response_stc-state-state_text = exref->get_text( ).

        response_json = conv_stc_to_json( response_stc = response_stc ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.
CLASS zrest_cl_req_http_handler DEFINITION
  PUBLIC
  INHERITING FROM cl_rest_resource
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    METHODS if_rest_resource~get
        REDEFINITION .

    METHODS if_rest_resource~post
        REDEFINITION .

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS ZREST_CL_REQ_HTTP_HANDLER IMPLEMENTATION.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZREST_CL_REQ_HTTP_HANDLER->IF_REST_RESOURCE~GET
* +-------------------------------------------------------------------------------------------------+
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD if_rest_resource~get.

    DATA(req_json) = mo_request->get_uri_query_parameter( iv_name = 'req' iv_encoded = abap_false ).

    DATA(controller) = NEW zrest_cl_req_controller( ).
    controller->process_request(
      EXPORTING
        req_json      = req_json
      IMPORTING
        response_json = DATA(response_json)
    ).

    mo_response->create_entity( )->set_string_data( iv_data = response_json  ).
  ENDMETHOD.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZREST_CL_REQ_HTTP_HANDLER->IF_REST_RESOURCE~POST
* +-------------------------------------------------------------------------------------------------+
* | [--->] IO_ENTITY                      TYPE REF TO IF_REST_ENTITY
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD if_rest_resource~post.

    DATA(req_json) = mo_request->get_entity( )->get_string_data( ).

    DATA(controller) = NEW zrest_cl_req_controller( ).
    controller->process_request(
      EXPORTING
        req_json      = req_json
      IMPORTING
        response_json = DATA(response_json)
    ).

    mo_response->create_entity( )->set_string_data( iv_data = response_json  ).

  ENDMETHOD.
ENDCLASS.

CSRF is disabled in handler below. Disabling it from GUI Parameters of Service does not work. You need to implement HANDLE_CSRF_TOKEN in order to disable it for rest.

class ZREST_CL_HTTP_HANDLER definition
  public
  inheriting from CL_REST_HTTP_HANDLER
  create public .

public section.

  "Provides routing. Routing paths are assigned to controllers in this method
  methods IF_REST_APPLICATION~GET_ROOT_HANDLER
    redefinition .

protected section.

  "If you want to disable, redefine that method. Just as an empty method.
  methods HANDLE_CSRF_TOKEN
    redefinition .

  PRIVATE SECTION.
ENDCLASS.

CLASS ZREST_CL_HTTP_HANDLER IMPLEMENTATION.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Protected Method ZREST_CL_HTTP_HANDLER->HANDLE_CSRF_TOKEN
* +-------------------------------------------------------------------------------------------------+
* | [--->] IO_CSRF_HANDLER                TYPE REF TO IF_REST_CSRF_HANDLER
* | [--->] IO_REQUEST                     TYPE REF TO IF_REST_REQUEST
* | [--->] IO_RESPONSE                    TYPE REF TO IF_REST_RESPONSE
* +--------------------------------------------------------------------------------------</SIGNATURE>
  method HANDLE_CSRF_TOKEN.
*CALL METHOD SUPER->HANDLE_CSRF_TOKEN
*  EXPORTING
*    IO_CSRF_HANDLER =
*    IO_REQUEST      =
*    IO_RESPONSE     =
*    .
  endmethod.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZREST_CL_HTTP_HANDLER->IF_REST_APPLICATION~GET_ROOT_HANDLER
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RO_ROOT_HANDLER                TYPE REF TO IF_REST_HANDLER
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD if_rest_application~get_root_handler.

    "Provides routing.
    "Service path /sap/bc/rest
    "Sample URL http://vhcalnplci:8000/sap/bc/rest/zrest/Rest?sap-client=001&req={"ID":1001,"BODY":"Some Json Content"}
    DATA(root_handler) = NEW cl_rest_router( ).
    root_handler->attach(
      EXPORTING
        iv_template      = '/Rest'                 " Unified Name for Resources
        iv_handler_class = 'ZREST_CL_REQ_HTTP_HANDLER'                 " Object Type Name
    ).

*    "You can add more request handler classes
*    "Service path /sap/bc/rest
*    "Sample URL http://vhcalnplci:8000/sap/bc/rest/zrest/Rest2?sap-client=001&req={"ID":1001,"BODY":"Some Json Content"}
*    root_handler->attach(
*      EXPORTING
*        iv_template      = '/Rest2'                 " Unified Name for Resources
*        iv_handler_class = 'ZREST_CL_REQ_HTTP_HANDLER2'                 " Object Type Name
*    ).

    ro_root_handler = root_handler.
  ENDMETHOD.
ENDCLASS.
 
And final step, create a service.

Open SICF tcode and run.

Go to /sap/bc/rest and add new sub element

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

Add description and go to Handler List tab and our class, ZREST_CL_HTTP_HANDLER, as handler.

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

Activate Service. Right click service and click test. It will open browser. Change url to handle /Rest requests.

In my case, http://vhcalnplci:8000/sap/bc/rest/zrest/Rest

If you want to pass some params in get request, add query string parameters like ‘http://vhcalnplci:8000/sap/bc/rest/zrest/Rest?sap-client=001&req={“ID”:1001,”BODY”:”Some Json Content”}’

If you do not disable CSRF on handler, you will have issues calling non-get methods, such as POST methods from POSTMAN or from a client different than server itself.

Therefore in my example I have disabled CSRF.

Postman examples

Basic Authentication Parameters

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

Get Example and Result

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

Post Example and Result

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation

To Consume from Abap

We will use a HTTPClient to make call and we will parse json to abap structure with get_datetime example.

Http Client Code

CLASS zutil_cl_rest_ws DEFINITION
  PUBLIC
  CREATE PUBLIC .

  "Class wrapper for making Rest Web Service Calls
  PUBLIC SECTION.

    "Constants
    CONSTANTS : c_content_type_json   TYPE string VALUE 'application/json; charset=utf-8',
                c_content_type_xml    TYPE string VALUE 'application/xml; charset=utf-8',
                c_request_method_get  TYPE string VALUE 'GET',
                c_request_method_post TYPE string VALUE 'POST'.

    "Makes WS Call
    METHODS call_ws
      IMPORTING
        VALUE(i_url)            TYPE string
        VALUE(i_content_type)   TYPE string DEFAULT c_content_type_json
        VALUE(i_request_method) TYPE string DEFAULT c_request_method_get
        VALUE(i_username)       TYPE string OPTIONAL
        VALUE(i_password)       TYPE string OPTIONAL
        VALUE(i_payload)        TYPE string OPTIONAL
      EXPORTING
        VALUE(e_state)          TYPE zutil_cl_defs=>gty_state
        VALUE(e_response_str)   TYPE string.

  PROTECTED SECTION.

  PRIVATE SECTION.

    DATA http_client TYPE REF TO if_http_client.

    METHODS fill_warning
      RETURNING VALUE(state) TYPE zutil_cl_defs=>gty_state.

ENDCLASS.

CLASS ZUTIL_CL_REST_WS IMPLEMENTATION.

* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZUTIL_CL_REST_WS->CALL_WS
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_URL                          TYPE        STRING
* | [--->] I_CONTENT_TYPE                 TYPE        STRING (default =C_CONTENT_TYPE_JSON)
* | [--->] I_REQUEST_METHOD               TYPE        STRING (default =C_REQUEST_METHOD_GET)
* | [--->] I_USERNAME                     TYPE        STRING(optional)
* | [--->] I_PASSWORD                     TYPE        STRING(optional)
* | [--->] I_PAYLOAD                      TYPE        STRING(optional)
* | [<---] E_STATE                        TYPE        ZUTIL_CL_DEFS=>GTY_STATE
* | [<---] E_RESPONSE_STR                 TYPE        STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD call_ws.
    DATA: exref TYPE REF TO cx_root.
    TRY.

        "Using the this CREATE_BY_URL can simplify certain aspects of using this class
        FREE http_client.
        cl_http_client=>create_by_url(
          EXPORTING
            url    = i_url
          IMPORTING
            client = http_client
          EXCEPTIONS
            argument_not_found = 1
            plugin_not_active = 2
            internal_error    = 3
            OTHERS            = 4
          ).

        IF sy-subrc <> 0.
          e_state-state = fill_warning( ).
          RETURN.
        ENDIF.

        " My logic originally used PUT, but you should be able to change to POST
        http_client->request->set_method( i_request_method ).
        http_client->request->set_content_type( i_content_type ).


        " Remember to authenticate
        IF i_username IS NOT INITIAL OR i_password IS NOT INITIAL.
          http_client->authenticate(
            username = i_username
            password = i_password
          ).
        ENDIF.

        "If exists, prepare payload and assign
        IF i_payload IS NOT INITIAL.
          " Convert that payload to xstring.
          DATA lv_payload_x TYPE xstring.
          CALL FUNCTION 'SCMS_STRING_TO_XSTRING'
            EXPORTING
              text   = i_payload
            IMPORTING
              buffer = lv_payload_x
            EXCEPTIONS
              failed = 1
              OTHERS = 2.

          IF sy-subrc <> 0.
            e_state-state = zutil_cl_defs=>c_state_warning.
            e_state-state = 'Encoding error!'.
            RETURN.
          ELSE.
            http_client->request->set_data( lv_payload_x ).    " Binary data
          ENDIF.
        ENDIF.

        " Sending the request
        http_client->send(
            EXCEPTIONS
              http_communication_failure = 1
              http_invalid_state         = 2 ).

        IF sy-subrc <> 0.
          e_state-state = fill_warning( ).
          RETURN.
        ENDIF.

        " Receiving the response
        http_client->receive(
          EXCEPTIONS
            http_communication_failure = 1
            http_invalid_state         = 2
            http_processing_failed     = 3 ).

        IF sy-subrc <> 0.
          e_state-state = fill_warning( ).
          RETURN.
        ENDIF.

        " Check the response. Hopefully you get back a JSON response.
        e_response_str = http_client->response->get_cdata( ).
        IF e_response_str IS INITIAL.
          e_response_str = http_client->response->get_data( ).
        ENDIF.

        e_state-state = zutil_cl_defs=>c_state_success.
        e_state-state_text = 'Successfully Completed.'.

      CATCH cx_root INTO exref.
        e_state-state = zutil_cl_defs=>c_state_error.
        e_state-state_text = exref->get_text( ).
    ENDTRY.
  ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZUTIL_CL_REST_WS->FILL_WARNING
* +-------------------------------------------------------------------------------------------------+
* | [<-()] STATE                          TYPE        ZUTIL_CL_DEFS=>GTY_STATE
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD fill_warning.
    state-state = zutil_cl_defs=>c_state_warning.
    http_client->get_last_error(
            IMPORTING
              message        = DATA(msg)" Error Message
          ).
    state-state_text = msg.
  ENDMETHOD.
ENDCLASS.

Consumer Class Code. First unwraps the response structure and checks the state. if it is success then unwraps the body json part for result of call id 1001.

CLASS zrest_cl_consumer DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.

    METHODS get_datetime
      EXPORTING
        state TYPE zutil_cl_defs=>gty_state
        dt    TYPE tzntimestp.

  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS ZREST_CL_CONSUMER IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZREST_CL_CONSUMER->GET_DATETIME
* +-------------------------------------------------------------------------------------------------+
* | [<---] STATE                          TYPE        ZUTIL_CL_DEFS=>GTY_STATE
* | [<---] DT                             TYPE        TZNTIMESTP
* +--------------------------------------------------------------------------------------</SIGNATURE>
  METHOD get_datetime.
    "Sample method to consume rest web api
    DATA: exref TYPE REF TO cx_root.
    TRY.
        "
        CONSTANTS: c_uname TYPE string VALUE 'developer',
                   c_pass  TYPE string VALUE 'Down1oad'.

        "Build get call
        DATA: url TYPE string VALUE 'http://vhcalnplci:8000/sap/bc/rest/zrest/Rest?sap-client=001'.

        DATA req_stc TYPE zrest_s_request.
        req_stc-id = '1001'.
        DATA(req_json) = /ui2/cl_json=>serialize( EXPORTING data = req_stc ).
        url = url && '&req=' && req_json.

        "Call Web api
        NEW zutil_cl_rest_ws( )->call_ws(
          EXPORTING
            i_url            = url
            i_username       = c_uname
            i_password       = c_pass
          IMPORTING
            e_state          = state
            e_response_str   = DATA(json_response)
        ).

        IF state-state EQ zutil_cl_defs=>c_state_success.

          DATA: resp_stc TYPE zrest_s_response.
          /ui2/cl_json=>deserialize(
                    EXPORTING
                      json             = json_response
                    CHANGING
                      data             = resp_stc
                  ).

          IF resp_stc-state-state EQ zutil_cl_defs=>c_state_success.

            TYPES : BEGIN OF ty_res,
                      datetime TYPE tzntimestp,
                    END OF ty_res.
            DATA: resp_1001 TYPE ty_res.

            /ui2/cl_json=>deserialize(
                      EXPORTING
                        json             = resp_stc-body
                      CHANGING
                        data             = resp_1001
                    ).


            dt = resp_1001-datetime.
          ENDIF.
        ENDIF.

      CATCH cx_root INTO exref.
        state-state = zutil_cl_defs=>c_state_error.
        state-state_text = exref->get_text( ).
    ENDTRY.
  ENDMETHOD.
ENDCLASS.

SAP ABAP Development, SAP ABAP Career, SAP ABAP Skills, SAP ABAP Jobs, SAP ABAP Prep, SAP ABAP Preparation
Call Result

That is all. In that way, you can integrate any environment, system to SAP.

No comments:

Post a Comment