Skip to Content
Technical Articles

SAP Cloud Application Programming Model: Deep Insert (5) Consume Remote Service with SAP Cloud SDK

or:

How to implement
Deep Insert
in
SAP Cloud Application Programming Model
with deep insert on remote service
using SAP Cloud SDK

or:
how to do the same
like in previous blog
but using SAP Cloud SDK

Quicklinks:
Project Files
Part 1: Intro
Part 2: UUID
Part 3: Multi-Level
Part 4: Previous Tutorial

In today’s tutorial, we‘re going to rewrite previous project, this time using the SAP Cloud SDK for handling the remote OData service.
The tasks to be done are the same, but it will look little bit better.
Again: we’re creating and exposing an OData service using the SAP Cloud Application Programming Model. Instead of using the wonderful feature of automatic persistence and exposure, we’re connecting to a remote OData service and manually doing the implementation work to read and expose data. For communicating with the remote service and reading/writing data, we’re using the SAP Cloud SDK.

What is it, the SAP Cloud SDK?
Basically, it is a library for service consumption, which comes with lot of nice features
Can I have more information?
To learn about it, please refer to the highly recommendable series of blogs
What is the difference to previous implementation?
First, we use proxy classes instead of HashMaps
Second, we use a method instead of manually composing the request
Sounds good, but there must be a negative side as well…
Yes, it takes a little time to get started
What do we have to do?
Follow this tutorial
But when does it start?
Sigh

Goal

Our goal in this tutorial is to achieve the same result like in previous tutorial, but with less code:
Provide an OData service which supports Deep Insert and connect to a remote service to execute the deep insert there
When dealing with the remote service, we use the SAP Cloud SDK

Prerequisites

As a prerequisite, follow the previous blog, we’re reusing the project which we created there.
Also, the prerequisites mentioned there are valid here as well

 

Experiences with SAP Cloud SDK is NOT required

Preparation

In case not already done, create a project like described in previous blog

To use the SAP Cloud SDK, we have to add the dependencies and generate proxies

Since we’re reusing the project created in the previous tutorial, we can just add the required dependencies to the existing project’s pom file
In my case, it is located at
…\Workspace\DeepInsertRemote\srv

1) Add required dependencies

As usual, when using a java library, we have to declare the dependencies
So we add the following snippet to our pom.xml

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>com.sap.cloud.s4hana</groupId>
				<artifactId>sdk-bom</artifactId>
				<version>2.19.0</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>com.sap.cloud.s4hana</groupId>
			<artifactId>s4hana-all</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.inject</groupId>
			<artifactId>javax.inject</artifactId>
		</dependency>
	</dependencies>

Note:
The “inject” dependency was required on my local machine. It might not be required on your side

Note:
If you’re an Eclipse user and haven’t used “Lombok” before, then adding the dependency is not enough (see Troubleshooting section)

2) Generate proxy classes

As mentioned, we want to use proxy classes instead of HashMaps
In SAP Cloud SDK, these classes are called “Virtual Data Model” and are much more powerful than simple PoJos
The SDK delivery comes with built-in data model for the existing APIs in S4 Hana
As such, if you want to connect to an S4 system, you can just use the delivered proxy classes

However, in our tutorial, we aren’t using an S4 system, because it is too difficult to get one, which would make it difficult to follow the tutorial

Instead, we’re using the public read/write OData v2 service GW_SAMPLE_BASIC, as decribed here
Benefit: everybody can follow this tutorial, even if no S4 system available
Disadvantage: the virtual data model of the SDK doesn’t contain corresponding proxies
Solution: we generate the proxies ourselves
How to do that?
Follow this description

1)  Download the generator tool
The generator is an executable jar file which needs to be downloaded from Maven Central Repository
Open this link in your browser
It displays the latest versions of the odata-generator-cli
Click on the newest little “jar” hyperlink to download the generator tool

Copy the downloaded jar file to a location of your choice, e.g. C:\tmp
Leave it there and continue with the next step

2) Download the metadata file
Open the metadata document of our remote V2 service:
https://sapes5.sapdevcenter.com/sap/opu/odata/iwbep/GWSAMPLE_BASIC/$metadata
Login with your registered user
Save/download the xml
Copy the file to C:\tmp\input
Rename the file to Gwsample.xml
If you don’t like this name, you may choose any different name.
But be aware that this name will be used while generating the data model

3) Generator syntax
To generate the proxy classes, change to command prompt, navigate to c:\tmp and run the generator.
The command is composed as follows

java -jar odata-generator-cli-2.19.1.jar
— input-dir
— output-dir   can be skipped, as a folder target will be used
— package-name-prefix
–use-odata-names

java 
-jar odata-generator-cli-2.19.1.jar
-- input-dir
-- output-dir   
-- package-name-prefix
--use-odata-names

Note
Use java -jar odata-generator-cli-2.19.1.jar -help  for description of parameters

Note:
I don’t use the output-dir, since a folder target will be generated as default
Note:
I don’t use the input-dir, because above we created the folder “input”, which is the default
Note:
I would recommend to use the optional parameter package-name-prefix, because otherwise the default would be used, which is very long and specific
Note:
I would recommend to use the –use-odata-names flag, otherwise you might fail to find the desired class, because the name would be different than expected
I don’t get it
OK, quickly check an example
In the downloaded metadata xml file, we can find
EntityType Name=”SalesOrder”
Which has a
Property Name=”Note”
In our previous tutorial, we used that property name to access the corresponding entry in the HashMap
v2RequestMap.put(“Note”, v4RequestMap.get(“commentOnOrder”));
Now we expect that the proxy generator generates a class with name SalesOrder and a variable called “note” which leads to a getter method called
SalesOrder.getNote()
That’s what we want, right?
Yes
However, the property has a label-annotation
vProperty Name=”Note” . . . sap:label=”Description”
The generator by default uses this label to generate names for variable and getter, which would lead to
SalesOrder.getDescription()
Nice
No, not nice, if you don’t know that getDescription() means getNote()
OK, got it
Agree to use the flag –use-odata-names ?
Yes, absolutely
Great

4) Generate the proxies

So finally, let’s execute the command.
In my example:

java -jar odata-generator-cli-2.19.1.jar -p com.example.proxies –use-odata-names

As a result, the proxy classes are generated into a new folder C:\tmp\target
Stop, I get an error…!
Did you create the input folder?
Oh, sorry
sigh…
What now?

Now we want to use the generated classes in our project
So we copy the root folder “proxies” and paste it into our CAP project, into the package folder com.example

In my case, (working with Eclipse) my filesystem looks like this

Afterwards, we refresh our IDE, such that the changes are activated: the new dependencies and the new classes
The project should build without errors
If you’re using Eclipse and have lots of build errors, you might try checking the Troubleshooting section

Are we eventually done with the tutorial?
No, we haven’t even started…
I’m already tired
This was only the preparation
Sigh

Project

Oh no, I don’t want to start from scratch again
Just kidding
We’re reusing the project we created in the previous tutorial, same CDS files
We open the java implementation class called “ServiceImplementation.java” and delete all the code
OK, it is enough to delete most of the content of the methods
Too late
Sigh

Implementation

We don’t need to go through all the 5 steps again, because all steps remain the same.
Let’s only have a look at 2 examples.

First example: How to execute a request to the remote OData V2 service?
See below:

DefaultGwsampleService service = new DefaultGwsampleService();        
ErpConfigContext config = new ErpConfigContext("ES5Destination");
SalesOrder createdV2SalesOrder = service.createSalesOrder(salesOrderToCreate).execute(config);

We need an instance of the service object, which is used to execute a “createSalesOrder”
The request payload is contained in the proxy class which is passed as parameter
As shown below, we instantiate such proxy class with
SalesOrder salesOrder = new SalesOrder()
And we fill it with data from the request payload, as expected
To execute a request, we also need to pass the destination, wrapped in a Context object

Second example: how to use proxies
Accessing the data is done with convenient generated getters
createdV2SalesOrder.getSalesOrderID()
As such, when converting the v2 data model to v4, it looks like this:

v4ResponseMap.put("orderId", v2ResponseSalesOrder.getSalesOrderID());

Note:
As mentioned in previous blog, the generic connectivity layer offers functionality to generate proxy classes as well.
If used, we would convert a V2 proxy to V4 proxy (and vice versa)

Interesting to see how the association is handled:

List<SalesOrderLineItem> itemList = v2ResponseSalesOrder
                                      .getLineItemsIfPresent()
                                      .get();

This get-method assumes that LineItems are present, because the navigation to LineItems has been followed, e.g. with a $expand.
So the payload can be returned.
More precisely, an Optional is returned, for better handling if LineItens not present
In our case:
we’re implementing the deep insert, thus the response contains the nested LineItems (like $expand)
Means that this method is perfect for us.

Note:
Our implementation is not clean: it will throw an exception if a normal CREATE is performed, without inline LineItems
So make sure to add the necessary code in productive service

Note:
The SDK offers a second a second method for accessing associated entities:

salesOrder.getLineItemsOrFetch()

This method name implies that a navigation link is executed in order to get the data, if not already present
This can be used as well, but what would happen if a normal CREATE without inline LineItems would hit our code?
The SDK would fire a request which we wouldn’t need?
I don’t know
Me neither
sigh

See Appendix for the full sample code

Deploy & Test

To test our project, we proceed exactly the same way like we did in previous tutorial, the service should behave exactly same way.

Summary

In this tutorial we’ve learned how to use the SAP Cloud SDK for executing deep insert on a V2 OData service
We’ve seen the convenient APIs

In our case it was necessary to generate the VDM, the “Virtual Data Model”, because in our example we were using a custom service, not pre-generated S4 Hana API
Or, to rephrase it positively:
We had the chance to learn how to use proxy generator.
Oh, what great learning experience!
Really?
No
sigh…

Troubleshooting

If you get compilation errors in your Eclipse project and the reasons are that getters cannot be found, then see below for the reason.
The getters are not generated because the Lombok framework is used.
With Lombok, it isn’t required to write getters/setters anymore, Lombok takes care
To verify, you can go to an erroneous class and search for error markers at the @Data annotation or the import Lombok.Data
This annotation tells Lombok to generate getters and setters under the hood
Installing Lombok into your Eclipse, tells the IDE how to find the missing (non-coded) getters
So, to solve, you need to install Lombok to your eclipse

Install Lombok:
Download Lombok.jar
Move it to your eclipse folder
It is an executable jar file
So either double-click it or run java -jar lombok.jar
In the Lombok configuration screen, browse to your eclipse installation (the “eclipse” folder)
Afterwards restart Eclipse
You might also need to “clean” your project
Result: the compilation errors are gone

Links

Official SAP Cloud SDK home

Appendix: Sample Project Files

ServiceImplementation.java

package com.example;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.example.proxies.namespaces.gwsample.SalesOrder;
import com.example.proxies.namespaces.gwsample.SalesOrderLineItem;
import com.example.proxies.services.DefaultGwsampleService;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.service.prov.api.exception.DatasourceException;
import com.sap.cloud.sdk.service.prov.api.operations.Create;
import com.sap.cloud.sdk.service.prov.api.request.CreateRequest;
import com.sap.cloud.sdk.service.prov.api.response.CreateResponse;
import com.sap.cloud.sdk.service.prov.api.response.ErrorResponse;

public class ServiceImplementation {
	private static Logger logger = LoggerFactory.getLogger(ServiceImplementation.class);
	
	@Create(entity = "OrderEntity", serviceName = "RelationService")
	public CreateResponse createSalesOrder(CreateRequest createRequest) throws DatasourceException{		
		
		// 1) access the payload from request body, which is OData V4
        Map<String, Object> v4RequestMap = createRequest.getData().asMap();
        
        // 2) compose the payload to be sent to remote OData V2 service
        SalesOrder salesOrder = convertV4RequestPayloadToV2(v4RequestMap); 
        
        try {
        	// 3) send request to v2 service
    		DefaultGwsampleService service = new DefaultGwsampleService();        
    		ErpConfigContext config = new ErpConfigContext("ES5Destination");
			SalesOrder createdV2SalesOrder = service.createSalesOrder(salesOrder).execute(config);
			
        	// 4) convert v2 response back to v4
			Map<String, Object> v4ResponseMap = convertV2ResponsePayloadToV4(createdV2SalesOrder);
			
			// 5) compose the V4 response of our service
			return CreateResponse.setSuccess().setData(v4ResponseMap).response();
		} catch (ODataException e) {
			return CreateResponse.setError(ErrorResponse.getBuilder()
					.setMessage(e.getMessage())
					.setStatusCode(501)
					.setCause(e)
					.response());
		}
	}	
	
	/* HELPERS */
	
	// handle the full payload, the parent entity (SalesOrder) including nested inline list of children (LineItems)
	private SalesOrder convertV4RequestPayloadToV2(Map<String, Object> v4RequestMap){
		// map the v4 property names to v2 and, in addition, fill some non-nullable fields
		SalesOrder salesOrder = new SalesOrder();		
		salesOrder.setNote((String)v4RequestMap.get("commentOnOrder"));
		salesOrder.setCustomerID("0100000000");
		salesOrder.setCurrencyCode("USD");

		// now handle the deep insert, the nested inline list of maps
		List<Map<String, Object>> v4InlineItemsList = (List<Map<String, Object>>)v4RequestMap.get("linkToItems"); 				
		List<SalesOrderLineItem> salesOrderLineItemList = convertV4InlineListToV2(v4InlineItemsList);		
		
		salesOrder.setLineItems(salesOrderLineItemList);
		return salesOrder;
	}
	 // inline list: SalesOrderLineItem collection
	private List<SalesOrderLineItem> convertV4InlineListToV2(List<Map<String, Object>> v4InlineItemsList){
		// the nested inline data is a list, because of to-many association
		List<SalesOrderLineItem> v2InlineItemsList = new ArrayList<SalesOrderLineItem>();
		// loop through all v4 line items in order to map them to v2
		for (Map<String, Object>  v4InlineMap : v4InlineItemsList) {
			SalesOrderLineItem v2InlineMap = convertV4InlineMapToV2(v4InlineMap);
			v2InlineItemsList.add(v2InlineMap);
		} 
		return v2InlineItemsList;
	}
	
	// inline Map: single SalesOrderLineItem entry
	private SalesOrderLineItem convertV4InlineMapToV2(Map<String, Object> v4InlineMap){
		SalesOrderLineItem item = new SalesOrderLineItem();		
		item.setQuantity((BigDecimal)v4InlineMap.get("numberOfProductsInOrder"));
		item.setProductID((String)v4InlineMap.get("productForOrder"));
		item.setDeliveryDate(LocalDateTime.now());
		item.setCurrencyCode("USD");
		item.setItemPosition("0000000010");
		return item;
	}
		
	// response of V2 deep insert contains a SalesOrder with expanded list of LineItems
	private Map<String, Object> convertV2ResponsePayloadToV4(SalesOrder v2ResponseSalesOrder) throws ODataException{
		Map<String, Object> v4ResponseMap = new HashMap<String, Object>();
		v4ResponseMap.put("orderId", v2ResponseSalesOrder.getSalesOrderID());
		v4ResponseMap.put("commentOnOrder", v2ResponseSalesOrder.getNote());
		// the response of deep insert is a $expand
		List<SalesOrderLineItem> v2ResponseInlineItemList = v2ResponseSalesOrder.getLineItemsIfPresent().get();
		// convert it to v4
		List<Map<String, Object>> v4ResponseInlineList = convertV2InlineListToV4(v2ResponseInlineItemList);
		// add the associated entities to our parent map
		v4ResponseMap.put("linkToItems", v4ResponseInlineList);
		
		return v4ResponseMap;
	}
	
	private List<Map<String, Object>> convertV2InlineListToV4(List<SalesOrderLineItem>  v2ResponseInlineList){
		List<Map<String, Object>> v4ResponseInlineList = new ArrayList<Map<String, Object>>();
		for(SalesOrderLineItem salesOrderLineItem : v2ResponseInlineList){
			Map<String, Object> v4ResponseInlineMap = convertV2InlineMapToV4(salesOrderLineItem);
			v4ResponseInlineList.add(v4ResponseInlineMap);
		}
		return v4ResponseInlineList;
	}
	
	// convert inline entry of V2 SalesOrderLineItem to V4 Map
	private Map<String, Object> convertV2InlineMapToV4 (SalesOrderLineItem v2LineItem){
		Map<String, Object> v4ResponseInlineMap = new HashMap<String, Object>();

		v4ResponseInlineMap.put("linkToOrder_orderId", v2LineItem.getSalesOrderID());
		v4ResponseInlineMap.put("itemId", UUID.randomUUID() ); 
		v4ResponseInlineMap.put("productForOrder", v2LineItem.getProductID());
		v4ResponseInlineMap.put("numberOfProductsInOrder", v2LineItem.getQuantity());
		
		return v4ResponseInlineMap;
	}	
}

pom.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.sap.cloud.servicesdk.prov</groupId>
		<artifactId>projects-parent</artifactId>
		<version>1.30.1</version>
	</parent>
	<artifactId>DeepInsertRemote-srv</artifactId>
	<groupId>customer</groupId>
	<version>1.0-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>DeepInsertRemote-srv</name>
	<properties>
		<packageName>com.example</packageName>
	</properties>
	<build>
		<finalName>${project.artifactId}-${project.version}</finalName>
		<plugins>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.6.0</version>
				<configuration>
					<executable>npm</executable>
					<workingDirectory>${project.basedir}/../</workingDirectory>
				</configuration>
				<executions>
					<execution>
						<id>npm install</id>
						<goals>
							<goal>exec</goal>
						</goals>
						<phase>generate-sources</phase>
						<configuration>
							<arguments>
								<argument>install</argument>
							</arguments>
						</configuration>
					</execution>
					<execution>
						<id>npm run build</id>
						<goals>
							<goal>exec</goal>
						</goals>
						<phase>generate-sources</phase>
						<configuration>
							<arguments>
								<argument>run</argument>
								<argument>build</argument>
							</arguments>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
		<resources>
			<resource>
				<directory>src/main/resources</directory>
				<excludes>
					<exclude>connection.properties</exclude>
				</excludes>
			</resource>
		</resources>
	</build>
	<profiles>
		<profile>
			<activation>
				<property>
					<name>devmode</name>
					<value>true</value>
				</property>
			</activation>
			<build>
				<plugins>
					<plugin>
						<artifactId>maven-war-plugin</artifactId>
						<version>3.0.0</version>
						<configuration>
							<webResources combine.children="append">
								<resource>
									<directory>${project.build.sourceDirectory}</directory>
									<targetPath>sources</targetPath>
								</resource>
							</webResources>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
		<profile>
			<id>only-eclipse</id>
			<activation>
				<property>
					<name>m2e.version</name>
				</property>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.slf4j</groupId>
					<artifactId>slf4j-api</artifactId>
					<scope>compile</scope>
				</dependency>
				<dependency>
					<groupId>com.sap.db.jdbc</groupId>
					<artifactId>ngdbc</artifactId>
					<scope>compile</scope>
				</dependency>
			</dependencies>
			<build>
				<pluginManagement>
					<plugins>
						<plugin>
							<groupId>org.eclipse.m2e</groupId>
							<artifactId>lifecycle-mapping</artifactId>
							<version>1.0.0</version>
							<configuration>
								<lifecycleMappingMetadata>
									<pluginExecutions>
										<pluginExecution>
											<pluginExecutionFilter>
												<groupId>org.codehaus.mojo</groupId>
												<artifactId>exec-maven-plugin</artifactId>
												<versionRange>[1.6.0,)</versionRange>
												<goals>
													<goal>exec</goal>
												</goals>
											</pluginExecutionFilter>
											<action>
												<ignore />
											</action>
										</pluginExecution>
									</pluginExecutions>
								</lifecycleMappingMetadata>
							</configuration>
						</plugin>
					</plugins>
				</pluginManagement>
			</build>
		</profile>
	</profiles>
	
	<!-- Required when using SAP Cloud SDK -->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>com.sap.cloud.s4hana</groupId>
				<artifactId>sdk-bom</artifactId>
				<version>2.19.0</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>com.sap.cloud.s4hana</groupId>
			<artifactId>s4hana-all</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.inject</groupId>
			<artifactId>javax.inject</artifactId>
		</dependency>
	</dependencies>
</project>

 

data-model.cds
(no change compared to previous blog)

namespace com.relation;

entity OrderEntity {
  key orderId: String;
  commentOnOrder: String;
  linkToItems :  Composition of many ItemEntity on linkToItems.linkToOrder = $self;
}

entity ItemEntity {
  key itemId: UUID; 
  productForOrder: String;
  numberOfProductsInOrder: Decimal(5, 2); 
  linkToOrder : Association to OrderEntity;
}

cat-service.cds
(no change compared to previous blog)

using com.relation from '../db/data-model';

service RelationService {

    @cds.persistence.skip
    entity OrderEntity as projection on relation.OrderEntity;
    
    @cds.persistence.skip
    entity ItemEntity as projection on relation.ItemEntity;
}

mta.yaml
(no change compared to previous blog)

_schema-version: 2.0.0
ID: DeepInsertRemote
version: 1.0.0
modules:
  - name: DeepInsertRemote-db
    type: hdb
    path: db
    parameters:
      memory: 256M
      disk-quota: 256M
    requires:
      - name: DeepInsertRemote-db-hdi-container
  - name: DeepInsertRemote-srv
    type: java
    path: srv
    parameters:
      memory: 990M
    provides:
      - name: srv_api
        properties:
          url: ${default-url}
    requires:
      - name: DeepInsertRemote-db-hdi-container
        properties:
          JBP_CONFIG_RESOURCE_CONFIGURATION: '[tomcat/webapps/ROOT/META-INF/context.xml:
            {"service_name_for_DefaultDB" : "~{hdi-container-name}"}]'
      - name: DeepInsertRemote-uaa
      - name: ES5Destination
        group: destinations
        properties:
         name: ES5Destination
         url: '~{url}'
         username: '~{username}'
         password: '~{password}'              
resources:
  - name: DeepInsertRemote-db-hdi-container
    type: com.sap.xs.hdi-container
    properties:
      hdi-container-name: ${service-name}
  - name: DeepInsertRemote-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service-plan: application
      service: xsuaa
      config:
        xsappname: DeepInsertRemote-${space}
        tenant-mode: dedicated
  - name: ES5Destination
    properties:
       url: https://sapes5.sapdevcenter.com
       username: your_ES5_username
       password: your_ES5_password
Be the first to leave a comment
You must be Logged on to comment or reply to a post.