Skip to Content

The following steps will explain how to create the very first Java project to call OData services using the SAP S/4HANA Cloud SDK.

Note: This post is part of a series. For a complete overview visit the SAP S/4HANA Cloud SDK Overview.

Goal of this blog post

In this tutorial, we will do the following:

  1. Enhance the HelloWorld project stub to call an existing OData service.
  2. Deploy the project on
    1. SAP Cloud Platform Neo
    2. SAP Cloud Platform based on Cloud Foundry
  3. Write an Integration Test

If you want to follow this tutorial, we highly recommend checking out Step 1 (Setup) and Step 2 (HelloWorld on SCP Neo) or Step 3 (HelloWorld on SCP CloudFoundry), respectively, depending on your choice of platform. You will not need any additional software besides the setup explained in the first part of the series as the server will run on your local machine. If you would like to know more about communication management and identity & access management artifacts in S/4HANA, please follow the Deep Dive on this topic.

Note: This tutorial requires access to an SAP ERP system or, as a fallback, any other OData V2 service.

Prerequisites

In order to execute this tutorial successfully, we assume a working and reachable system of SAP S/4HANA on-premise or S/4HANA Cloud. You may substitute the presented cost center service by any other API published on the SAP API BusinessHub.

If you do not have a S/4HANA system at hand, you may use a public service such as the Northwind OData Service as a fallback solution.

Please note that depending on the platform (Neo or CloudFoundry) you are using, the configuration to the respective S/4HANA system might be different. In the following, we list the methods by which you can how you can access your system.

SAP Cloud Platform, Neo SAP Cloud Platform, Cloud Foundry
S/4HANA on-premise SAP Cloud Connector required with HTTP Destination Stay tuned 😉
S/4HANA Cloud

Direct Connection with BASIC Auth (technical user)

Direct Connection with SAMLOAuthBearer (PrincipalPropagation with BusinessUser)

Direct Connection with BASIC Auth (Technical User, see below)

Note that your application code is not dependent on this. Using the S/4HANA Cloud SDK, you can write your code once and it is capable of dealing with all different authentication and connectivity options.

Write the CostCenterServlet

The SAP S/4HANA Cloud SDK provides simple and convenient ways to access your ERP systems out of the box. In this example we will implement an endpoint that performs an OData query to SAP S/4HANA in order to retrieve a list of cost centers from our ERP system.

To get started open your previously created Hello World project (in our case this is called firstapp) and create a new file called CostCenterServlet.java in the following location:

./application/src/main/java/com/sap/cloud/sdk/tutorial/CostCenterServlet.java

package com.sap.cloud.sdk.tutorial;

import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.odatav2.connectivity.ODataException;
import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
import com.sap.cloud.sdk.s4hana.connectivity.ErpConfigContext;
import com.sap.cloud.sdk.s4hana.connectivity.ErpDestination;
import com.sap.cloud.sdk.s4hana.connectivity.ErpEndpoint;
import com.sap.cloud.sdk.s4hana.serialization.SapClient;

@WebServlet("/costcenters")
public class CostCenterServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldServlet.class);

    @Override
    protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
            throws ServletException, IOException
    {
        final SapClient sapClient = new SapClient("SAPCLIENT-NUMBER"); // adjust SAP client to your respective S/4HANA system
        try {
            final ErpEndpoint endpoint = new ErpEndpoint(new ErpConfigContext(ErpDestination.getDefaultName(), sapClient));
            final List<CostCenterDetails> costCenters = ODataQueryBuilder
                    .withEntity("/sap/opu/odata/sap/FCO_PI_COST_CENTER", "CostCenterCollection")
                    .select("CostCenterID", "Status", "CompanyCode", "Category", "CostCenterDescription")
                    .build()
                    .execute(endpoint)
                    .asList(CostCenterDetails.class);

            response.setContentType("application/json");
            response.getWriter().write(new Gson().toJson(costCenters));

        } catch(final ODataException e) {
            logger.error(e.getMessage(), e);
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write(e.getMessage());
        }
    }
}

The code is fairly simple. In the servlet GET method a numeric identifier SAPCLIENT is defined, which you must change according to your respective S/4HANA system.  An ErpEndpoint is initialized by using a dynamically created ErpConfigContext, that is provided with an ErpDestination. With the help of the SDK’s ODataQueryBuilder a query is being prepared, build and executed to the endpoint. The query result gets wrapped to a navigatable List of CostCenterDetails . Finally the servlet response is declared as JSON content and transformed as such.

In addition, we require a new class called CostCenterDetails.java which is required to read the OData query response in a type-safe manner. Create this new class in the following location:

./application/src/main/java/com/sap/cloud/sdk/tutorial/CostCenterDetails.java

package com.sap.cloud.sdk.tutorial;
 
import lombok.Data;
 
import com.sap.cloud.sdk.result.ElementName;
 
@Data
public class CostCenterDetails
{
    @ElementName( "CostCenterID" )
    private String id;
 
    @ElementName( "CompanyCode" )
    private String companyCode;
 
    @ElementName( "Status" )
    private String status;
 
    @ElementName( "Category" )
    private String category;
 
    @ElementName( "CostCenterDescription" )
    private String description;
}
  • The SDK @ElementName annotation maps OData values to their corresponding object fields.
  • The Lombok @Data annotation automatically generates the boilerplate code for us:
    • getter and setter methods
    • constructor (for @NonNull fields)
    • hashCode(), equals(...) and toString()

    You can add Lombok to your provided dependencies:

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>​

Deploying the project

Depending on your chosen archetype and SCP setup you can deploy the project on either SCP Neo or SCP CloudFoundry. Don’t forget to change the SAPCLIENT numerical identifier in the servlet source code accordingly.

On SAP Cloud Platform Neo

Now you can deploy your application to your local SCP using the following maven goals:

cd /path/to/firstapp
mvn clean install
mvn scp:clean scp:push -pl application -Derp.url=https://URL

Replace URL with the URL to your SAP ERP system (host and, if necessary, port).
Note: the -pl argument defines the location in which the Maven goals will be executed.

Maven will then prompt you for your username and password that is going to be used to connect to SAP S/4HANA. Alternatively, you can also set these values as command parameters: -Derp.username=USER -Derp.password=PASSWORD

If you now deploy the project and visit the page http://localhost:8080/costcenters you should be seeing a list of cost centers that was retrieved from the ERP system. Note: Please login with test / test).

On SAP Cloud Platform Cloud Foundry

Before you can deploy the new version to Cloud Foundry, you need to supply the destination of your SAP S/4HANA system.

Connecting to SAP S/4HANA from SAP Cloud Platform CloudFoundry

In order to perform queries against your ERP system, you have to inform CloudFoundry about the location of your ERP endpoint. To do this, you need provide an environment variable with the destination configuration. Currently, you have two ways of accomplishing that.

Setting destination as environment variable using CF CLI

 cf set-env firstapp destinations '[{name: "ErpQueryEndpoint", url: "https://HOST:PORT", username: "USER", password: "PASSWORD"}]'

Please change the values HOST,  PORT, USER and PASSWORD accordingly.

Note: You can also add more ERP endpoints to this JSON representation, following the same schema. However, please note that “ErpQueryEndpoint” corresponds to ErpDestination.getDefaultName() we used to create our ErpConfigContext.

Setting destination as user-provided variables using the Cockpit

Alternatively, you can directly set the destination variable using the cockpit. For this, find your application in the SCP Cockpit and provide the variable as user-provided variable:

Deploy

Now you can deploy your application to CloudFoundry using the CloudFoundry CLI (command line interface):

cd /path/to/firstapp
mvn clean install
cf push

If you change the destinations afterwards with one of the two methods outlined above, you need to at least restart (or restage) your application so that the environment setting becomes effective:

cf restart firstapp

Integration test to check CostCenterServlet

To construct an extensible integration test for the newly created CostCenterServlet, the following items will be prepared:

  • Adjustment: Maven pom file
  • New: test class
  • New: JSON Schema for servlet response validation

Adjustment: Maven pom file

First, let’s adjust the Maven pom file of the integrations-tests sub module, by adding a dependency for JSON schema validation:

./integration-tests/pom.xml

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>json-schema-validator</artifactId>
    <version>3.0.3</version>
    <scope>test</scope>
</dependency>

New: test class

Navigate to the integration-tests project and create a new class:

./integration-tests/src/test/java/com/sap/cloud/sdk/tutorial/CostCenterServiceTest.java

package com.sap.cloud.sdk.tutorial;
 
import com.jayway.restassured.RestAssured;
import com.jayway.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 org.slf4j.LoggerFactory;
 
import java.net.URI;
import java.net.URL;
import java.net.URISyntaxException;
 
import com.sap.cloud.sdk.s4hana.connectivity.ErpEdition;
import com.sap.cloud.sdk.s4hana.connectivity.ErpRelease;
import com.sap.cloud.sdk.s4hana.serialization.SapClient;
import com.sap.cloud.sdk.testutil.ErpSystem;
import com.sap.cloud.sdk.testutil.MockUtil;
import com.sap.cloud.sdk.testutil.TestSystem;
 
import static com.jayway.restassured.RestAssured.given;
 
@RunWith( Arquillian.class )
public class CostCenterServiceTest
{
    private static final MockUtil mockUtil = new MockUtil();
    private static final Logger logger = LoggerFactory.getLogger(HelloWorldServiceTest.class);
 
    @ArquillianResource
    private URL baseUrl;
 
    @Deployment
    public static WebArchive createDeployment()
    {
        return TestUtil.createDeployment(CostCenterServlet.class);
    }
 
    @BeforeClass
    public static void beforeClass() throws URISyntaxException
    {
        final SapClient sapClient = new SapClient("SAPCLIENT-NUMBER"); // adjust SAP client to your respective S/4HANA system

        mockUtil.mockDefaults();
        mockUtil.mockErpDestination(new ErpSystem(
            "ERP_TEST_SYSTEM",
            new URI("https://HOST:PORT/"),
            "ERP",
            sapClient));
    }
 
    @Before
    public void before()
    {
        RestAssured.baseURI = baseUrl.toExternalForm();
    }
 
    @Test
    public void testService()
    {
        // JSON schema validation from resource definition
        final JsonSchemaValidator jsonValidator = JsonSchemaValidator.matchesJsonSchemaInClasspath("costcenters-schema.json");
 
        // HTTP GET response OK, JSON header and valid schema
        given().
                get("/costcenters").
            then().
            assertThat().
                statusCode(200).
                contentType(ContentType.JSON).
                body(jsonValidator);
    }
}

Please change the values HOST, PORT and SAPCLIENT-NUMBER accordingly.

What you see here, is the usage of RestAssured on a JSON service backend. The HTTP GET request is run on the local route /costcenters, the result is validated on multiple assertions:

  • HTTP response status code: 200 (OK)
  • HTTP ContentType: application/json
  • HTTP body is valid JSON code, checked with a costcenters-schema.json definition

New: JSON Schema for servlet response validation

Inside the integration-tests project, create a new resource file

./integration-tests/src/test/resources/costcenters-schema.json

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Simple CostCenter List",
  "type": "array",
  "items": {
    "title": "CostCenter Item",
    "type": "object",
    "javaType": "com.sap.cloud.sdk.tutorial.CostCenterDetails",
    "required": ["id", "companyCode"]
  }
}

As you can see, the properties id and companyCode will be marked as requirement for every entry of the expected cost center list. The JSON validator would break the test, if any of the items was missing a required value.

That’s it! You can now start all tests with the default Maven command:

mvn test -Derp.username=USER -Derp.password=PASSWORD

Please change the values USER and PASSWORD accordingly.

If you want to run the tests without Maven, please remember to also use include the parameters.

 


Appendix

Hint: Remember ERP username and password

If you do not want to pass the erp username and password all the time when executing tests or want to execute tests on a CI where more people could see the password in log outputs, you can also provide credentials in a credentials.yml file that the SDK understands.

To do this create the following credentials.yml file in a save location (e.g., like storing your ssh keys in ~/.ssh), i.e., not in the source code repository.

/secure/local/path/credentials.yml

---
credentials:
- alias: "ERP_TEST_SYSTEM"
  username: "user"
  password: "pass"

Afterwards you may pass the credentials file using its absolute path:

mvn test -Dtest.credentials=/secure/local/path/credentials.yml

Troubleshooting

If you are facing troubles when connecting to the OData service, try the following to get more insights into what is happening:

  • Add a logger implementation to the test artifact’s dependencies in order to get more detailed log output during tests: expand the dependencies section of integration-tests/pom.xml with:
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
        <scope>test</scope>
    </dependency>​
  • Supply a custom error handler to the OData query execution. For example, use the following class ./application/src/main/java/com/sap/cloud/sdk/tutorial/ODataV2SimpleErrorHandler.java
    package com.sap.cloud.sdk.tutorial;
    
    import org.slf4j.Logger;
    
    import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
    import com.sap.cloud.sdk.odatav2.connectivity.ErrorResultHandler;
    import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
    
    public class ODataV2SimpleErrorHandler implements ErrorResultHandler<ODataException> {
        private static final Logger logger = CloudLoggerFactory.getLogger(ODataV2SimpleErrorHandler.class);
    
    	public ODataException createError(String content, Object origin, int httpStatusCode) {
    		String msg = String.format(
    				"OData V2 Simple Error Handler received backend OData V2 service response with status %s, full response was %s", 
    				httpStatusCode,
    				content);
    		logger.error(msg);
    		
    		ODataException e = new ODataException();
    		e.setMessage(msg);
    		e.setCode(String.valueOf(httpStatusCode));
    		
    		return e;
    	}
    
    }
    

    and in CostCenterServlet, add the following to the ODataQueryBuilder chain:

    ODataQueryBuilder...
        .errorHandler(new ODataV2SimpleErrorHandler())
        .build()...
To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

  1. Julian Frank

    Hey Alexander,

    first of all: great example on how to use the SDK.

    One question from my side: is there anything more to do to establish the ERP connection when deploying to CloudFoundry? I always receive an UnknownHostException that the name or service is not known.

    Thanks and best regards

    Julian

    (0) 
    1. Philipp Herzig

      Hi Julian,

      this depends a little bit on the setup you intent to use. We have added a small little prerequisite section that explains which configurations are currently available. To which system do you try to talk to? Is it an S/4HANA Cloud system? Or do you try against an S/4HANA on-prem system. Also it matters whether you try to do it from Neo or CloudFoundry (but see yourself in the updated prerequisite section).

      Don’t mind to drop us additional questions.

      Best regards

      Philipp

       

      (0) 

Leave a Reply