Technical Articles
How to call a remote OData service from the trial version of SAP Cloud Platform ABAP environment
Updates
- 21.10. use of more inline declarations and filter to overcome the BT restriction
We recently made the trial version of SAP Cloud Platform ABAP Environment availalble as described in my blog It’s Trial Time for ABAP in SAP Cloud Platform.
One of the known limitations is that you can’t use the destination service and it is thus not possible to use http or RFC destinations to communicate to remote systems.
There is however the option to create a destination through providing a URL
data base_url type string.
base_url = '<base url of an OData service>'.
http_client = cl_web_http_client_manager=>create_by_http_destination( i_destination = cl_http_destination_provider=>create_by_url( i_url = base_url ) ).
http_client->get_http_request( )->set_authorization_basic(
i_username = '<username>'
i_password = '<password>'
).
!!! Warning !!!
An obvious drawback of this option when being used in a shared trial version is that everybody else can read your code and would thus be able to read your username and password as well.
Demo service being used
The demo service that I will use is a simple OData service that is published on the SAP Gateway demo system ES5.
https://sapes5.sapdevcenter.com/sap/opu/odata/sap/ZE2E100_SOL_2_SRV/$metadata
How to get a user in ES5 is described in my following blog.
Creating the Service consumption model
We first have to download the $metadata file of the OData service we want to consume. Firefox is a good option.
We start by creating a service consumption model ZSC_SALESORDERDEMO which has to be used by the client proxy that we use in our ABAP code to call the remote OData service by uploading the Service Metadata File that we have just downloaded.
In the next step you are able to select then entity sets of the OData service you want to consume.This option comes very handy since it allows to minimize the number of repository objects (CDS views that contain abstract entities) that are going to be created to the bare minimum needed.
Please note that it is in this step where you can provide another name for the ABAP artifacts that will be generated.
You just have to confirm that the below shown artifacts will be generated.
As a result three repository objects are generated
such as a Service Consumption Model
A Service Definition
@EndUserText.label: 'ZSC_SALESORDERDEMO'
@OData.schema.name: 'ZE2E100_SOL_2_SRV'
define service ZSC_SALESORDERDEMO {
expose ZSALESORDERDEMO;
}
as well as a Data Definition that contains an abstract entity
/********** GENERATED on 10/19/2019 at 18:25:05 by CB0000000083**************/
@OData.entitySet.name: 'Zsepm_C_Salesorder_SOL'
@OData.entityType.name: 'Zsepm_C_Salesorder_SOLType'
define root abstract entity ZSALESORDERDEMO {
key SalesOrder : abap.char( 10 ) ;
@Semantics.amount.currencyCode: 'TransactionCurrency'
NetAmountInTransactionCurrency : abap.dec( 16, 3 ) ;
@Semantics.amount.currencyCode: 'TransactionCurrency'
TaxAmountInTransactionCurrency : abap.dec( 16, 3 ) ;
SalesOrderLifeCycleStatus : abap.char( 1 ) ;
SalesOrderBillingStatus : abap.char( 1 ) ;
SalesOrderDeliveryStatus : abap.char( 1 ) ;
SalesOrderOverallStatus : abap.char( 1 ) ;
Opportunity : abap.char( 35 ) ;
SalesOrder_Text : abap.char( 255 ) ;
CreationDateTime : tzntstmpl ;
LastChangedDateTime : tzntstmpl ;
IsCreatedByBusinessPartner : abap_boolean ;
IsLastChangedByBusinessPartner : abap_boolean ;
Customer : abap.char( 10 ) ;
@Semantics.currencyCode: true
TransactionCurrency : abap.cuky( 5 ) ;
@Semantics.amount.currencyCode: 'TransactionCurrency'
GrossAmountInTransacCurrency : abap.dec( 16, 3 ) ;
}
Please note that also sample code is generated to consume your OData service for all CRUD-Q operations.
Consuming the service using a simple class
I have taken the above code as a starting point and replaced the Hungarian notation prefixes ? and tried to provide a sample code that follows the clean ABAP principles.
Any suggestions to improve the code are welcome.
The class has basically two methods
- get_http_client
- get_odata_response
The method get_http_client( ) creates an instance of the http_client object using the base URL of our OData service. The http_client object is than used by the method get_odata_response( ) to consume the OData service.
The client proxy needs three parameters:
- the name of the consumption model that ZSC_SALESORDERDEMO
- the http_client object
- the relative URL /sap/opu/odata/sap/ZE2E100_SOL_2_SRV/ that points to the service document of the OData service
Based on the client proxy object another object is created to perform the READ request on the entity set Zsepm_C_Salesorder_SOL. Please note that you have to use the ABAP internal name ZSEPM_C_SALESORDER_SOL here which is in upper case .
In the following a filter tree is created to filter for the key field SalesOrder and the property NetAmountInTransactionCurrency which is of type currency.
Please note that the filter node for the property NetAmountInTransactionCurrency needs the currency provided via the optional parameter iv_currency_code.
If you omit to provide this information you will get the error message:
No currency code supplied for property path ‘NETAMOUNTINTRANSACTIONCURRENCY’.
How to create filter statements for intervals
In my example I want to retrieve only sales orders with a net amount between $3000 and $7000. To achieve this goal we currently have to create two child nodes that both contain filter options to retrieve sales orders. One node for sales orders with a net amount larger than $3000 and one node for sales orders with a net amount smaller than $7000. This is because currently range tables with the option BT are NOT supported.
Not supported:
range_for_netamounts2 = VALUE #( ( sign = ‘I’ option = ‘BT‘ low = ‘3000’ high = ‘8000’) ).
The code looks like follows:
CLASS zcl_call_odata_from_trial DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PROTECTED SECTION.
DATA salesorders TYPE STANDARD TABLE OF zsalesorderdemo.
PRIVATE SECTION.
METHODS get_http_client
IMPORTING base_url TYPE string
RETURNING VALUE(http_client) TYPE REF TO if_web_http_client
RAISING
cx_web_http_client_error
cx_http_dest_provider_error.
METHODS get_odata_response
IMPORTING http_client TYPE REF TO if_web_http_client
EXPORTING entityset LIKE salesorders
RAISING
cx_web_http_client_error
/iwbep/cx_gateway.
ENDCLASS.
CLASS zcl_call_odata_from_trial IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA base_url TYPE string.
base_url = 'https://sapes5.sapdevcenter.com/'.
TRY.
DATA(http_client) = get_http_client( base_url = base_url ).
CATCH cx_web_http_client_error cx_http_dest_provider_error INTO DATA(error_creating_http_client).
out->write( error_creating_http_client->get_longtext( ) ).
EXIT.
ENDTRY.
TRY.
get_odata_response(
EXPORTING http_client = http_client
IMPORTING entityset = DATA(salesorders) ).
CATCH cx_web_http_client_error /iwbep/cx_gateway INTO DATA(error_calling_OData_service).
"handle exception
out->write( error_calling_OData_service->get_longtext( ) ).
EXIT.
ENDTRY.
LOOP AT salesorders INTO DATA(salesorder).
out->write( salesorder ).
ENDLOOP.
ENDMETHOD.
METHOD get_http_client.
http_client = cl_web_http_client_manager=>create_by_http_destination( i_destination = cl_http_destination_provider=>create_by_url( i_url = base_url ) ).
http_client->get_http_request( )->set_authorization_basic(
i_username = '<your ES5 user>'
i_password = '<password of your ES5 user>'
).
ENDMETHOD.
METHOD get_odata_response.
DATA entity LIKE LINE OF entityset.
DATA range_for_salesorders LIKE RANGE OF entity-salesorder.
DATA range_for_salesorders2 LIKE RANGE OF entity-salesorder.
DATA range_for_netamounts LIKE RANGE OF entity-netamountintransactioncurrency.
DATA range_for_netamounts2 LIKE RANGE OF entity-netamountintransactioncurrency.
DATA(client_proxy) = cl_web_odata_client_factory=>create_v2_remote_proxy(
EXPORTING
iv_service_definition_name = 'ZSC_SALESORDERDEMO'
io_http_client = http_client
iv_relative_service_root = '/sap/opu/odata/sap/ZE2E100_SOL_2_SRV/' ).
DATA(odata_request) = client_proxy->create_resource_for_entity_set( 'ZSEPM_C_SALESORDER_SOL' )->create_request_for_read( ).
odata_request->set_top( 5 )->set_skip( 0 ).
* filter
range_for_salesorders = VALUE #(
( sign = 'I' option = 'GE' low = '500000100' ) ).
range_for_netamounts = VALUE #(
( sign = 'I' option = 'LE' low = '8000' ) ).
range_for_netamounts2 = VALUE #(
( sign = 'I' option = 'GE' low = '3000' ) ).
DATA(filter_factory) = odata_request->create_filter_factory( ).
DATA(filter_child_node_1) = filter_factory->create_by_range( iv_property_path = 'SALESORDER'
it_range = range_for_salesorders ).
DATA(filter_child_node_2) = filter_factory->create_by_range( iv_property_path = 'NETAMOUNTINTRANSACTIONCURRENCY'
it_range = range_for_netamounts
iv_currency_code = 'USD' ).
DATA(filter_child_node_3) = filter_factory->create_by_range( iv_property_path = 'NETAMOUNTINTRANSACTIONCURRENCY'
it_range = range_for_netamounts2
iv_currency_code = 'USD' ).
DATA(filter_root_node) = filter_child_node_1->and( filter_child_node_2->and( filter_child_node_3 ) ).
odata_request->set_filter( filter_root_node ).
* retrieve business data
DATA(odata_response) = odata_request->execute( ).
odata_response->get_business_data( IMPORTING et_business_data = entityset ).
ENDMETHOD.
ENDCLASS.
And if you run it via F9 you get the following result.
All salesorders have a net amount between 3000$ and 8000$ and the Salesorder ID is larger than 500000100 and the result set is limited to the first 5 hits.
No animals nor hungarian notations where harmed in this example. Nice, the hope is still alive 🙂
You asked for suggestions to improve the coding: I think all of the data declarations can be replaced by inline declarations.
Definitely I'll play around with OData consumption using this method. Thank you for this idea.
Don't try range_for_netamounts = VALUE #( ( sign = 'I' option = 'BT' low = '10000' high= '20000') ).
It is not supported (yet).
Okay, okay.
Corrected: "I think all of the data declarations (except the two ranges) can be replaced by inline declarations"
I did not mean that you should not try an inline declaration for the ranges, though you are right that this doesn't work.
I meant that you should not try to use the option BT in your ranges, since it is not yet supported to use BT. But I updated my code so that it shows how can get around this restriction by simply creating another filter node and combine three of them.
We were checking on the alternative approach to call OData service from on-premise system..
Thank you for sharing 🙂
I tried instead of accessing the Odata service from the public gateway to consume a API from the SAP API Business Hub. This was perfectly possible with the code snipped which is provided.
The next way was to use your article to have a model so I can consume the data in a meaningful way not just the raw response.
Instead of authorizing myself via a username and password I use the APIKey in the header field.
In addition to that I added the Content-Type = 'application/json' and the Accept with the same value.
Now my problem is that I get "The OData service has raised a Client Error '415': Unsupported Media Type'. I tried to look a bit into this topic and some article state that it has something to do with the Content type not being set but I did set it.
I furthermore tried to debug the coding and I am not sure but I think the setting of the Content-Type did not happen correctly.
Do you have any idea where I got lost here?
Thanks for your help
Hi,
Did you happen to find the answer to this?
I have a similar issue. I tried to connect to an on prem odata service by means of an approuter deployed on my trial cloud foundry environment.
I keep getting back the error "Server response has the wrong format (content-type) 'text/html'."
My code is pretty much exactly the same as the one provided here in the blog.
What I can't figure out is how the response from a regular odata service comes back with content-type application/json, whereas the response from my approuter (where the only visible difference is a different base url) comes back with content type text/html.
I anybody has any ideas?
Best regards,
Ann
Hi Ann KOOLEN ,
an unexpected text/html is usually a name/password input form...
Regards,
Wolfgang
Hello Andre,
This blog is really informative, Thanks for writing this.
Further on this topic I want to proceed and build a fiori application using the same service which I have imported through metadata file. I have posted my question on the below tag:
https://answers.sap.com/questions/13008490/error-while-creating-the-service-binding-in-abap-c.html
Could you please let me know the further usage.
Thanks & Regards
Shilpa Gupta
Hi Andre Fischer,
I have a query on how to expose CDS views and its corresponding Service Definitions and Service Bindings created in an on-premise system to the ABAP instance on the cloud.
I have posted the query here:
https://answers.sap.com/questions/13052525/exposing-cds-corresponding-service-definition-serv.html
Can you please guide?
Regards,
Sangita Purkayastha
Hi Andre Fischer, ,
I am trying to expose the data from an on-premise system to Cloud. The first step in this series was achieved by using the class cl_http_destination_provider. Now when the data is available in the ABAP console, how can we use this data further so that the same is consumed on a Fiori Application on ABAP on Cloud Platform created using SAP Web IDE.
I have also posted a question on 'https://answers.sap.com/questions/13049978/how-to-use-the-data-exposed-using-cl-http-destinat.html' , with appropriate explanation and screenshot.
Looking forward to your expert suggestions.
Regards,
Udita
Hi Andre Fischer,
hope you keeping well. Just seeking your thoughts on the OData consumption model generating a BDEF and Service for the Odata entity. In what capacity could these be used?
Regards,
Sitakant
Hi Andre,
Thanks for the detailed blog.
We would like to know " how to navigate / associate between two or more entities " after odata service consumption in SAP BTP ABAP . We have ongoing development and would be really helpful if you find some time to answer this.
Regards,
kani.
Hi Andre,
In continuation of Kani question above, when we are calling the CRM_BUPA_ODATA from ABAP on Cloud for navigation property "WORKADDRESS", it's giving below error.
"Navigation properties are currently not supported for select ('_WORKADDRESS')"
Can you please let us know, if the navigation properties are not allowed?
Regards,
Sarath J
Hi Andre Fischer,
Thanks for the article
I’ve implemented something similar to handle BT but how are multiple BTs (LE and GE) handled?
Also do you know if the BT fix is on SAPs roadmap and when it can be expected, as this would be a very helpful feature?
Best Wishes
Adam
Hi All,
I am trying to work on RAP620-Github Repo on my local S4HANA system and some classes are not available, so I saw this blog and tried to solve my problem.
I am sharing my solution to help everyone.
I encountered the following errors during the development process, now it works as I expected.
Server response has the wrong format (content-type) 'text/html'.
Resource not found for the segment 'sap'.
It is hard to judge what went wrong for you.
In order to create an OData CLient Proxy in your SAP S/4HANA System one problem is to create an OData Client Proxy there in the first place.
Are you sure you have a working OData CLient Proxy in place?
Please check my updated blog post.
How to use the OData Client Proxy in SAP S/4 HANA or How to use the OData Service Consumption Model 2.0 in SAP S/4HANA | SAP Blogs