This is a test message to test the length of the message box.
Login
BTP Create Entity OData v4 with Draft
Created by Software-Heroes

BTP - Create Entity for OData v4 with Draft

437

This article is about the flow for creating and activating an instance in the BTP from on-premise, where the endpoint is an OData version 4 that supports draft.

In a current project we want to call an endpoint in the BTP (ABAP Environment) from an on-premise system to create a new entity. This is an OData version 4 and the endpoint works with Draft. It takes a few calls to create and activate a new entry in the entity so that it becomes visible in the application.

 

Introduction

What exactly is this article about? The OData endpoint is a RAP application that provides an API to enable the creation of new entries in the ABAP environment. We cannot use events and the event mesh here because we need identification from the new instance in order to make it available to the on-premise application, so we need a synchronous interface.

 

Flow

So before we go into the details of the process, here is the entire flow summarized in a graphic. For more details, simply click on the graphic and zoom in. In the following sections we will then carry out the different calls and explain the corresponding benefits.

 

 

Destination

However, before we can make calls, we need a destination or RFC connection in SM59. For this we need our endpoint on the BTP and access data in the form of basic authentication (user and password) or an OAuth endpoint. In our example, we use Basic Authentication to log in to the endpoint. To do this, we create a connection of type "G", the target is the account of the ABAP Environment "<SUBACCOUNT-ID>.abap.eu10.hana.ondemand.com", you should enter your address here.

 

Then enter the user and password under Login & Security and activate SSL in the lower part.

 

Preperation

HTTP Client

First, we need an HTTP client to be able to perform queries. To do this, we create a client based on the connection created and save the instance in a member variable of the class. We will then activate the acceptance of cookies as we will carry out a few more requests.

cl_http_client=>create_by_destination( EXPORTING destination = 'TEST_xxx'
                                       IMPORTING client      = mo_http_client ).
                                       
mo_http_client->propertytype_accept_cookie = if_http_client=>co_enabled.

 

Send and receive

We also use an auxiliary method to send the request and receive the response. We will call this method again and again in the following sections and thus save ourselves some coding.

METHOD process_send_and_receive.
  mo_http_client->send( EXCEPTIONS http_communication_failure = 1
                                   http_invalid_state         = 2
                                   http_processing_failed     = 3
                                   http_invalid_timeout       = 4
                                   OTHERS                     = 5 ).
  IF sy-subrc <> 0.
  ENDIF.

  mo_http_client->receive( EXCEPTIONS http_communication_failure = 1
                                      http_invalid_state         = 2
                                      http_processing_failed     = 3
                                      OTHERS                     = 4 ).
  IF sy-subrc <> 0.
  ENDIF.
ENDMETHOD.

 

In addition, we define a constant in the class that contains the basic path to the OData endpoint; there should be a slash at the end.

CONSTANTS c_root_path TYPE string VALUE `/sap/opu/odata4/sap/<SERVICE>/srvd_a2x/sap/<NAME>/0001/`.

 

POST Request

Since our POST requests are always structured quite similarly, we define a method that we will use again and again in the following processing and that provides us with a flexible interface. The definition of the types looks like this:

TYPES:
  td_process TYPE char15,

  BEGIN OF ts_answer,
    code     TYPE i,
    reason   TYPE string,
    location TYPE string,
    content  TYPE string,
    action   TYPE string,
  END OF ts_answer,

  BEGIN OF ts_post_request,
    process  TYPE td_process,
    uri      TYPE string,
    payload  TYPE string,
    if_match TYPE string,
  END OF ts_post_request.


METHODS process_post
  IMPORTING is_post_request  TYPE ts_post_request
  RETURNING VALUE(rs_result) TYPE ts_answer
  RAISING   cx_t100_msg.

 

For logging, there is a process ID in "TS_POST_REQUEST" to make it easier to assign the error message. The URI contains the requested endpoint and the data would be in the payload as JSON. The IF_MATCH field later sets a header in the request. The implementation of the method looks like this:

METHOD process_post.
  DATA(ld_uri) = |{ c_root_path }{ is_post_request-uri }|.

  mo_http_client->request->set_method( if_http_request=>co_request_method_post ).
  mo_http_client->request->set_header_field( name = 'x-csrf-token' value = ms_auth-token ).
  mo_http_client->request->set_header_field( name = '~request_uri' value = ld_uri ).
  mo_http_client->request->set_header_field( name = 'Content-Type' value = 'application/json' ).

  IF is_post_request-if_match IS NOT INITIAL.
    mo_http_client->request->set_header_field( name = 'If-Match' value = is_post_request-if_match ).
  ENDIF.

  IF is_post_request-payload IS NOT INITIAL.
    mo_http_client->request->set_cdata( is_post_request-payload ).
  ENDIF.

  process_send_and_receive( is_post_request-process ).

  mo_http_client->response->get_status( IMPORTING code   = rs_result-code
                                                  reason = rs_result-reason ).
  rs_result-content = mo_http_client->response->get_cdata( ).
ENDMETHOD.

 

The following steps are carried out in the method:

  • Generation of the target URI
  • Setting the header information (especially important is setting the token)
  • Takeover of the payload
  • Sending and receiving the request
  • Reading the status and content

 

Token

Before we can send our first POST request against the endpoint, we need a valid X-CSRF token and a valid SAP session on the ABAP environment. To do this, we set the corresponding header field to “fetch”. We assemble the URI to be called from root and entity name and also pass it to the header.

mo_http_client->request->set_header_field( name = 'x-csrf-token' value = 'fetch' ).
mo_http_client->request->set_header_field( name = '~request_uri' value = |{ c_root_path }Entity| ).

process_send_and_receive( ).

ms_auth-token = mo_http_client->response->get_header_field( 'x-csrf-token' ).

 

Finally, we execute the request and get the corresponding token from the response header, which we store in an attribute in the class because we need it for further requests.

 

Create

As a next step, we want to create a new entry in the entity. To do this, we execute our first POST against the interface. First we need to convert the payload to JSON, for this we use a structure that corresponds to the target type, but be careful because the CamelCase notation is not used here, all field labels remain small. We then start the system using the PROCESS_POST method for our target entity (see URI).

DATA(ld_payload) = NEW /ui2/cl_abap2json( )->struc2json( is_payload ).

DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-create
                                                           uri     = `Entity`
                                                           payload = ld_payload ) ).

IF ls_answer-code = 201.
  ms_auth-location = mo_http_client->response->get_header_field( 'location' ).

ELSE.
  " Error Handling
ENDIF.

 

The server should report us an HTTP status "201 Created", then there is a new entry in the draft table of the entity on the BTP. In the header fields we also receive the necessary information in the “location” field to work with the new entity. We also include the location in our class attribute because we need it for the next requests.

 

Actions

Before we can carry out the actions, we first need the relevant information from the metadata of the OData service. To do this, call the endpoint with the addition $metadata and search for Activate in the XML file. We receive our defined actions accordingly.

 

For further processing we need Prepare, Activate and Discard. The type is equally important, otherwise the action will not be found when executed.

 

Prepare

The next step is the prepare phase. We build the URI using the saved location, followed by the action with the type. You have to replace the placeholder and, if necessary, the version with your data. The request is made again via a POST request to the endpoint. It is also important in this step to include the "IF-MATCH" header. Here you have the option of including the ETag, in this case we only have one instance and include a star.

DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Prepare|.

DATA(ls_answer) = process_post( is_post_request = VALUE #( process  = cs_process-prepare
                                                           uri      = ld_uri
                                                           if_match = '*' ) ).

CASE ls_answer-code.
  WHEN 200 OR 204.
    " Success message

  WHEN OTHERS.
    add_error_to_log( ls_answer ).
    discard_draft( ).

    " Error Handling

ENDCASE.

 

You can find how to handle the error in the “Discard” section, where we will go into the mapping again.

 

Activate

If the prepare phase has been completed so far without errors, the Activate can be carried out. In this phase, the validations in the RAP are run on the BTP and error messages may come back that prevent the data record from being activated. The “IF-MATCH” header is also important in this step.

DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Activate|.

DATA(ls_answer) = process_post( is_post_request = VALUE #( process  = cs_process-activate
                                                           uri      = ld_uri
                                                           if_match = '*' ) ).

CASE ls_answer-code.
  WHEN 200 OR 204.
    rs_result = ls_answer.

    " Success message

  WHEN OTHERS.
    add_error_to_log( ls_answer ).
    discard_draft( ).

    " Error Handling

ENDCASE.

 

The source code is similar to Prepare, the same error handling occurs at the end of processing. If we receive an HTTP status "200 OK", the corresponding data about the entity is also in the body of the response. We can now derive the new key from these. In addition, a piece of source code to convert the JSON string into an internal structure.

DATA ls_entity TYPE ts_entity.

/ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
                           CHANGING  data = ls_entity ).

rd_result = ls_entity-key.

 

Discard

Delete

If errors occur during the Prepare or Activate steps, we don't want to leave the "half" instance standing, but rather remove it from the draft table. To do this, we cannot work with a DELETE, but must trigger the “Discard” action for the data record.

DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Discard|.

DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-discard
                                                           uri     = ld_uri ) ).

CASE ls_answer-code.
  WHEN 200 OR 204.
    " Success message

  WHEN OTHERS.
    " Error Handling

ENDCASE.

 

Finally, we get the HTTP status “200 OK” and the instance has been removed from the draft table.

 

Error handling

In case of an error, the body can also be parsed to extract the error messages. As a first step, let's take a look at a complex message.

 

The message in the upper area is the main message for the error, the details area lists the other messages that were generated during validation. Accordingly, a type is created for parsing the message.

TYPES:
  BEGIN OF ts_detail,
    code    TYPE string,
    message TYPE string,
  END OF ts_detail,
  tt_detail TYPE STANDARD TABLE OF ts_detail WITH EMPTY KEY,

  BEGIN OF ts_error,
    code    TYPE string,
    message TYPE string,
    details TYPE tt_detail,
  END OF ts_error,

  BEGIN OF ts_message,
    error TYPE ts_error,
  END OF ts_message.

 

In the last step, we parse the JSON into an internal structure and can then transfer the messages to our log object or output them in another way. The TODOs are marked in the source code and must be implemented accordingly.

DATA ls_message TYPE ts_message.

/ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
                           CHANGING  data = ls_message ).

" TODO: Log ls_message-error-message

LOOP AT ls_message-error-details INTO DATA(ls_detail).
  " TODO: Log ls_detail-message
ENDLOOP.

 

Complete example

As always, here is the complete example of this article. This time the example is very long and will not work without adjustments. First of all, you need a connection to the cloud and then a corresponding endpoint as OData v4.

CLASS zcl_24bs_create_review DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun.

  PRIVATE SECTION.
    TYPES:
      td_process TYPE char15,

      BEGIN OF ts_answer,
        code     TYPE i,
        reason   TYPE string,
        location TYPE string,
        content  TYPE string,
        action   TYPE string,
      END OF ts_answer,

      BEGIN OF ts_auth,
        token    TYPE string,
        location TYPE string,
      END OF ts_auth,

      BEGIN OF ts_post_request,
        process  TYPE td_process,
        uri      TYPE string,
        payload  TYPE string,
        if_match TYPE string,
      END OF ts_post_request,

      BEGIN OF ts_payload,
        field1 TYPE string,
        field2 TYPE string,
        field3 TYPE string,
      END OF ts_payload,

      BEGIN OF ts_detail,
        code    TYPE string,
        message TYPE string,
      END OF ts_detail,
      tt_detail TYPE STANDARD TABLE OF ts_detail WITH EMPTY KEY,

      BEGIN OF ts_error,
        code    TYPE string,
        message TYPE string,
        details TYPE tt_detail,
      END OF ts_error,

      BEGIN OF ts_message,
        error TYPE ts_error,
      END OF ts_message,

      BEGIN OF ts_entity,
        key            TYPE char32,
        status         TYPE char2,
        isactiveentity TYPE abap_bool,
      END OF ts_entity.

    CONSTANTS c_root_path   TYPE string VALUE `/sap/opu/odata4/sap/<SERVICE>/srvd_a2x/sap/<NAME>/0001/`. " TODO
    CONSTANTS c_destination TYPE string VALUE 'TEST_xxx'.                                                " TODO

    CONSTANTS:
      BEGIN OF cs_process,
        token    TYPE td_process VALUE 'TOKEN',
        create   TYPE td_process VALUE 'CREATE_ENTITY',
        prepare  TYPE td_process VALUE 'PREPARE',
        activate TYPE td_process VALUE 'ACTIVATE',
        discard  TYPE td_process VALUE 'DISCARD',
      END OF cs_process.

    DATA mo_http_client TYPE REF TO if_http_client.
    DATA ms_auth        TYPE ts_auth.

    METHODS create_token_and_cookie
      RAISING cx_t100_msg.

    METHODS create_entity
      IMPORTING is_payload TYPE ts_payload
      RAISING   cx_t100_msg.

    METHODS process_send_and_receive
      IMPORTING id_process TYPE td_process
      RAISING   cx_t100_msg.

    METHODS process_post
      IMPORTING is_post_request  TYPE ts_post_request
      RETURNING VALUE(rs_result) TYPE ts_answer
      RAISING   cx_t100_msg.

    METHODS prepare
      RAISING cx_t100_msg.

    METHODS activate
      RETURNING VALUE(rs_result) TYPE ts_answer
      RAISING   cx_t100_msg.

    METHODS discard_draft
      RAISING cx_t100_msg.

    METHODS extract_key_from_content
      IMPORTING is_answer        TYPE ts_answer
      RETURNING VALUE(rd_result) TYPE char32.

    METHODS add_error_to_log
      IMPORTING is_answer TYPE ts_answer.
ENDCLASS.


CLASS zcl_24bs_create_review IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
    CLEAR ms_auth.
    cl_http_client=>create_by_destination( EXPORTING destination = CONV char20( c_destination )
                                           IMPORTING client      = mo_http_client ).

    mo_http_client->propertytype_accept_cookie = if_http_client=>co_enabled.

    TRY.
        create_token_and_cookie( ).
        create_entity( VALUE #( ) ).
        prepare( ).
        DATA(ls_activated_entity) = activate( ).

        DATA(ld_new_key) = extract_key_from_content( ls_activated_entity ).

      CATCH cx_t100_msg.
        " TODO: Error Handling
    ENDTRY.

    mo_http_client->close( ).

    " TODO: Set returning values
  ENDMETHOD.


  METHOD create_token_and_cookie.
    mo_http_client->request->set_header_field( name = 'x-csrf-token' value = 'fetch' ).
    mo_http_client->request->set_header_field( name = '~request_uri' value = |{ c_root_path }Review| ).

    process_send_and_receive( cs_process-token ).

    ms_auth-token = mo_http_client->response->get_header_field( 'x-csrf-token' ).
  ENDMETHOD.


  METHOD create_entity.
    DATA(ld_payload) = NEW /ui2/cl_abap2json( )->struc2json( is_payload ).

    DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-create
                                                               uri     = `Entity`
                                                               payload = ld_payload ) ).

    IF ls_answer-code = 201.
      ms_auth-location = mo_http_client->response->get_header_field( 'location' ).
    ELSE.
      " TODO: Error Handling
    ENDIF.
  ENDMETHOD.


  METHOD process_post.
    DATA(ld_uri) = |{ c_root_path }{ is_post_request-uri }|.

    mo_http_client->request->set_method( if_http_request=>co_request_method_post ).
    mo_http_client->request->set_header_field( name = 'x-csrf-token' value = ms_auth-token ).
    mo_http_client->request->set_header_field( name = '~request_uri' value = ld_uri ).
    mo_http_client->request->set_header_field( name = 'Content-Type' value = 'application/json' ).

    IF is_post_request-if_match IS NOT INITIAL.
      mo_http_client->request->set_header_field( name = 'If-Match' value = is_post_request-if_match ).
    ENDIF.

    IF is_post_request-payload IS NOT INITIAL.
      mo_http_client->request->set_cdata( is_post_request-payload ).
    ENDIF.

    process_send_and_receive( is_post_request-process ).

    mo_http_client->response->get_status( IMPORTING code   = rs_result-code
                                                    reason = rs_result-reason ).
    rs_result-content = mo_http_client->response->get_cdata( ).
  ENDMETHOD.


  METHOD prepare.
    DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Prepare|.

    DATA(ls_answer) = process_post( is_post_request = VALUE #( process  = cs_process-prepare
                                                               uri      = ld_uri
                                                               if_match = '*' ) ).

    CASE ls_answer-code.
      WHEN 200 OR 204.
        " TODO: Success message

      WHEN OTHERS.
        add_error_to_log( ls_answer ).
        discard_draft( ).

        " TODO: Error Handling

    ENDCASE.
  ENDMETHOD.


  METHOD activate.
    DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Activate|.

    DATA(ls_answer) = process_post( is_post_request = VALUE #( process  = cs_process-activate
                                                               uri      = ld_uri
                                                               if_match = '*' ) ).

    CASE ls_answer-code.
      WHEN 200 OR 204.
        rs_result = ls_answer.

        " TODO: Success message

      WHEN OTHERS.
        add_error_to_log( ls_answer ).
        discard_draft( ).

        " TODO: Error Handling

    ENDCASE.
  ENDMETHOD.


  METHOD discard_draft.
    DATA(ld_uri) = |{ ms_auth-location }/com.sap.gateway.srvd_a2x.<NAME>.v0001.Discard|.

    DATA(ls_answer) = process_post( is_post_request = VALUE #( process = cs_process-discard
                                                               uri     = ld_uri ) ).

    CASE ls_answer-code.
      WHEN 200 OR 204.
        " TODO: Success message

      WHEN OTHERS.
        " TODO: Error Handling

    ENDCASE.
  ENDMETHOD.


  METHOD process_send_and_receive.
    mo_http_client->send( EXCEPTIONS http_communication_failure = 1
                                     http_invalid_state         = 2
                                     http_processing_failed     = 3
                                     http_invalid_timeout       = 4
                                     OTHERS                     = 5 ).
    IF sy-subrc <> 0.
      " TODO: Error Handling
    ENDIF.

    mo_http_client->receive( EXCEPTIONS http_communication_failure = 1
                                        http_invalid_state         = 2
                                        http_processing_failed     = 3
                                        OTHERS                     = 4 ).
    IF sy-subrc <> 0.
      " TODO: Error Handling
    ENDIF.
  ENDMETHOD.


  METHOD extract_key_from_content.
    DATA ls_entity TYPE ts_entity.

    /ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
                               CHANGING  data = ls_entity ).

    rd_result = ls_entity-key.
  ENDMETHOD.


  METHOD add_error_to_log.
    DATA ls_message TYPE ts_message.

    /ui2/cl_json=>deserialize( EXPORTING json = is_answer-content
                               CHANGING  data = ls_message ).

    " TODO: Log ls_message-error-message

    LOOP AT ls_message-error-details INTO DATA(ls_detail).
      " TODO: Log ls_detail-message
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

 

Learnings

Unfortunately, at the time of implementation there was no source at SAP that described exactly how to do something like this and also not all the special features that need to be taken into account. Here is a brief summary and the learnings of the connection:

  • Activation of cookies in the HTTP client (ACCEPT_COOKIE)
  • Pay attention to the RAP flow (Prepare -> Activate)
  • Determining the correct names of the actions
  • Delete the instance in case of error (Discard)
  • Use of the IF-MATCH header for the draft instance

 

Conclusion

Implementing such a feature is not that difficult once you have solved all the problems. But since there are currently no such concrete examples, the research can be time-consuming. Once you understand how it works, further implementations should no longer be a problem and you should be prepared for the next requests.


Included topics:
BTPODatav4POSTDraftCreate Entity
Comments (0)



And further ...

Are you satisfied with the content of the article? We post new content in the ABAP area every Friday and irregularly in all other areas. Take a look at our tools and apps, we provide them free of charge.


RAP - Deep Action in OData v4

Category - ABAP

In this article we will look at actions with deep structures, how we can create them and pass data to an API endpoint.

05/24/2024

BTP - Business Configuration (Adaptation)

Category - ABAP

What do adjustments to the RAP business object actually look like in the context of the business configuration? More information in this article.

05/17/2024

BTP - Business Configuration (without Transport)

Category - ABAP

How do you actually use the Business Configuration without the transport recording? In this article we clarify whether it is possible.

05/07/2024

BTP - Business Configuration (Usage)

Category - ABAP

Today it's about using the various apps and transporting the business configuration in the ABAP environment.

05/03/2024

BTP - Business Configuration (Creation)

Category - ABAP

This article is about creating maintenance views in the ABAP environment to maintain data and later transport the settings.

04/26/2024