SAP Cloud Platform SDK for service development: Create OData Service [13] Data Source OData V2
In the previous blog, we’ve discussed the different SDKs and APIs and how they relate to each other.
After so much code-free text, it is now time to get the hands dirty again and play around with the data source for OData V2.
And as usual, we’ll keep the first example as simple as possible.
Learning
In the present tutorial, you’ll be creating an OData V4 service exposing data which is fetched from an external service
This blog is part of the series of tutorials for beginners. (prerequisites are listed here)
All blogs were showing how to expose data.
Now, in the present blog, we’re going to learn how to fetch such data
That data is accessible via an OData V2 service
For accessing it, we’re using the data source library for OData V2 consumption
It’s easy !
We’re using destination, and even that is made easy when using the data source library !
Overview
This example is meant to just show how to use the Data Source API, so please don’t ask for a meaningful reason about why we’re doing what we’re doing.
What we’re doing:
Like we did in previous tutorials, we’re creating an OData V4 service which exposes data
In the implementation code of our service, we call a second OData service in order to fetch data
In our simple example, we don’t modify the data, we just pass it to the response of our service
In order to differentiate our service from the used service, we reduce the amount of properties.
That’s it.
The flow:
The end-user calls our V4 service
Our V4 service calls the V2 service
The V2 service responds with the requested data
Our V4 service receives the data and sends it in the response
The end user receives the data as result of his OData V4 request
Steps to be performed:
Find and explore an OData V2 service for consumption
Create a destination pointing to that service
Create OData V4 service, based on SAP Cloud Platform SDK for service development
Bind application to the destination service
Deploy application
Run and test the OData v4 service and compare the result with the OData V2 service
Preparation
OData V2 service for consumption
For this first simple example, I’ve thought of using the reference service which is advertised on the OData homepage.
It can be found navigating as follows
www.odata.org -> Developers -> Reference Services -> OData v2
Here we can see one service listed with name: OData
The service-URL is http://services.odata.org/V2/OData/OData.svc/
The service has an entity set called Products, which we can nicely consume.
I think this is the easiest way to try a scenario.
In the next tutorial we’ll cover the other operations and functionality
Destination
After we’ve decided which OData V2 service we want to call, we have to create a destination in the SAP Cloud Platform.
Please refer to the description in the prerequisites blog:
How to create a destination in SAP Cloud Platform, Cloud Foundry Environment
For the scenario in the present tutorial, the following information needs to be entered in the destination configuration:
Name | odatarefservices |
Type | HTTP |
Description | Destination for reference services on odata.org |
URL | http://services.odata.org |
Proxy Type | Internet |
Authentication | NoAuthentication |
Project
Nothing about project creation here (but there)
However, after generation of the project, we have to do a few changes to the manifest.yml file
manifest.yml
In the “Preparation” section, we’ve created the required service instances.
Now we have to bind our application to the service instances, otherwise they would be useless.
Binding a service instance to a deployed application can be done on Cloud Foundry via the cockpit, or using the command line client.
However, it is more comfortable, to declare the service bindings in the manifest file of the application, even before first deployment.
Service usage:
In the manifest.yml file, we declare the usage of existing Cloud Foundry services. In our case, it is the destination service and the xsuaa service which we created beforehand.
During deployment to Cloud Foundry, our app will be bound to the service instances.
As such, these service instances need to be created beforehand, otherwise the deployment will fail.
These are the lines which we need to add to the manifest.yml file which is generated along with our project:
services:
- demoxsuaa
- demodestination
Note:
The lines below “services” are meant to list the required service instances.
The names which are entered here, have to match exactly the names of the instances, as created in the preparation section.
Otherwise, the log will print meaningful error messages
Note:
If the creation wizard has asked for hdi-container-service name, then the generated manifest will contain that entry under “services”.
In that case, you need to delete that entry, otherwise the deployment will fail if that service instance is not existing.
Un-Security:
Remember: in our implementation we’re going to reach out to an existing OData V2 service. For that, we’re using a convenience API.
The OData V2 Data Source API has a security setting which enforces the usage of JWT token.
See here.
However, for simple prototyping use cases it can be switched off.
This is done via an environment variable.
It can be set directly in CloudFoundry in the browser UI, or via command line.
But it can also be set in the manifest file, which makes life easier.
This is the relevant line:
env:
ALLOW_MOCKED_AUTH_HEADER: 'true'
Note:
Although it is not really required, you may add an entry called “host” to the manifest.
The value of host will be part of the URL of the deployed application.
If the entry is missing, then just the application name will be used.
Example:
host: demoproject
Note:
You’ve already found that editing the manifest.yml file in a text editor is a – say – delicate operation.
Every little space can damage the file.
You should make sure to use the yml editor in eclipse to avoid problems. The editor at least gives error markers if the indentation is not correct.
Specially copying such lines from a browser window can lead to problems.
When doing copy&paste, I think it is a good practice to make sure that the copied text doesn’t contain any formatting metadata, before pasting it into the manifest file. To do so, copy the text from browser into clipboard, then paste it into a notepad editor, then copy it again from there and paste it into the manifest file.
Model
As usual, we create an xml file which defines the OData model of our OData V4 service which we want to create and provision.
In this example, our procedure is lightly different. We don’t specify arbitrary property names according to our wish.
Instead, we create our model strictly according to the OData V2 service which we want to consume.
Let’s open the metadata document of the OData V2 service, which acts as our backend:
http://services.odata.org/V2/OData/OData.svc/$metadata
The service defines an EntityType called Product, which we want to reuse:
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID"/>
</Key>
<Property Name="ID" Type="Edm.Int32" Nullable="false"/>
<Property Name="Name" Type="Edm.String" Nullable="true" m:FC_TargetPath="SyndicationTitle" m:FC_ContentKind="text" m:FC_KeepInContent="false"/>
<Property Name="Description" Type="Edm.String" Nullable="true" m:FC_TargetPath="SyndicationSummary" m:FC_ContentKind="text" m:FC_KeepInContent="false"/>
<Property Name="ReleaseDate" Type="Edm.DateTime" Nullable="false"/>
<Property Name="DiscontinuedDate" Type="Edm.DateTime" Nullable="true"/>
<Property Name="Rating" Type="Edm.Int32" Nullable="false"/>
<Property Name="Price" Type="Edm.Decimal" Nullable="false"/>
<NavigationProperty Name="Category" Relationship="ODataDemo.Product_Category_Category_Products" FromRole="Product_Category" ToRole="Category_Products"/>
<NavigationProperty Name="Supplier" Relationship="ODataDemo.Product_Supplier_Supplier_Products" FromRole="Product_Supplier" ToRole="Supplier_Products"/>
</EntityType>
For today, we ignore all the facets, attributes and navigation properties, and we create our own model with identical property names:
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="Description" Type="Edm.String"/>
</EntityType>
Note:
Why we don’t use all properties?
Because we want to show that we can have different model in our V4 service, than the used V2 service
Note:
Why do we have to stick to equal property names?
Because like that, the result of the call to V2 can be automatically mapped to our V4 response data.
It would be possible to invent different property names, but then it would be required to do manual mapping in the Java code.
The full edmx file can be found at the end of this page
Implementation
Now we’re coming to the interesting part.
First of all, you will ask yourself:
“I’m using a new Data Source API, so which dependency do I need to add to my pom?”
The user-friendly answer is: nothing needs to be added.
The dependency comes out-of-the-box, because the data source libraries are delivered under the umbrella of the SAP Cloud Platform SDK for service development
Coming to coding.
We’re implementing the QUERY operation only.
As you know, we need to provide a list of maps to the response object (let’s ignore the other formats, POJO and EntityData, for now)
Instead of manually creating an ArrayList and a HashMap with some dummy mock data, this time we get real (sample) data from the (real) OData V2 service.
This is the URL to get the desired data:
http://services.odata.org/V2/OData/OData.svc/Products
And this is how how it is called by using the API:
ODataQuery odataQuery = ODataQueryBuilder.withEntity("/V2/OData/OData.svc", "Products").build();
ODataQueryResult result = odataQuery.execute("odatarefservices");
As usual, the API follows the Builder-pattern and the Builder has to be configured with:
The service name, including the path: “/V2/OData/OData.svc”
The name of the EntitySet to call: “Products”
When invoking the execute, the name of the destination which is configured in the Cloud, has to be passed: “odatarefservices”
Remember? Above, we’ve created a destination and the value of the URL is:
http://services.odata.org
Under the hood, the destination is resolved and the full URL is computed, such that it matches the desired full URL http://services.odata.org/V2/OData/OData.svc/Products
On successful execution, the result of this call can be found in the ODataQueryResult instance.
If an error occurs, which can easily happen – unfortunately…. – then an exception is thrown, which we need to catch (see below)
The ODataQueryResult object has some convenience, we can ask it to present the result in one of the 3 different formats of data.
In our simple example, we use HashMap, which is most simple.
List<Map<String,Object>> productList = result.asListOfMaps();
BTW, these 3 lines can be chained into one line:
List<Map<String,Object>> productList = ODataQueryBuilder
.withEntity("/V2/OData/OData.svc", "Products")
.build()
.execute("odatarefservices")
.asListOfMaps();
This list is the result of the call to the OData V2 service, parsed and converted to list of map.
Usually in a productive scenario, we would do some modifications on this V2 service result.
Why?
Because otherwise, the end-user could directly use the V2 service and wouldn’t look for our V4 service.
But we don’t do anything in our example, we just pass the list to our response object:
return QueryResponse.setSuccess().setDataAsMap(productList).response();
Note:
As you know, the keys of the entries of the Maps in the response have to strictly match the definition of our metadata in the edmx file.
But since we’ve taken care to name our properties exactly like names of the V2 service, we can just pass the maps like they are.
Just one thing we’re modifying: we’re reducing the number of properties, our V4 service has less properties than the response of the V2 service.
With other words: we’re passing a list of maps to the framework and each map contains properties which don’t exist in our V4 service (e.g. ReleaseDate, etc)
However, the FWK doesn’t care if there are more properties than expected, as long as it finds those properties which are required.
That’s it.
We’ve created our first OData V4 service with real data from a real backend (somewhat real, as it is also sample content…)
Deploy
Make sure that your manifest.yml file looks similar to the sample file which I’ve pasted at the end of this page. As usual, you might need to modify the name of the host, as it might be already taken.
After build and push, you might want to check the application bindings, to see if the services have been properly resolved:
You can use the browser UI, or execute this command:
cf service demodestination
This command provides information about the service instance “demodestination” and you should see that your deployed app is listed there
Run
If you’re lucky…..if everything is fine…. If the destination is configured properly….. if the backend is up and running….. if the network is healthy…… if the weather is nice….. well, if you’re just lucky, then you’ll be able to see the products list. You just need to call your OData V4 service URL:
e.g.
https://demoproject.cfapps.eu10.hana.ondemand.com/odata/v4/DemoService/Products
Result should look like this:
Comparing to the original service:
As you can expect, the existing framework capabilities are applicable here as well,
e.g. you can apply system query options which are working out of the box:
…/odata/v4/DemoService/Products?$top=1&$select=Name
Summary
You should have gained understanding of using a data source API in the implementation of your service.
You can differentiate the SAP Cloud Platform SDK for service development on one side – from the data source library on the other side.
Provisioning on one side, consumption on the other side.
If anything is not clear, please let me know.
In the next tutorial, we’ll make use of a different v2 service in order to show how the other operations are implemented and also to do some more modification.
Links
Overview of blog series and link collection
OData homepage: http://www.odata.org
OData reference services: http://www.odata.org/odata-services
Appendix 1: Source code of manifest file: manifest.yml
---
applications:
- name: demoservice
memory: 512M
buildpack: sap_java_buildpack
path: target/DemoService-0.0.1-SNAPSHOT.war
env:
ALLOW_MOCKED_AUTH_HEADER: 'true'
services:
- demoxsuaa
- demodestination
Appendix 2: Source code of model file: DemoService.xml
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="demo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Product">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32"/>
<Property Name="Name" Type="Edm.String"/>
<Property Name="Description" Type="Edm.String"/>
</EntityType>
<EntityContainer Name="container">
<EntitySet Name="Products" EntityType="demo.Product"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Appendix 3: Source code of Java file: ServiceImplementation.java
package com.example.odata.DemoProject;
import java.util.List;
import java.util.Map;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryResult;
import com.sap.cloud.sdk.service.prov.api.operations.Query;
import com.sap.cloud.sdk.service.prov.api.request.QueryRequest;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;
import com.sap.cloud.sdk.service.prov.api.response.QueryResponse;
public class ServiceImplementation {
@Query(serviceName = "DemoService", entity = "Products")
public QueryResponse getProducts(QueryRequest queryRequest) {
try {
ODataQueryResult result = ODataQueryBuilder
.withEntity("/V2/OData/OData.svc", "Products")
.build()
.execute("odatarefservices");
List<Map<String,Object>> productList = result.asListOfMaps();
return QueryResponse.setSuccess()
.setDataAsMap(productList)
.response();
} catch (ODataException e) {
return QueryResponse.setError(
ErrorResponse.getBuilder()
.setMessage("Error occurred while getting products from backend. See error log for details.")
.setStatusCode(500)
.response());
}
}
}
Hello Carlos,
thanks for the great blog series! We would like to use this approach in our project to create services, using data from different sources.
I was able to implement the previous blogs using mock data successfully. Only with this one where the data source is another Odata service I am now running into an issue.
Could it be that it is because I want to deploy the service in Neo instead of Cloud Foundry? For cloud foundry, it is explained in the "Prerequisites Cloud" blog how to set up the destination and the xsuaa-service. For Neo, I of course created the destination, but I am not sure what needs to be set up in terms of xsuaa (Authorization&Trust Management).
When I now deploy the service on Neo, I see in the log the following error message:
DestinationAccessException: Failed to get destinations of provider: Failed to get access token for destination service.
It would be great if you could help me to get this up and running on Neo!
Thanks and best regards,
Tobias
Hello Tobias Renth
Thanks so much for your Feedback..!
I have to say that neo was never supported by the sdk. I did a basic attempt and it worked, but your Problem seems more severe, because it seems that the FWK is connecting to the Destination Service. This is done in CF with 2 calls, one to get the oauth token, second one to get the destinations. On neo, it works different, there's a convenience layer on top, which gives you an httpClient containing the destinations.
Not sure if it would help changing the pom, if you find a neo-dependency (there's a Cloud platform abstraction)
Anyways, I wouldn't recommend moving Forward with the SDK on neo, I'm afraid ;-(
Furthermore, the SDK is no more developped, it has been Kind of replaced by CAP (which also doesn't support neo)
I'm sorry, I don't have better info for you… ;-(
Cheers,
Carlos
Hi Carlos,
thanks so much for your quick feedback!
In that case it seems we need to check for a different solution for our requirements.
Cheers, Tobias