Sunday 12 May 2019

How to implement a custom entity in the ABAP RESTful Programming Model using remote function modules

Introduction


In this blog I want to show how to build and implement a custom entity ZCE_Product_via_RFC whose query is being implemented via a class zcl_cq_product_via_rfc that reads the data from a remote system via RFC using the function modules

◈ BAPI_EPM_PRODUCT_GET_LIST
◈ BAPI_EPM_PRODUCT_GET_DETAIL

The class only others one method select that is called for both queries and GET requests for single products.

BAPI_EPM_PRODUCT_GET_LIST

FUNCTION bapi_epm_product_get_list
  IMPORTING
    VALUE(max_rows) TYPE bapi_epm_max_rows OPTIONAL
  TABLES
    headerdata LIKE bapi_epm_product_header OPTIONAL
    selparamproductid LIKE bapi_epm_product_id_range OPTIONAL
    selparamsuppliernames LIKE bapi_epm_supplier_name_range OPTIONAL
    selparamcategories LIKE bapi_epm_product_categ_range OPTIONAL
    return LIKE bapiret2 OPTIONAL.

BAPI_EPM_PRODUCT_GET_DETAIL

FUNCTION bapi_epm_product_get_detail
  IMPORTING
    VALUE(product_id) TYPE bapi_epm_product_id
  EXPORTING
    VALUE(headerdata) TYPE bapi_epm_product_header
  TABLES
    conversion_factors LIKE bapi_epm_product_conv_factors OPTIONAL
    return LIKE bapiret2 OPTIONAL.

Please note that both function modules need the structure BAPI_EPM_PRODUCT_HEADER.

Step 1: Create a class


We start our implementation by creating a class zcl_cq_product_via_rfc that implements the method select of the interface if_a4c_rap_query_provider.

We will continue with the implementation later on.

CLASS zcl_cq_product_via_rfc DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .
  PUBLIC SECTION.
    INTERFACES if_a4c_rap_query_provider.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zcl_cq_product_via_rfc IMPLEMENTATION.
  METHOD if_a4c_rap_query_provider~select.

  ENDMETHOD.
ENDCLASS.

Step 2: Create custom entity


In ABAP in Eclipse we create a custom entity by creating a new Data Definition. In the wizard you can select the default template or start with the template for a custom entity with parameters.

SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Tutorials and Materials

In either case you have to change the coding such that it looks like follows. Please notice that the custom entity does not take any parameters and that we have added the root Statement.

@EndUserText.label: 'product demo data read via rfc from on prem'
@QueryImplementedBy: 'zcl_cq_product_via_rfc'

define root custom entity ZCE_Product_via_RFC
{
...
}

With the Annotation @QueryImplementedBy we have to provide the name of the class that has been created in step 1 which implements the select method of the interface if_a4c_rap_query_provider.

The tricky part is now the creation of the DDL source code of our custom entity. Since there is no design time support available for this yet I have developed a class that takes the name of the structure BAPI_EPM_PRODUCT_HEADER that you can run via F9.

In this case where we want to create the custom entity in the SAP CP ABAP Environment System you have to run the class in the backend system where the RFC function module is being called since the structure BAPI_EPM_PRODUCT_HEADER is not available in the SAP CP ABAP Environment system.

In future versions of SAP S/4HANA it is planned to have the ABAP RESTful programming model available so that in this case the report can be run in the same system.

When running the class via F9 we get the following output in the console.

SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Tutorials and Materials

We can take the code and copy and paste it in the our custom entity.

In addition we will add some UI annotations.

@EndUserText.label: 'product demo data read via rfc from on prem'
@QueryImplementedBy: 'zcl_cq_product_via_rfc'

@UI: {
  headerInfo: {
  typeName: 'Product',
  typeNamePlural: 'Products'
  }
}

define root custom entity ZCE_Product_via_RFC
{

      @UI.facet     : [
        {
          id        :       'Product',
          purpose   :  #STANDARD,
          type      :     #IDENTIFICATION_REFERENCE,
          label     :    'Product',
          position  : 10 }
      ]
      // DDL source code for custom entity for BAPI_EPM_PRODUCT_HEADER
      // generated on: 20190214 at:142338
      @UI           : {
      lineItem      : [{position: 10, importance: #HIGH}],
      identification: [{position: 10}],
      selectionField: [{position: 10}]
      }
  key ProductId     : abap.char( 10 );
      TypeCode      : abap.char( 2 );
      @UI           : {
      lineItem      : [{position: 20, importance: #HIGH}],
      identification: [{position: 20}],
      selectionField: [{position: 20}]
      }
      Category      : abap.char( 40 );
      @UI           : {
      lineItem      : [{position: 30, importance: #HIGH}],
      identification: [{position: 30}]
      }
      Name          : abap.char( 255 );
      @UI           : {
      identification: [{position: 40}]
      }
      Description   : abap.char( 255 );
      SupplierId    : abap.char( 10 );
      SupplierName  : abap.char( 80 );
      TaxTarifCode  : abap.int1;
      @Semantics.unitOfMeasure: true
      MeasureUnit   : abap.unit( 3 );
      @Semantics.quantity.unitOfMeasure: 'WeightUnit'
      WeightMeasure : abap.quan( 13, 3 );
      @Semantics.unitOfMeasure: true
      WeightUnit    : abap.unit( 3 );
      @UI           : {
      lineItem      : [{position: 50, importance: #HIGH}],
      identification: [{position: 50}]
      }
      Price         : abap.dec( 23, 4 );
      @Semantics.currencyCode: true
      CurrencyCode  : abap.cuky( 5 );
      @Semantics.quantity.unitOfMeasure: 'DimUnit'
      Width         : abap.quan( 13, 3 );
      @Semantics.quantity.unitOfMeasure: 'DimUnit'
      Depth         : abap.quan( 13, 3 );
      @Semantics.quantity.unitOfMeasure: 'DimUnit'
      Height        : abap.quan( 13, 3 );
      @Semantics.unitOfMeasure: true
      DimUnit       : abap.unit( 3 );
      ProductPicUrl : abap.char( 255 );

}

Step 3: Implement query


When implementing the query you have to know that the implementation class only offers one method which is called select.

The code first checks if data is being requested or not.

io_request->is_data_requested( ).

Since the same method is used for queries and single selects we have to find out whether a single select has been performed, that means whether a call like the following has been sent by the OData client:

/…/Products(‘HT-1000’)

In this case the table lt_filter_cond will only contain one entry for the key field PRODUCTID.

Since the structure BAPIRET2 is not yet on the whitelist of released structures I have used the same report mentioned above to generate a type definition ty_bapiret2.

It is important to note that you must return the number of entries found, also if a single request is returned because otherwise no data will be shown in the object page. This is done by the statement:

io_response->set_total_number_of_records( lines( lt_product ) ).

The data both for the single read as well as for queries is returned as a internal table lt_return to the framework.

io_response->set_data( lt_product ).


CLASS zcl_cq_product_via_rfc DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_a4c_rap_query_provider.
  PROTECTED SECTION.
  PRIVATE SECTION.
ENDCLASS.

CLASS zcl_cq_product_via_rfc IMPLEMENTATION.
  METHOD if_a4c_rap_query_provider~select.

    "variables needed to call BAPI's
    DATA lt_product TYPE STANDARD TABLE OF  zce_product_via_rfc.
    DATA ls_product TYPE zce_product_via_rfc.

    "key for BAPI_GET_DETAIL
    TYPES : BEGIN OF product_rfc_key_type,
              productid TYPE zce_product_via_rfc-productid,
            END OF product_rfc_key_type.
    DATA ls_product_rfc_key TYPE product_rfc_key_type.

    "select options
    DATA lt_filter_ranges_productid TYPE RANGE OF zce_product_via_rfc-productid.
    DATA ls_filter_ranges_productid LIKE LINE OF lt_filter_ranges_productid.
    DATA lt_filter_ranges_supplier  TYPE RANGE OF zce_product_via_rfc-suppliername.
    DATA ls_filter_ranges_supplier  LIKE LINE OF lt_filter_ranges_supplier.
    DATA lt_filter_ranges_category  TYPE RANGE OF zce_product_via_rfc-category.
    DATA ls_filter_ranges_category  LIKE LINE OF lt_filter_ranges_category.

    "######################### ABAP source code ################################
    " ABAP source code for type definition for BAPIRET2
    " generated on: 20190301 at: 165321 in: UIA
    TYPES : BEGIN OF ty_bapiret2,
              type      TYPE c LENGTH 1,
              id        TYPE c LENGTH 20,
              number    TYPE n LENGTH 3,
              message   TYPE c LENGTH 220,
              logno     TYPE c LENGTH 20,
              logmsgno  TYPE n LENGTH 6,
              messagev1 TYPE c LENGTH 50,
              messagev2 TYPE c LENGTH 50,
              messagev3 TYPE c LENGTH 50,
              messagev4 TYPE c LENGTH 50,
              parameter TYPE c LENGTH 32,
              row       TYPE i,
              field     TYPE c LENGTH 30,
              system    TYPE c LENGTH 10,
            END OF ty_bapiret2.

    "DATA lt_return   TYPE STANDARD TABLE OF bapiret2.
    DATA lt_return   TYPE STANDARD TABLE OF ty_bapiret2.
    "variables generic for implementation of custom entity
    DATA lv_details_read TYPE abap_bool.
    "DATA ls_sel_opt TYPE /iwbep/s_cod_select_option.

*   ensure: in case of a single record is requested (e.g. data for a detail page),
*           only one record is returned and SET_TOTAL_NUMBER_OF_RECORDS = 1

    TRY.
        DATA(lo_rfc_dest) = cl_rfc_destination_provider=>create_by_cloud_destination(
                                 i_name = |S4H_ON_PREM_RFC|
                                 i_service_instance_name = |OutboundCommunication| ).
        DATA(lv_rfc_dest_name) = lo_rfc_dest->get_destination_name( ).

      CATCH cx_rfc_dest_provider_error INTO DATA(lx_dest).

    ENDTRY.

    TRY.

        IF io_request->is_data_requested( ).

          "get and add filter
          DATA(lt_filter_cond) = io_request->get_filter_conditions( ).
          DATA(ls_paging)      = io_request->get_paging( ).
          DATA(lt_fields)      = io_request->get_requested_elements( ).
          DATA(lt_sort)        = io_request->get_sort_elements( ).

          "check if the request is a single read
          READ TABLE lt_filter_cond WITH KEY element = 'PRODUCTID' INTO DATA(ls_productid_filter_key).
          IF sy-subrc = 0 AND lines( ls_productid_filter_key-option ) = 1.
            READ TABLE ls_productid_filter_key-option INTO DATA(ls_id_option) INDEX 1.
            IF sy-subrc = 0 AND ls_id_option-sign = 'I' AND ls_id_option-option = 'EQ' AND ls_id_option-low IS NOT INITIAL.
              "read details for single record in list

              ls_product_rfc_key-productid = ls_id_option-low.

              CALL FUNCTION 'BAPI_EPM_PRODUCT_GET_DETAIL'
                DESTINATION lv_rfc_dest_name
                EXPORTING
                  product_id = ls_product_rfc_key
                IMPORTING
                  headerdata = ls_product
                TABLES
                  return     = lt_return.

              APPEND ls_product TO lt_product.
              lv_details_read = abap_true.

            ENDIF.
          ENDIF.

          "the request is a GET_LIST request and a filter has been provided
          IF lv_details_read = abap_false  AND lt_filter_cond IS NOT INITIAL.

            "-get filter for ProductID
            READ TABLE lt_filter_cond WITH TABLE KEY element = 'PRODUCTID' INTO DATA(ls_productid_cond).
            IF sy-subrc EQ 0.
              LOOP AT ls_productid_cond-option INTO DATA(ls_sel_opt_productid).
                MOVE-CORRESPONDING ls_sel_opt_productid TO ls_filter_ranges_productid.
                INSERT ls_filter_ranges_productid INTO TABLE lt_filter_ranges_productid.
              ENDLOOP.
            ENDIF.

            "-get filter for SUPPLIERNAME
            READ TABLE  lt_filter_cond WITH TABLE KEY element = 'SUPPLIERNAME' INTO DATA(ls_suppliername_cond).
            IF sy-subrc EQ 0.
              LOOP AT ls_suppliername_cond-option INTO DATA(ls_sel_opt_suppliername).
                MOVE-CORRESPONDING ls_sel_opt_suppliername TO ls_filter_ranges_supplier.
                INSERT ls_filter_ranges_supplier INTO TABLE lt_filter_ranges_supplier.
              ENDLOOP.
            ENDIF.

            "-get filter for CATEGORY
            READ TABLE  lt_filter_cond WITH TABLE KEY element = 'CATEGORY' INTO DATA(ls_category_cond).
            IF sy-subrc EQ 0.
              LOOP AT ls_category_cond-option INTO DATA(ls_sel_opt_category).
                MOVE-CORRESPONDING ls_sel_opt_category TO ls_filter_ranges_category.
                INSERT ls_filter_ranges_category INTO TABLE lt_filter_ranges_category.
              ENDLOOP.
            ENDIF.


            CALL FUNCTION 'BAPI_EPM_PRODUCT_GET_LIST'
              DESTINATION lv_rfc_dest_name
*          EXPORTING
*            max_rows              =
              TABLES
                headerdata            = lt_product
                selparamproductid     = lt_filter_ranges_productid
                selparamsuppliernames = lt_filter_ranges_supplier
                selparamcategories    = lt_filter_ranges_category
                return                = lt_return.
          ELSE.
            "raise exception that no filter has been provided
          ENDIF.

          "we have to set the number of returned entries to avoid an error when lines( lt_product ) = 1
          "IF io_request->is_total_rec_number_requested( ).
          io_response->set_total_number_of_records( lines( lt_product ) ).
          "ENDIF.

          io_response->set_data( lt_product ).

        ELSE.
          "no data has been requested
        ENDIF.

        "error handling
      CATCH cx_a4c_rap_query_provider INTO DATA(lx_exc).


    ENDTRY.

  ENDMETHOD.
ENDCLASS.

Step 4: Create Service Definition and Service Binding


We can now create a Service definition

@EndUserText.label: 'Read product demo data via RFC'
define service ZSD_PRODUCT_VIA_RFC {
  expose ZCE_Product_via_RFC;

and a service binding.

SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Tutorials and Materials

Result


Using the preview functionality

SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Tutorials and Materials

we can see that the app supports filtering as indicated by the @UI annotations.

SAP ABAP Certifications, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Tutorials and Materials

No comments:

Post a Comment