ABAP RESTful Application Programming Model, SAP ABAP Development, Cloud Integration, SAP BTP

Calling ABAP on Cloud Trial V4 ODATA from SAP S4 HANA On-premise Using ABAP – SM59

As a ABAP developer, creating and deploying your application in SAP BTP Platform is now very easy. Use of ABAP Restful application Programming model makes this tasks very easy. More over there is a huge demand now to deploy your application in ABAP on Cloud and to consume it in your On-premise applications and enhancements.

Introduction

This approach actually helps a ABAP developer to create more reusable content across different landscapes and for SAP Partners / Vendors – this can increase the reusability and easy plug and play of your functionality.

ABAP Cloud RAP development

Create a easy application in ABAP on Cloud environment.

Tables –

@EndUserText.label : 'Country Specific Material Tax'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zmaterial_tax {

  key client            : abap.clnt not null;
  key uuid              : sysuuid_x16 not null;
  matnr                 : abap.char(40) not null;
  land                  : abap.char(3) not null;
  matnrtax              : abap.char(4) not null;
  local_created_by      : abp_creation_user;
  local_created_at      : abp_creation_tstmpl;
  local_last_changed_by : abp_locinst_lastchange_user;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
  last_changed_at       : abp_lastchange_tstmpl;

}

@EndUserText.label : 'Draft table for Material Tax'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zmatnr_draft {

  key client         : abap.clnt not null;
  key uuid           : sysuuid_x16 not null;
  matnr              : abap.char(40) not null;
  land               : abap.char(3) not null;
  matnrtax           : abap.char(4) not null;
  locallastchangedat : abp_locinst_lastchange_tstmpl;
  "%admin"           : include sych_bdl_draft_admin_inc;

}

CDS Entity –

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Material Details'
define root view entity ZCDS_I_MATERIAL 
    as select from zmaterial_tax
//composition of target_data_source_name as _association_name
{
    key uuid,
        matnr,
        land,
        matnrtax,
        local_last_changed_at as LocalLastChangedAt
}

Create a Projection View –

@EndUserText.label: 'Projection View'
@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
define root view entity ZCDS_P_MATERIAL
provider contract transactional_query as projection on ZCDS_I_MATERIAL
{
    key uuid,
        matnr,
        land,
        matnrtax,
        LocalLastChangedAt
}

Create Behavior Definitions in Managed Scenario –

managed implementation in class zbp_cds_i_material unique;

//strict ( 1 );
with draft;

define behavior for ZCDS_I_MATERIAL alias MaterialTax
persistent table zmaterial_tax
draft table zmatnr_draft
lock master
total etag LocalLastChangedAt
authorization master ( global )
etag master LocalLastChangedAt
{
  field ( numbering : managed, readonly ) uuid;
  field ( mandatory ) matnr, land;
  field ( readonly ) LocalLastChangedAt;
  create;
  update;
  delete;
}

projection;
//strict ( 1 ); //Uncomment this line in order to enable strict mode 2. The strict mode has two variants (strict(1), strict(2)) and is prerequisite to be future proof regarding syntax and to be able to release your BO.
use draft;

define behavior for ZCDS_P_MATERIAL alias MaterialTax
{
  use create;
  use update;
  use delete;
}

You can also have some metadata extensions –

@Metadata.layer: #CORE
@UI:{
headerInfo:{typeName:'Material',
typeNamePlural:'Materials',
title:{type:#STANDARD,label:'Material Import Tax',value:'matnr'}},
presentationVariant:[{sortOrder:[{by:'matnr',direction:#DESC}]}]}
annotate view ZCDS_P_MATERIAL with
{
  @UI.facet:[{id:'matnr',
  purpose:#STANDARD,
  type:#IDENTIFICATION_REFERENCE,
  label:'Material Import Tax',
  position:10}]
  @UI:{lineItem:[{label: 'Material',position:10}],
  identification:[{label: 'Material',position:10}],
  selectionField:[{position:10}]}
  matnr;

  @UI:{lineItem:[{label: 'Country',position:20}],
  identification:[{label: 'Country',position:20}],
  selectionField:[{position:20}]}
  land;

  @UI:{lineItem:[{label: 'Tax Percentage',position:30}],
  identification:[{label: 'Tax Percentage',position:30}]}
  matnrtax;
  @UI.hidden: true
  LocalLastChangedAt;
}

Now Create Service Definition and Service Binding to expose your OData V4.

The App will be similar to this –

Now lets try to test this application through Postman. In order to do that you need to configure your Oauth Token as below –

Note that you need to use Password Credentials for to get a viable Token.

Now if you wander about the rest Client secrets, IDs and all, then just to clarify – this information is the same when you create the ABAP Trial tenant in SAP BTP. There are several blogs as well to give you idea about this API testing.

Anyways, moving forward. We are now a few step far away to call the ABAP Cloud Trial API from On-premise.

Note that ABAP BTP Trial gives us a Host which is dynamic and hosted in AWS as a Infrastructure Cloud. Hence we need to create a Custom proxy to the tenant Host

https://SECRET-4148-434a-YAYS-64b75d24911a.abap.us10.hana.ondemand.com

Now create a NodeJS adapter for proxy host –

var axios = require('axios');
var qs = require('qs');
var express = require('express')
var app = express()
var config = require('./.secret.json')

var tokenEndpoint = process.env['token_url'];
// console.log(tokenEndpoint)
if (tokenEndpoint) {
    tokenEndpoint = tokenEndpoint + '/oauth/token'
}
else {
    tokenEndpoint = config.uaa.url;
}

var clientId = process.env['client_id'];
if (!clientId) {
    clientId = config.uaa.clientid
}
var clientSecret = process.env['client_secret'];
if (!clientSecret) {
    clientSecret = config.uaa.clientsecret
}
var abaphost = process.env['abaphost'];
if (!abaphost) {
    abaphost = config.url
}
var emailid = process.env['emailid'];

if (!emailid) {
    emailid = config.emailid
}
var password = process.env['password'];
if (!password) {
    password = config.password
}
var port = process.env['nodeport'];
if (!port) {
    port = 8080
}

app.use(express.json());

async function getabapcloudodataresponse(url, accessToken, idToken) {
    console.log('I am here')
    console.log(url)
    try {
      const response = await axios.get(url, {
        headers: {
          Authorization: accessToken
        }
      });
      console.log('Response:', response.data);
      return response.data
      // Process the response
      
    } catch (error) {
      // Handle error
      console.error('Error:--------------------------------------------->')
      console.error(error);
    }
  }

app.get('*', async (req, res) => {
    // console.log(req.headers['authorization'])
    auth = req.headers['authorization']
    id_token = req.headers['x-id-token']
    var url = req.originalUrl;
    var fullUrl = abaphost + url;    
    var token_data = {
        access_token : auth,
        id_token : id_token
    }
    var responseABAP = await getabapcloudodataresponse(fullUrl, token_data.access_token, token_data.id_token);
    res.json(responseABAP);
})

app.listen(port)

console.log(`Server Listening in Port:: ${port}`)

Then create another file .secret.json and copy paste the Service Secret created for the ABAP Cloud Tenant.

Use manifest.yml to push your code to cloud foundry environment –

---
applications:
- name: abap_on_cloud
  random-route: false
  path: ./
  memory: 256M
  buildpack: nodejs_buildpack

After deploying this adapter to ABAP Cloud. Create new destinations in SM59 as below

This above destination is to get the Authorization Token. And the below SM59 destination is the proxy of the deployed NodeJS application –

We are almost done. Now here is a sample ABAP code to trigger ABAP BTP ODATA and consume it inside the ABAP S4 Onpremise.

*&---------------------------------------------------------------------*
*& Report ZTESTABAPCLOUD
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
REPORT ztestabapcloud.
DATA: lo_http_client   TYPE REF TO if_http_client,
      lo_rest_client   TYPE REF TO cl_rest_http_client,
      lo_request       TYPE REF TO if_rest_entity,
      lv_uri           TYPE string,
      lv_username      TYPE string,
      lv_password      TYPE string,
      lv_client_id     TYPE string,
      lv_client_secret TYPE string,
      lv_token         TYPE string,
      lv_response      TYPE string,
      lt_fields        TYPE tihttpnvp,
      lt_response      TYPE STANDARD TABLE OF string.
PARAMETERS: p_matnr TYPE matnr,
            p_land  TYPE land1.

lv_uri = '/oauth/token'.
lv_username = 'sabarnXXYY@gmail.com'.
lv_password = 'XPASSWORDX'.
lv_client_id = 'sb-XXSECRET-cfef-4379-9d14-XXSECRET!b168955|abap-trial-service-broker!b3132'.
lv_client_secret = 'd7b6ec59-XXYY-4f5a-93f3-XXSECRET$RzEH2wP5xrEtL-XXSECRETZOZ1e268Q='.

cl_http_client=>create_by_destination(
 EXPORTING
   destination              = 'OAUTH_BTPABAP'    " Logical destination (specified in function call)
 IMPORTING
   client                   = lo_http_client    " HTTP Client Abstraction
 EXCEPTIONS
   argument_not_found       = 1
   destination_not_found    = 2
   destination_no_authority = 3
   plugin_not_active        = 4
   internal_error           = 5
   OTHERS                   = 6
).

lo_http_client->request->set_method( 'POST' ).

CALL METHOD cl_http_utility=>set_request_uri
  EXPORTING
    request = lo_http_client->request
    uri     = lv_uri.
lo_http_client->request->set_header_field( name = 'Content-Type' value = 'application/x-www-form-urlencoded' ).

DATA: lv_bodystr TYPE string.
lv_bodystr = 'grant_type=password&username=' && lv_username &&
             '&password=' && lv_password &&
             '&client_id=' && lv_client_id &&
             '&client_secret=' && lv_client_secret.


CREATE OBJECT lo_rest_client
  EXPORTING
    io_http_client = lo_http_client.
lo_request = lo_rest_client->if_rest_client~create_request_entity( ).

lo_request->set_string_data( lv_bodystr ).

lo_rest_client->if_rest_resource~post( lo_request ).

DATA(lo_response) = lo_rest_client->if_rest_client~get_response_entity( ).
DATA(http_status) = lo_response->get_header_field( '~status_code' ).
lv_response = lo_response->get_string_data( ).
DATA lr_json_deserializer TYPE REF TO cl_trex_json_deserializer.
TYPES: BEGIN OF ty_json_res,
         access_token TYPE string,
         id_token     TYPE string,
         token_type   TYPE string,
         expires_in   TYPE string,
         scope        TYPE string,
         jti          TYPE string,
       END OF ty_json_res.
DATA: json_res TYPE ty_json_res.
/ui2/cl_json=>deserialize(
  EXPORTING
  json = lv_response
  CHANGING
  data = json_res
  ).



START-OF-SELECTION.
  IF p_matnr IS NOT INITIAL AND p_land IS NOT INITIAL.

* ---------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    CLEAR: lv_uri, lo_http_client, lo_request, lo_rest_client, lv_response,lo_response,http_status.
    DATA: lo_http_client1 TYPE REF TO if_http_client,
          lo_rest_client1 TYPE REF TO cl_rest_http_client,
          lo_request1     TYPE REF TO if_rest_entity.
    cl_http_client=>create_by_destination(
     EXPORTING
       destination              = 'OAUTH_BTPABAP_SERVER'    " Logical destination (specified in function call)
     IMPORTING
       client                   = lo_http_client1    " HTTP Client Abstraction
     EXCEPTIONS
       argument_not_found       = 1
       destination_not_found    = 2
       destination_no_authority = 3
       plugin_not_active        = 4
       internal_error           = 5
       OTHERS                   = 6
    ).

*lo_http_client1->request->set_method( 'GET' ).
    lv_uri = '/sap/opu/odata4/sap/zsrv_def_material_details_v4/srvd/sap/zsrv_def_material_details/0001/ZCDS_P_MATERIAL'.

    DATA: lv_filter  TYPE string,
          lv_new_uri TYPE string.
    lv_filter = '?$filter=matnr%20eq%27' && p_matnr && '%27and%20land%20eq%27' && p_land && '%27'.
    "lv_new_uri = lv_uri && lv_filter.
    "lv_uri = lv_uri && lv_filter.
    "BREAK-POINT.
    CALL METHOD cl_http_utility=>set_request_uri
      EXPORTING
        request = lo_http_client1->request
        uri     = lv_uri.

*lo_http_client1->request->set_header_field( name = 'Content-Type' value = 'application/json;odata.metadata=minimal;charset=utf-8' ).

    DATA: lv_auth TYPE string.

    CONCATENATE 'Bearer ' json_res-access_token INTO lv_auth SEPARATED BY space.

    lo_http_client1->request->set_header_field( name = 'Authorization' value = lv_auth ).
    lo_http_client1->request->set_header_field( name = 'x-id-token' value = json_res-id_token ).

    CREATE OBJECT lo_rest_client1
      EXPORTING
        io_http_client = lo_http_client1.


    lo_rest_client1->if_rest_resource~get( ).

    lo_response = lo_rest_client1->if_rest_client~get_response_entity( ).
    http_status = lo_response->get_header_field( '~status_code' ).
    lv_response = lo_response->get_string_data( ).

    IF http_status EQ 200.
      TYPES: BEGIN OF ty_tax,
               matnr    TYPE string,
               land     TYPE string,
               matnrtax TYPE string,
             END OF ty_tax.
      TYPES: value TYPE TABLE OF ty_tax.
      DATA: ls_value TYPE ty_tax,
            lt_value TYPE value.

      DATA: lo_data TYPE REF TO data.
      FIELD-SYMBOLS: <lfs_data>   TYPE any,
                     <lfs_values> TYPE any,
                     <lfs_line>   TYPE   any.

      "DATA
      /ui2/cl_json=>deserialize(
        EXPORTING
        json = lv_response
        CHANGING
        data = lo_data
        ).
      ASSIGN lo_data->* TO <lfs_data>.
      "BREAK-POINT.
      ASSIGN COMPONENT 'VALUE' OF STRUCTURE <lfs_data> TO <lfs_values>.
      ASSIGN <lfs_values>->* TO FIELD-SYMBOL(<lfs_table>).
      LOOP AT <lfs_table> ASSIGNING <lfs_line>.
        "ASSIGN <lfs_line>->* T0 FIELD-SYMBOL(<lfs_table>).
        ASSIGN <lfs_line>->* TO FIELD-SYMBOL(<lfs_lineref>).

        ASSIGN COMPONENT 'MATNR'    OF STRUCTURE <lfs_lineref> TO FIELD-SYMBOL(<lfs_matnr>).
        ASSIGN COMPONENT 'LAND'     OF STRUCTURE <lfs_lineref> TO FIELD-SYMBOL(<lfs_land>).
        ASSIGN COMPONENT 'MATNRTAX' OF STRUCTURE <lfs_lineref> TO FIELD-SYMBOL(<lfs_matnrtax>).
        IF <lfs_matnr> IS ASSIGNED AND <lfs_land> IS ASSIGNED AND <lfs_matnrtax> IS ASSIGNED.
          ASSIGN <lfs_matnr>->* TO FIELD-SYMBOL(<lfs_any>).
          IF <lfs_any> IS ASSIGNED.
            ls_value-matnr = <lfs_any>.
          ENDIF.
          ASSIGN <lfs_land>->* TO <lfs_any>.
          IF <lfs_any> IS ASSIGNED.
            ls_value-land   = <lfs_any>.
          ENDIF.
          ASSIGN <lfs_matnrtax>->* TO <lfs_any>.
          IF <lfs_matnrtax> IS ASSIGNED.
            ls_value-matnrtax = <lfs_any>.
          ENDIF.
          APPEND ls_value TO lt_value.
          CLEAR: ls_value.
        ENDIF.
      ENDLOOP.
      "BREAK-POINT.
      DATA(lv_tax) = VALUE #( lt_value[ matnr = p_matnr land = p_land ]-matnrtax OPTIONAL ).
      DATA: lv_msg TYPE string.
      IF lv_tax IS NOT INITIAL.
        CONCATENATE 'Import Tax Amount for Material:'
                  p_matnr 'and Country :'
                  p_land 'is :'
                  lv_tax '%' INTO
                  lv_msg
        SEPARATED BY space.

      ELSE.
        CONCATENATE 'Import Tax Amount for Material:'
                  p_matnr 'and Country :'
                  p_land 'is not present in ABAP Cloud DB. Please maintain tax percentage.'
                  INTO
                  lv_msg
        SEPARATED BY space.

      ENDIF.
      MESSAGE lv_msg TYPE 'I'.

    ELSE.
      MESSAGE 'Error in ABAP Cloud Connection' TYPE 'E' DISPLAY LIKE 'I'.
      SET SCREEN 1000.
    ENDIF.
  ELSE.
    MESSAGE 'Please enter Material and land' TYPE 'E' DISPLAY LIKE 'I'.
  ENDIF.

We are done with all the configurations and developments. Lets test our program now from ABAP On-premise SE38 transaction:

Execute your program and get the response back from the ABAP Cloud OData:

Understandings

  1. While going through this POC you can get idea on how to call password credential Oauth with ABAP.
  2. You also create and can use the Oauth Profile from SE80
  3. You can leverage Cloud Applications and consume it easily from On-premise which will give robustness of your application designs.
  4. I have used a GET method while Creating a Proxy on top of ABAP Trial in NodeJS. In your scenario you can leverage POST operations as well to update back to ABAP Cloud BTP OData services.