The following steps will explain how to create the very first Java project to call OData services using the SAP S/4HANA Cloud SDK. If you want to follow this tutorial, we highly recommend checking out the first part of this blog series. You will not need any additional software as the server will run on your local machine.

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.

Note: This tutorial requires access to an SAP ERP system.

Write the CostCenterServlet

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

./firstapp-app/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.cloudplatform.connectivity.exception.DestinationException;
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.connectivity.exception.QueryExecutionException;
import com.sap.cloud.sdk.s4hana.connectivity.exception.QuerySerializationException;
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 QueryExecutionException | QuerySerializationException | DestinationException 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.

For the ODataQueryResult it is necessary to wrap the retrieved information to a POJO:

./firstapp-app/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 Lombok @Data annotation automatically generates the boilerplate code for us:
    • getter and setter methods
    • constructor (for @NonNull fields)
    • hashCode(), equals(...) and toString()
  • The SDK @ElementName annotation is mapping the OData values to their corresponding object fields.

If you now deploy the project and visit the page .../costcenters you should be seeing a list of cost centers that was retrieved from the ERP system. For how this is done, please follow the next step.

(For the initial login test / test is being used.)

 

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 SCP Neo

To run the OData queries against your ERP system, you need to configure the location of your ERP endpoint. To do this, you need to modify the application Maven project file.

./firstapp-app/pom.xml

<properties>
...
    <erp.username></erp.username>
    <erp.password></erp.password>
    <erp.url>https://HOST:PORT/</erp.url>
</properties>

Please change the values HOST and  PORT accordingly. If you leave erp.username or erp.password empty, you will automatically get asked during the deployment procedure.

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 firstapp-app

Note: the -pl argument defines the location in which the Maven goals will be executed.

If you have not entered your credentials into the ./firstapp-app/pom.xml you will get asked by Maven for the username and password.

You can also set these values as command parameters: -Derp.username=USER -Derp.password=PASSWORD

 

On SCP 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 to modify the manifest.yml.

The destinations environment variable holds the information about the ERP endpoint. Change the url, username and password, so that it conforms to your ERP endpoint. The new entry should then look like this:

./firstapp-app/manifest.yml

env:
  TARGET_RUNTIME: tomee
  destinations: '[{name: "ErpQueryEndpoint", url: "https://HOST:PORT", username: "USER", password: "PASSWORD"}]'

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

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

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

cd /path/to/firstapp
cf push -f firstapp-app/manifest.yml

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:

./firstapp-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:

./firstapp-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 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.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()
    {
        mockUtil.mockDefaults();
 
        try {
            mockUtil.mockErpDestination(new ErpSystem(
                    "ERP_TEST_SYSTEM",
                    new URI("https://HOST:PORT/"),
                    new SapClient("SAPCLIENT-NUMBER")));
        }
        catch(final Exception e) {
            logger.error("Could not mock ERP destination for test.", e);
        }
    }
 
    @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

./firstapp-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 want to save the ERP username and password parameter, you can change the following lines for the maven-surefire-plugin plugin configuration:

./firstapp-integration-tests/pom.xml

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
 
    <!-- ERP Endpoint credentials for tests -->
    <configuration>
        <systemPropertyVariables>
            <erp.username>USER</erp.username>
            <erp.password>PASSWORD</erp.password>
        </systemPropertyVariables>
    </configuration>
</plugin>

 

 

To report this post you need to login first.

1 Comment

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) 

Leave a Reply