Skip to Content

Updates

  • 12.05.2017
    • added code to lock the data before comparing the time stamp of the last change date with the time stamp read from the etag in the update method.
  • 16.05.2017
    • fixed MPC_EXT code that creates the function import
    • added code to lock the data before comparing the time stamp of the last change date with the time stamp read from the etag in the function import

Introduction

SAP Gateway offers an out of the box support for etags if a property of an entity type is marked as an etag. When using the generic framework support the following happens when using eTags.

Before an update is performed the Gateway framework on the hub performs a READ request and compares the Etag send by the client with the Etag retrieved from the backend. The Etag handling offered by the framework does not require any coding but it requires that the GET_ENTITY method has to be implemented. The generic SAP Gateway framework support is offered for read, update and delete requests.

Instead of using the generic framework support it is possible to perform a code based implementation in the SAP Business Suite backend system.

This has some advantages since it allows the developer to lock the data while checking the etag before the update is performed. When using the out of the box framework support this is not possible and you cannot be 100% sure that another user has updated the data in the short timeframe between the read request of the framework and your update request.

The second advantage is that a code based implementation also offers etag support for function imports.

The handling of etags in the backend is available as of SAP Gateway 2.0 SP09 and SAP NetWeaver 740 SP08. You have to implement some methods where you have to add code to tell the framework that the data provider class has implemented ETag handling.

Prerequisites

This blog is based on an OData service that has been desrbied in my following blog:

OData service development with SAP Gateway using CDS via Referenced Data Sources – How to implement updates

What are etags?

Etags are usually timestamps that are updated when an entity is updated. For more complex scenarios however timestamps might not be the best choice and a hashtag is the better approach. How to implement a hash tag based etag has been described in the following blog https://blogs.sap.com/2016/03/18/maintain-data-concurrency-in-odata/ by https://people.sap.com/thiru.siva

 

How to annotate a property as an etag?

When doing code based development you can select the property that should be used as an etag on entity type level.

If you have used the referenced data source approach you have to perform these changes in the DEFINE method of the model provider extension class.

In the define method of the OData service that we used in the following blog

we will have to add to lines to set the property ‘LastChangedDateTime’ as an etag for the entity type   ‘Zsepm_C_Salesorder_TplType’.

 

  method define.

    data lo_entity_type type ref to /iwbep/if_mgw_odata_entity_typ.
    data lo_property    type ref to /iwbep/if_mgw_odata_property.

    super->define( ).

    lo_entity_type = model->get_entity_type( iv_entity_name = 'Zsepm_C_Salesorder_TplType' ).
    lo_property = lo_entity_type->get_property( iv_property_name = 'SalesOrder_Text' ).
    lo_property->set_updatable( abap_true ).

    lo_property = lo_entity_type->get_property( iv_property_name = 'LastChangedDateTime' ).
    lo_property->set_as_etag( ).

endmethod.

 

Testing the out of the box ETag framework support

After you have implemented these changes you will get the following error message when trying to perform an update with the http status code 428 Value “Precondition Required”.

<message xml:lang="en">The Data Service Request is required to be conditional. Try using the "If-Match" header.</message>

This check is done by the SAP Gateway framework on the hub.

We have to add an if-match HTTP header with a value like this:

W/”datetimeoffset’2017-05-05T09%3A15%3A05.6050000Z'”

In the Gateway Client you can easily add this header as shown in the following screen shot.

If we now perform the update request again we will receive an empty http response with the http returncode 204 which indicates that the update was successful.

If you try to perform the update without changing the value of the etag you will receive the error message Precondition Failed with an http return code 428.

Code based implementation of ETag support for updates

/IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_IS_CONDITIONAL_IMPLEMENTED

We first have to implement the method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_IS_CONDITIONAL_IMPLEMENTED by adding the following code

  method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_IS_CONDITIONAL_IMPLEMENTED.
* conditional handling set for Employee update and patch operations
  IF iv_entity_set_name EQ 'Zsepm_C_Salesorder_Tpl' AND
     ( iv_operation_type = /iwbep/if_mgw_appl_types=>gcs_operation_type-update_entity OR
       iv_operation_type = /iwbep/if_mgw_appl_types=>gcs_operation_type-patch_entity ).
    rv_conditional_active = abap_true.
  ELSE.
    rv_conditional_active = abap_false.
  ENDIF.
  endmethod.

Please note that the name of the enity set of our service that we have created using referenced data source and Service Builder is called ‘Zsepm_C_Salesorder_Tpl’. For other entity sets the framework would handle the Etag support.

Since the SAP Gatway Hub framework does now not perform the etag handling we have to add the follwing code into our update method because otherwise all updated request would be performed without checking the content of the etag.

Add Etag check to the update method

We now have to code to the update method that first checks whether the sales order has not changed by comparing the value of the last changed date with the value provided by the update request in the if-match header.

For this we use the get_conditional_info method of the  io_tech_request_context object. This method returns if_match as well as if_non_match headers in a deep structure that we have to evaluate.

In addition we are locking the sales order that we want to update before performing the update.

The lock is released if not updated is performed if the check of the etag fails.

If you add a break-point into the code you can see that a lock is aquired in transaction SM12.

Source code ZSEPM_C_SALESORD_UPDATE_ENTITY

    method ZSEPM_C_SALESORD_UPDATE_ENTITY.

    data: lt_keys               type /iwbep/t_mgw_tech_pairs,
          ls_key                type /iwbep/s_mgw_tech_pair,
          ls_so_id              type bapi_epm_so_id,
          ls_headerdata_update  type bapi_epm_so_header,
          ls_headerdatax        type bapi_epm_so_headerx,
          ls_headerdata_key     type zcl_ze2e100_xx_2_mpc=>ts_zsepm_c_salesorder_tpltype,
          ls_headerdata_payload type zcl_ze2e100_xx_2_mpc=>ts_zsepm_c_salesorder_tpltype,
          lt_return             type table of bapiret2,
          ls_return             type          bapiret2,
          err_msg               type          string,
          ls_message            type          scx_t100key.

* data definitions for etag handling
    data ls_conditions  type /iwbep/if_mgw_appl_types=>ty_s_conditions.
    data ls_etag           type /iwbep/if_mgw_appl_types=>ty_s_etag.
    data lv_matched        type abap_bool.
    data lv_etag_value type c length 22 .
    data ls_entity type sepm_isoe.

*       Lock argument for table SNWD_LOCK

    data: begin of ls_snwd_lock,
*            node_key(000032) type c,
            node_key type snwd_lock-NODE_KEY,
            bo_name          type snwd_lock-bo_name,
            bo_node_name     type snwd_lock-bo_node_name,
            client           type snwd_lock-client,
          end of ls_snwd_lock.

    data ls_so   type snwd_so.


*          " Initialization of lock argument:
    call 'C_ENQ_WILDCARD' id 'HEX0' field ls_snwd_lock.

*          " assign lock parameters to lock fields
    ls_snwd_lock-bo_name = 'IF_EPM_SO'.
    ls_snwd_lock-bo_node_name = 'IF_EPM_SO_HEADER'.
    ls_snwd_lock-client         = sy-mandt.


    call method io_tech_request_context->get_converted_keys
      importing
        es_key_values = ls_headerdata_key.


    " determine value of node_key.
    select single * from snwd_so
      where SO_ID = @ls_headerdata_key-salesorder
      into @ls_so.

    " add check whether product exist or not
    if sy-subrc <> 0.
      " implement suitable error handling here
      ls_message-msgid = 'SY'.
      ls_message-msgno = '002'.
      concatenate 'Update for ' ls_headerdata_key-salesorder ' failed' into ls_message-attr1.

      raise exception type /iwbep/cx_mgw_busi_exception
        exporting
          textid = ls_message.
    else.
      ls_snwd_lock-node_key = ls_so-node_key.
    endif.

* lock the sales order entity by enqueue lock
    call function 'ENQUEUE_EPM_LOCK'
      exporting
        mode_snwd_lock = 'E'
        node_key       = ls_snwd_lock-node_key
        bo_name        = ls_snwd_lock-bo_name
        bo_node_name   = ls_snwd_lock-bo_node_name
        client         = ls_snwd_lock-client
*       X_NODE_KEY     = ' '
*       X_BO_NAME      = ' '
*       X_BO_NODE_NAME = ' '
        _scope         = '2'
        _wait          = ' '
        _collect       = ' '
      exceptions
        foreign_lock   = 1
        system_failure = 2
        others         = 3.

    if sy-subrc <> 0.
      " implement suitable error handling here
      ls_message-msgid = 'SY'.
      ls_message-msgno = '002'.
      concatenate 'Could not aquire lock entry for ' ls_headerdata_key-salesorder into ls_message-attr1.

      raise exception type /iwbep/cx_mgw_busi_exception
        exporting
          textid = ls_message.
    endif.

* read the value of the timestamp from the database
    select single * from sepm_isoe into ls_entity where salesorder = ls_headerdata_key-salesorder.
    move ls_entity-lastchangeddatetime to lv_etag_value.

* read the etag value provided in the update request
    io_tech_request_context->get_conditional_info(
      importing
        es_conditions = ls_conditions
    ).

*check if both values match
    read table ls_conditions-if_match transporting no fields
        with key any_value = abap_true.

    if sy-subrc ne 0.
      loop at ls_conditions-if_match into ls_etag.
        read table ls_etag-tag_values transporting no fields
           with key name = 'LASTCHANGEDDATETIME' value = lv_etag_value.
        if sy-subrc eq 0.
          lv_matched = abap_true.
          exit.
        endif.
      endloop.
    else.
      lv_matched = abap_false.
    endif.


    if lv_matched eq abap_false.
* release the lock that we have aquired beforehand
      call function 'DEQUEUE_EPM_LOCK'
        exporting
          mode_snwd_lock = 'E'
          node_key       = ls_snwd_lock-node_key
          bo_name        = ls_snwd_lock-bo_name
          bo_node_name   = ls_snwd_lock-bo_node_name
          client         = ls_snwd_lock-client
          _scope         = '2'
        " _synchron      = space
          _collect       = 'X'
        exceptions
          others         = 3.

      if sy-subrc <> 0.
        " implement suitable error handling here
      endif.

      raise exception type /iwbep/cx_mgw_busi_exception
        exporting
          http_status_code = /iwbep/cx_mgw_busi_exception=>gcs_http_status_codes-precondition_failed
          textid           = /iwbep/cx_mgw_busi_exception=>precondition_failed.
    endif.


* perform the update

    io_data_provider->read_entry_data( importing es_data =  ls_headerdata_payload ).

    ls_so_id-so_id = ls_headerdata_key-salesorder.

    " Salesorder header data (non-key) fields that can be updated
    " via the BAPI are marked with an 'X'

    ls_headerdatax-so_id = ls_headerdata_key-salesorder.
    ls_headerdatax-note = 'X'.

    " move content of the fields that should be
    " updated from payload to the corresponding
    " field of the BAPI

    move ls_headerdata_key-salesorder to ls_headerdata_update-so_id.
    move ls_headerdata_payload-t_salesorder  to ls_headerdata_update-note.

    call function 'BAPI_EPM_SO_CHANGE'
      exporting
        so_id         = ls_so_id         " EPM: SO Id
        soheaderdata  = ls_headerdata_update " EPM: so header data of BOR object
        soheaderdatax = ls_headerdatax
      tables
        return        = lt_return.     " Return Parameter

    if lt_return is not initial.

      loop at lt_return into ls_return.
        err_msg = ls_return-message .
      endloop.

      ls_message-msgid = 'SY'.
      ls_message-msgno = '002'.
      ls_message-attr1 = err_msg.

      raise exception type /iwbep/cx_mgw_busi_exception
        exporting
          textid = ls_message.

    endif.


  endmethod.

 

Code based implementation of ETag support for function imports

Adding a function import

Since the service we are using has been generated using the referenced data source approach we cannot create a function import usingt the service builder. Reason is that the dialogue to create function imports does not find the entity types and entity sets of the CDS view.

We thus have to create a function import ‘ConfirmOrder’ by adding code to our DEFINE method of the modle provider extension class.

******************************************************************
*   ACTION - ConfirmOrder
******************************************************************
    data lo_action         type ref to /iwbep/if_mgw_odata_action.
    data lo_parameter      type ref to /iwbep/if_mgw_odata_parameter.

    lo_action = model->create_action( 'ConfirmOrder' ).
*Set return entity type
    lo_action->set_return_entity_type( 'Zsepm_C_Salesorder_TplType' ).
*Set HTTP method GET, PUT or POST
    lo_action->set_http_method( 'PUT' ).                    "#EC NOTEXT
* change 16.5.
*Set the action for entity type (not the entity set)
    lo_action->set_action_for( 'Zsepm_C_Salesorder_TplType' ).
* Set return type multiplicity
    lo_action->set_return_multiplicity( '0' ).

******************************************************************
* Parameters
******************************************************************

    lo_parameter = lo_action->create_input_parameter( iv_parameter_name = 'SalesOrder'    iv_abap_fieldname = 'SALESORDER' ).
    lo_parameter->/iwbep/if_mgw_odata_property~set_type_edm_string( ).
    lo_parameter->set_maxlength( iv_max_length = 10 ).
    lo_parameter->BIND_DATA_ELEMENT(
      exporting
        IV_ELEMENT_NAME             = 'SNWD_SO_ID'
        IV_BIND_CONVERSIONS         = ABAP_TRUE
    ).

Activate etag support through the backend

If you want to add Etag support for function modules you have to implement the following method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_IS_CONDI_IMPLE_FOR_ACTION. In this case the code will inform the framework that conditional handling is done by the service implementation in the backend for a function import ‘ConfirmOrder’.

method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_IS_CONDI_IMPLE_FOR_ACTION.

    if  iv_action_name = 'ConfirmOrder'.
      rv_conditional_active = abap_true.
    else.
      rv_conditional_active = abap_false.
    endif.

endmethod.

 

Implement function import and add etag support

By implementating the method /iwbep/if_mgw_appl_srv_runtime~execute_action we can add business logic to the function import.

Like in the update method code has beend added that locks the sales order before checking the etag. Only after having checked that the sales order. hasn’t been changed by another user  the status is updated.

 

    method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~EXECUTE_ACTION.

    data: ls_parameter type /iwbep/s_mgw_name_value_pair.
    data: lv_soid	type snwd_so_id.
    data: ls_so_id  type bapi_epm_so_id.
    data: lt_return type standard table of bapiret2, ls_return  type bapiret2.
    data: ls_headerdata type bapi_epm_so_header.
    data: ls_message           type          scx_t100key.
    data: ls_entity  type  sepm_isoe.
    types: begin of lty_import_parameter,
             salesorder type snwd_so_id,
           end of lty_import_parameter.
    data: ls_parameter_values     type lty_import_parameter,
          lv_function_import_name type /iwbep/mgw_tech_name.
    data: ls_conditions  type /iwbep/if_mgw_appl_types=>ty_s_conditions.
    data: ls_etag           type /iwbep/if_mgw_appl_types=>ty_s_etag.
    data: lv_matched        type abap_bool.
    data : lv_etag_value type c length 22 .


*       Lock argument for table SNWD_LOCK

    data: begin of ls_snwd_lock,
*            node_key(000032) type c,
            node_key type snwd_lock-NODE_KEY,
            bo_name          type snwd_lock-bo_name,
            bo_node_name     type snwd_lock-bo_node_name,
            client           type snwd_lock-client,
          end of ls_snwd_lock.

    data ls_so   type snwd_so.


    lv_function_import_name = io_tech_request_context->get_function_import_name( ).

    io_tech_request_context->get_converted_parameters(
      importing
        es_parameter_values = ls_parameter_values ).

    case lv_function_import_name.

      when 'ConfirmOrder'.

*          " Initialization of lock argument:
    call 'C_ENQ_WILDCARD' id 'HEX0' field ls_snwd_lock.

*          " assign lock parameters to lock fields
    ls_snwd_lock-bo_name = 'IF_EPM_SO'.
    ls_snwd_lock-bo_node_name = 'IF_EPM_SO_HEADER'.
    ls_snwd_lock-client         = sy-mandt.

 " determine value of node_key.
    select single * from snwd_so
      where SO_ID = @ls_parameter_values-SalesOrder
      into @ls_so.

    " add check whether product exist or not
    if sy-subrc <> 0.
      " implement suitable error handling here
      ls_message-msgid = 'SY'.
      ls_message-msgno = '002'.
      concatenate 'Update for ' ls_parameter_values-SalesOrder ' failed' into ls_message-attr1.

      raise exception type /iwbep/cx_mgw_busi_exception
        exporting
          textid = ls_message.
    else.
      ls_snwd_lock-node_key = ls_so-node_key.
    endif.

* lock the sales order entity by enqueue lock
    call function 'ENQUEUE_EPM_LOCK'
      exporting
        mode_snwd_lock = 'E'
        node_key       = ls_snwd_lock-node_key
        bo_name        = ls_snwd_lock-bo_name
        bo_node_name   = ls_snwd_lock-bo_node_name
        client         = ls_snwd_lock-client
*       X_NODE_KEY     = ' '
*       X_BO_NAME      = ' '
*       X_BO_NODE_NAME = ' '
        _scope         = '2'
        _wait          = ' '
        _collect       = ' '
      exceptions
        foreign_lock   = 1
        system_failure = 2
        others         = 3.

    if sy-subrc <> 0.
      " implement suitable error handling here
      ls_message-msgid = 'SY'.
      ls_message-msgno = '002'.
      concatenate 'Could not aquire lock entry for ' ls_parameter_values-SalesOrder into ls_message-attr1.

      raise exception type /iwbep/cx_mgw_busi_exception
        exporting
          textid = ls_message.
    endif.




        ls_so_id-so_id = ls_parameter_values-SalesOrder.

* add check for matching etags here
        select single * from sepm_isoe into ls_entity where salesorder = ls_so_id-so_id.
        move ls_entity-lastchangeddatetime to lv_etag_value.
        io_tech_request_context->get_conditional_info(
          importing
            es_conditions = ls_conditions
        ).
* check if both values match
        read table ls_conditions-if_match transporting no fields
               with key any_value = abap_true.
        if sy-subrc ne 0.
          loop at ls_conditions-if_match into ls_etag.
            read table ls_etag-tag_values transporting no fields
               with key name = 'LASTCHANGEDDATETIME' value = lv_etag_value.
            if sy-subrc eq 0.
              lv_matched = abap_true.
              exit.
            endif.
          endloop.
        else.
          lv_matched = abap_false.
        endif.


        if lv_matched eq abap_false.

* release the lock that we have aquired beforehand
      call function 'DEQUEUE_EPM_LOCK'
        exporting
          mode_snwd_lock = 'E'
          node_key       = ls_snwd_lock-node_key
          bo_name        = ls_snwd_lock-bo_name
          bo_node_name   = ls_snwd_lock-bo_node_name
          client         = ls_snwd_lock-client
          _scope         = '2'
        " _synchron      = space
          _collect       = 'X'
        exceptions
          others         = 3.

      if sy-subrc <> 0.
        " implement suitable error handling here
      endif.

          raise exception type /iwbep/cx_mgw_busi_exception
            exporting
              http_status_code = /iwbep/cx_mgw_busi_exception=>gcs_http_status_codes-precondition_failed
              textid           = /iwbep/cx_mgw_busi_exception=>precondition_failed.
        endif.


* perform change of so if etag check was successful


        call function 'BAPI_EPM_SO_CONFIRM'
          exporting
            so_id  = ls_so_id
*           persist_to_db = abap_true
          tables
            return = lt_return.


        if not lt_return[] is initial.
          me->/iwbep/if_sb_dpc_comm_services~rfc_save_log(
          exporting
* iv_entity_type = iv_entity_name
          it_return	= lt_return
          it_key_tab  = it_parameter ).

        else.



          select single * from sepm_isoe into ls_entity where salesorder = ls_so_id-so_id.

          if ls_entity is initial.
            ls_message-msgid = 'SY'.
            ls_message-msgno = '002'.
            ls_message-attr1 = 'sales order not found'.

            raise exception type /iwbep/cx_mgw_busi_exception
              exporting
                textid = ls_message.
          endif.

          copy_data_to_ref( exporting
          is_data = ls_entity changing
          cr_data = er_data
          ).

        endif.
    endcase.
  endmethod.

 

As a result we are now able to perform a confirmation of a sales order with optimistic locking by executing the following URI.

 

/sap/opu/odata/SAP/ZE2E100_XX_2_SRV/ConfirmOrder?SalesOrderID='0500000017'

 

To report this post you need to login first.

7 Comments

You must be Logged on to comment or reply to a post.

  1. Pavan Golesar

    Nice & Neat!

    Function import is as well covered here, This is all what I was looking for setting and get-going with eTag functionality.

     

    Thank you Andre again!

    –Pavan G

    (0) 
  2. Parul Garg

    Hello Andre,

    We are facing a similar situation in one of our clients.

    Does etag work for create_deep_entity??(we are using create_deep_entity to confirm work order/operation and do goods movement using a deep structure)

    I have implemented method /IWBEP/IF_MGW_APPL_SRV_RUNTIME~GET_IS_CONDITIONAL_IMPLEMENTED and set rv_conditional_active abap_true for operation type CD and XS .

    Thanks

    Parul

     

    (0) 
    1. Andre Fischer Post author

       

      Hi Parul,

      etags are only used for update scenarios. I you create a ressource that does not exist you don’t have to perform a check for the last change date of that ressource.

      Best Regards,

      Andre

       

       

      (0) 
  3. Kim Carmack

    Hi Andre,

    I have a question in regards to your reply to Parul.

     

    I wanted to enhance an existing custom OData app.  I was thinking of adding e-tag logic.

    However,  they implemented CREATE_DEEP_ENTITY for both “Create” and “Update” functions.

    They branch to each operation by inspecting the presence of the key field.  If null, then “Create”, else “Update”. The service doesn’t respond to “PUT” commands, only “POST” commands.

     

    Is it possible to implement the e-tag solution in this scenario?

     

    Thank you,

    Kimberly

    (0) 
    1. Andre Fischer Post author

       

      Hi Kimberly,

      what you are doing here is not part of the official OData V2 spec. But since the app is not following the standard you could enhance it by using your custom etag handling.

      The following code will search for a http header ‘myetag’.

      The variable myetag in the end contains the value that is sent in this header. In this example I used ‘hugo’.

      This is not an official etag handling but it might do what you are looking for.

      Kudos also go to Jonathan who posted this code three years ago in another thread.

      Regards,

      Andre

       

       

      DATA: lo_facade TYPE REF TO /iwbep/if_mgw_dp_int_facade,
              lt_request_headers TYPE tihttpnvp,
              ls_request_headers LIKE LINE OF lt_request_headers,
              myetag type string.
      
      * Read the HTTP Request headers.
        lo_facade ?= /iwbep/if_mgw_conv_srv_runtime~get_dp_facade( ).
        lt_request_headers = lo_facade->get_request_header( ).
      
      READ TABLE lt_request_headers with key name = 'myetag' INTO data(lv_request_header).
      
      if sy-subrc = 0 . "do something
       myetag = lv_request_header-value.
      endif.

       

      (0) 
  4. Kim Carmack

    Thank you for your feedback.  I will give this a try and post my results.

    And thank you for the insight to the SAP standard usage.

    Best Regards,

    Kimberly

     

    (0) 

Leave a Reply