This blog post is only applicable for the latest version 2 of the SAP Cloud SDK. You can find an updated tutorial for version 3 over at our tutorial page.
SAP Cloud Platform, Neo | SAP Cloud Platform, Cloud Foundry | |
S/4HANA on-premise | SAP Cloud Connector required with HTTP Destination | SAP Cloud Platform Connectivity and Cloud Connector |
S/4HANA Cloud | Direct Connection with BASIC Auth (technical user) Direct Connection with SAMLOAuthBearer (PrincipalPropagation with BusinessUser) | Direct Connection with BASIC Auth (technical user, used below) |
final ErpConfigContext configContext = new ErpConfigContext();
final List<MyBusinessPartnerType> businessPartners = ODataQueryBuilder
.withEntity("/sap/opu/odata/sap/API_BUSINESS_PARTNER",
"A_BusinessPartner")
.select("BusinessPartner",
"LastName",
"FirstName",
"IsMale",
"IsFemale",
"CreationDate")
.build()
.execute(configContext)
.asList(MyBusinessPartnerType.class);
.withEntity("/sap/opu/odata/sap/API_BUSINESS_PARTNER", "A_BusinessPartner")
you already need to know three things: the OData endpoints service path (/sap/opu/odata/sap
), the endpoints name (API_BUSINESS_PARTNER
) and the name of the entity collection (A_BusinessPartner
) as defined in the metadata of the endpoint.select()
function, you need to know how these fields are named. But since they are only represented as strings in this code, you need to look at the metadata to find out how they're called. The same also applies for functions like order()
and filter()
. And of course using strings as parameters is prone to spelling errors that your IDE most likely won't be able to catch for you.final List<BusinessPartner> businessPartners =
new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.select(BusinessPartner.BUSINESS_PARTNER,
BusinessPartner.LAST_NAME,
BusinessPartner.FIRST_NAME,
BusinessPartner.IS_MALE,
BusinessPartner.IS_FEMALE,
BusinessPartner.CREATION_DATE)
.execute();
DefaultBusinessPartnerService
(default implementation of the interface BusinessPartnerService
). So now there's no more need to know the endpoint's service path, service name or entity collection name. We can call this service's getAllBusinessPartner()
function to retrieve a list of all the business partners from the system.select()
function. Instead of passing strings that represent the field of the entity, we can simply use the static fields provided by the BusinessPartner class. So not only have we eliminated the risk of spelling errors, we also made it type-safe! Again, the same applies for filter()
and orderBy()
. For example, filtering to male business partners becomes as easy as .filter(BusinessPartner.IS_MALE.eq(true))
– note the type-safe comparison.com.sap.cloud.sdk.s4hana.datamodel.odata.services
, instantiate the default implementation of the service you need (class name prefixed with Default
), and then look for the methods of the service class that represent the different available operations. Based on this, you can choose the fields to select and filters to apply using the fields of the return type.BusinessPartnerService
. The SDK provides a default, complete implementation of each service interface. The corresponding implementation is available in a class whose name is the name of the interface prefixed with Default
, for example, DefaultBusinessPartnerService
. You can either simply instantiate that class, or use dependency injection with a corresponding Java framework (covered in Step 22 of our tutorial series). The benefit of the interfaces is better testing and extensibility support.filter()
, select()
, orderBy()
, top()
and skip()
. You can also resolve navigation properties on demand or eagerly (expand, see Step 22). The VDM also gives easy access to create (see Step 20), update, and delete operations as well as function imports.BusinessPartnerServlet.java
in the following location:./application/src/main/java/com/sap/cloud/sdk/tutorial/BusinessPartnerServlet.java
package com.sap.cloud.sdk.tutorial;
import com.google.gson.Gson;
import org.slf4j.Logger;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.s4hana.datamodel.odata.helper.Order;
import com.sap.cloud.sdk.s4hana.datamodel.odata.namespaces.businesspartner.BusinessPartner;
import com.sap.cloud.sdk.s4hana.datamodel.odata.services.DefaultBusinessPartnerService;
@WebServlet("/businesspartners")
public class BusinessPartnerServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = CloudLoggerFactory.getLogger(BusinessPartnerServlet.class);
private static final String CATEGORY_PERSON = "1";
@Override
protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
try {
final List<BusinessPartner> businessPartners =
new DefaultBusinessPartnerService()
.getAllBusinessPartner()
.select(BusinessPartner.BUSINESS_PARTNER,
BusinessPartner.LAST_NAME,
BusinessPartner.FIRST_NAME,
BusinessPartner.IS_MALE,
BusinessPartner.IS_FEMALE,
BusinessPartner.CREATION_DATE)
.filter(BusinessPartner.BUSINESS_PARTNER_CATEGORY.eq(CATEGORY_PERSON))
.orderBy(BusinessPartner.LAST_NAME, Order.ASC)
.top(10)
.execute();
response.setContentType("application/json");
response.getWriter().write(new Gson().toJson(businessPartners));
} catch (final ODataException e) {
logger.error(e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write(e.getMessage());
}
}
}
BusinessPartnerService
to prepare the query to S/4HANA with the help of the SDK's Virtual Data Model. We call the method getAllBusinessPartners
, which represents the operation of the OData service that we want to call. WIth the fluent query helper returned by this method, we can gradually build up the query:BusinessPartner
that we want to retrieve (if we leave out this part, all fields will be returned),"1"
), andexecute
method. This method does a lot of the heavy lifting necessary to connect to an S/4HANA system and relieves us as developers from dealing with complex aspects such as:execute
method of the VDM returns the query result as a navigatable list of type BusinessPartner
, which represents the entity type of the response in a type-safe manner. We declare the servlet's response as JSON content and transform the result into a JSON response.ODataException
thrown by the OData call is caught and logged, before returning an error response.cd /path/to/firstapp
mvn clean install
mvn scp:clean scp:push -pl application -Derp.url=https://URL
URL
with the URL to your SAP ERP system (host and, if necessary, port).-pl
argument defines the location in which the Maven goals will be executed.-Derp.username=USER -Derp.password=PASSWORD
http://localhost:8080/businesspartners
you should be seeing a list of business partners that was retrieved from the ERP system. Note: Please login with test
/ test
).set
with the corresponding commands to define environment variables on your command shell).set destinations=[{name: "ErpQueryEndpoint", url: "https://URL", username: "USER", password: "PASSWORD"}]
URL
, USER
and PASSWORD
accordingly.cd /path/to/firstapp
mvn clean install
mvn tomee:run -pl application
destinations
that we used for the local deployment above to the Cloud Foundry application, or use the destination service of SAP Cloud Platform Cloud Foundry. Using the destination service is the recommended approach, because it already handles important aspects related to multi-tenancy, connectivity and security and is transparently integrated into the SAP Cloud SDK. Therefore, we explain how to use the destination service in detail below. cf set-env firstapp destinations '[{name: "ErpQueryEndpoint", url: "https://URL", username: "USER", password: "PASSWORD"}]'
cf set-env firstapp destinations "[{name: \"ErpQueryEndpoint\", url: \"https://URL\", username: \"USER\", password: \"PASSWORD\"}]"
cf unset-env firstapp destinations
as soon as you are done with the initial testing and when you want to use the real destination service.cf create-service xsuaa application my-xsuaa
cf create-service destination lite my-destination
my-xsuaa
for the xsuaa
service with service plan application
, and a second instance named my-destination
for the destination
service (plan lite
).manifest.yml
file by adding the two instances to the services section at the end. The remainder of the file remains as before:---
applications:
- name: firstapp
memory: 768M
host: firstapp-D123456
path: application/target/firstapp-application.war
buildpack: sap_java_buildpack
env:
TARGET_RUNTIME: tomee
JBP_CONFIG_SAPJVM_MEMORY_SIZES: 'metaspace:96m..'
SET_LOGGING_LEVEL: '{ROOT: INFO, com.sap.cloud.sdk: INFO}'
ALLOW_MOCKED_AUTH_HEADER: true
services:
- my-destination
- my-xsuaa
# - my-application-logs
# - my-connectivity
host
property declared with a unique name of your choice. The recommended way is to include the username as suffix. The hostname will later be used as subdomain of a publicly reachable route. Since this is a setup with multiple, dedicated instances, a random-route
should be omitted.ALLOW_MOCKED_AUTH_HEADER
. When the variable is explicitly set to true, the SDK will fall back to providing mock tenant and user information when no actual tenant information is available. This setting must never be enabled in productive environments. It is only meant to make testing easier if you do not yet implement the authentication mechanisms of Cloud Foundry. If you want to learn more about authorizing user access in a productive environment, please find Step 7: Securing Your Application.ErpQueryEndpoint
(this is the destination accessed by default by the SAP Cloud SDK)https://URL
(URL to your SAP S/4HANA system)cd /path/to/firstapp
mvn clean install
cf push
https://YOUR-ROUTE/businesspartners
.cf restart firstapp
./integration-tests/pom.xml
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<version>3.0.6</version>
<scope>test</scope>
</dependency>
./integration-tests/src/test/java/com/sap/cloud/sdk/tutorial/BusinessPartnerServletTest.java
package com.sap.cloud.sdk.tutorial;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.module.jsv.JsonSchemaValidator;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import java.net.URL;
import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
import com.sap.cloud.sdk.testutil.MockUtil;
import static io.restassured.RestAssured.when;
@RunWith(Arquillian.class)
public class BusinessPartnerServletTest {
private static final MockUtil mockUtil = new MockUtil();
private static final Logger logger = CloudLoggerFactory.getLogger(BusinessPartnerServletTest.class);
@ArquillianResource
private URL baseUrl;
@Deployment
public static WebArchive createDeployment() {
return TestUtil.createDeployment(BusinessPartnerServlet.class);
}
@BeforeClass
public static void beforeClass() {
mockUtil.mockDefaults();
mockUtil.mockErpDestination();
}
@Before
public void before() {
RestAssured.baseURI = baseUrl.toExternalForm();
}
@Test
public void testService() {
// JSON schema validation from resource definition
final JsonSchemaValidator jsonValidator = JsonSchemaValidator
.matchesJsonSchemaInClasspath("businesspartners-schema.json");
// HTTP GET response OK, JSON header and valid schema
when()
.get("/businesspartners")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body(jsonValidator);
}
}
testService
, is the usage of RestAssured on a JSON service backend. The HTTP GET request is run on the local route /businesspartners
, the result is validated on multiple assertions:businesspartners-schema.json
definitionintegration-tests
project, create a new resource file./integration-tests/src/test/resources/businesspartners-schema.json
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Business Partner List",
"type": "array",
"items": {
"title": "Business Partner",
"type": "object",
"required": ["BusinessPartner", "LastName"]
},
"minItems": 1
}
BusinessPartner
and LastName
will be marked as requirement for every entry of the expected business partner list. The JSON validator would break the test if any of the items was missing a required value.URL
as before../integration-tests/src/test/resources/systems.json
{
"erp": {
"default": "ERP_TEST_SYSTEM",
"systems": [
{
"alias": "ERP_TEST_SYSTEM",
"uri": "https://URL"
}
]
}
}
mvn test -Derp.username=USER -Derp.password=PASSWORD
USER
and PASSWORD
accordingly. If you do not want to pass the erp username and password, checkout the appendix below for an alternative./secure/local/path/credentials.yml
---
credentials:
- alias: "ERP_TEST_SYSTEM"
username: "user"
password: "pass"
mvn test -Dtest.credentials=/secure/local/path/credentials.yml
Failed to execute GET https://<URL>/$metadata
TrustAll
flag in your destinations configuration file ./config_master/service.destinations/destinations/ErpQueryEndpoint
...
TrustAll=TRUE
...
destinations
environment variable to additionally include the properties map:[{name: "ErpQueryEndpoint", url: "https://URL", username: "USER", password: "PASSWORD", properties: [{key: "TrustAll", value: "true"}]}]
integration-tests/pom.xml
with:<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>
[{name: "ErpQueryEndpoint", url: "https://URL", username: "USER", password: "PASSWORD", properties: [{key: "TrustAll", value: "true"},{key: "proxyHost", value: "my-proxy.com"},{key: "proxyPort", value: "8080"}]}]
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
13 | |
13 | |
10 | |
9 | |
9 | |
8 | |
7 | |
7 | |
7 | |
6 |