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
- While going through this POC you can get idea on how to call password credential Oauth with ABAP.
- You also create and can use the Oauth Profile from SE80
- You can leverage Cloud Applications and consume it easily from On-premise which will give robustness of your application designs.
- 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.
Must Read & References-
https://answers.sap.com/questions/13542524/better-way-to-test-btp-rap-url.html?childToView=13550136