BTP - Create Entity for OData v4 with Draft
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.
Table of contents
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.