Skip to Content
Technical Articles
Author's profile photo Johannes Schneider

Updating an Application to Version 4 of the SAP Cloud SDK

In this blog post, we are going to showcase how to update an existing application to version 4 of the SAP Cloud SDK. The new major version has just been released and comes with a lot of under-the-hood improvements as well as some new features. Since updating a dependency – even to a new major version – shouldn’t come with a lot of effort for consumers, we kept public API as stable as possible to keep the process brief and the effort low. In fact, many consumers will probably not even notice any of the breaking changes we did.

This blog post is for those who do notice that some things have changed and want to see what the development team recommends for the migration.

The Demo Application

Although all changes introduced in version 4 of the SAP Cloud SDK are listed in our Documentation, we still want to put them in a more practical perspective. For that, we are going to migrate a small sample application that uses a slightly outdated SAP Cloud SDK 3 version (3.60.0 to be precise) to the latest one. The demo application has initially been created using the scp-cf-spring SAP Cloud SDK archetype. It’s main purpose is to interact with the SAP Business Partner service.

For that, we have implemented a Spring Controller that looks as follows:

BusinessPartnerContrtoller.java
@RestController
    public class BusinessPartnerController {
        private final BusinessPartnerService service = new DefaultBusinessPartnerService();
    
        @GetMapping("/bupa/addresses")
        public List<BusinessPartnerAddress> getBusinessPartnerAddresses(@RequestParam String destinationName,
                                                                        @RequestParam UUID partnerId) {
            HttpDestination destination = DestinationAccessor.getDestination(destinationName).asHttp();
    
            List<BusinessPartnerAddress> matchingPartners = fetchPartnersWithId(partnerId, destination);
    
            if (matchingPartners.isEmpty()) {
                return Collections.emptyList();
            }
    
            if (matchingPartners.size() > 1) {
                throw new IllegalStateException("More than one business partner found.");
            }
    
            try {
                return matchingPartners.get(0).getBusinessPartnerAddressOrFetch();
            } catch (ODataException e) {
                throw new IllegalStateException("Unable to fetch business partner addresses.", e);
            }
        }
    
        @GetMapping("/bupa/speaksMyLanguage")
        public boolean getBusinessPartnerSpeaksMyLanguage(@RequestParam String destinationName,
                                                          @RequestParam UUID partnerId) {
            HttpDestination destination = DestinationAccessor.getDestination(destinationName).asHttp();
    
            List<BusinessPartnerAddress> matchingPartners = fetchPartnersWithId(partnerId, destination);
    
            if (matchingPartners.isEmpty()) {
                return false;
            }
    
            if (matchingPartners.size() > 1) {
                throw new IllegalStateException("More than one business partner found.");
            }
    
            return businessPartnerSpeaksMyLanguage(matchingPartners.get(0));
        }
    
        private List fetchPartnersWithId(UUID partnerId, HttpDestination destination) {
            try {
                return service
                        .getAllBusinessPartner()
                        .filter(BusinessPartner.BUSINESS_PARTNER_UUID.eq(partnerId))
                        .execute(destination);
            } catch (ODataException e) {
                throw new IllegalStateException("Unable to fetch business partners.", e);
            }
        }
    
        private boolean businessPartnerSpeaksMyLanguage(BusinessPartner partner) {
            String correspondenceLanguage = partner.getCorrespondenceLanguage();
    
            return RequestAccessor
                    .tryGetCurrentRequest()
                    .map(request -> request.getHeaders("Accept-Language"))
                    .map(values -> (List) Collections.list(values))
                    .filter(values -> !values.isEmpty())
                    .getOrElse(Collections.singletonList("en"))
                    .stream()
                    .anyMatch(language -> language.equals("*")
                            || language.substring(0, 2).equalsIgnoreCase(correspondenceLanguage));
        }
    }

In the shown code above, we would like to bring your attention to a few things:

  1. We are using the .execute(HttpDestinationProperties) API in our fetchPartnersWithId method. Therefore, we have to catch and handle the com.sap.cloud.sdk.odatav2.connectivity.ODataException since it’s a checked exception.
  2. In a similar manner, we are using the getBusinessPartnerAddressOrFetch() API to avoid eagerly fetching all addresses when requesting our Business Partners. Inconveniently, this method might also throw an com.sap.cloud.sdk.odatav2.connectivity.ODataException, which needs to be handled once again.
  3. Lastly, we want to get access to the HTTP headers the user sent to our application. For that, we are using the RequestAccessor.tryGetCurrentRequest() API (see correspondenceLanguageEqualsAcceptLanguage method).

Of course, our implementation also has to be tested. Hence, we set up two integration tests that use Wiremock to decouple our controller from the actual SAP S/4HANA Cloud service:

BusinessPartnerControllerTest.java
RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class BusinessPartnerControllerTest {
    private static final String DESTINATION_NAME = "Destination";
    private static final UUID BUSINESS_PARTNER_ID = UUID.fromString("00163e2c-7b39-1ed9-91d0-1182c32dc6ff");
    private static final MockUtil mockUtil = new MockUtil();

    @Rule
    public final WireMockRule BACKEND_SYSTEM = new WireMockRule(wireMockConfig().dynamicPort());


    @Autowired
    private MockMvc mvc;

    @Test
    public void testGetBusinessPartnerAddresses() throws Exception {
        mockDestination();
        mockMetadataLookUp();
        mockBusinessPartnerLookUp();
        mockAddressesLookUp();

        mvc.perform(MockMvcRequestBuilders.get("/bupa/addresses")
                        .queryParam("destinationName", DESTINATION_NAME)
                        .queryParam("partnerId", BUSINESS_PARTNER_ID.toString()))
                .andExpect(status().isOk());
    }

    @Test
    public void testGetBusinessPartnerSpeaksMyLanguage() throws Exception {
        mockDestination();
        mockMetadataLookUp();
        mockBusinessPartnerLookUp();

        mvc.perform(MockMvcRequestBuilders.get("/bupa/speaksMyLanguage")
                .queryParam("destinationName", DESTINATION_NAME)
                .queryParam("partnerId", BUSINESS_PARTNER_ID.toString())
                .header("Accept-Language", "de-DE")).andExpect(status().isOk());
    }

    private void mockDestination() {
        mockUtil.mockDestination(DESTINATION_NAME, URI.create(BACKEND_SYSTEM.baseUrl()));
    }

    private void mockMetadataLookUp() throws IOException {
        String metadata = readResourceFile("service.edmx");

        BACKEND_SYSTEM
                .stubFor(get(urlMatching("/.*API_BUSINESS_PARTNER/\\$metadata"))
                        .willReturn(aResponse().withBody(metadata)));
    }

    private void mockBusinessPartnerLookUp() throws IOException {
        String singleBusinessPartner = readResourceFile("single-business-partner.json");

        BACKEND_SYSTEM
                .stubFor(get(urlMatching("/.*A_BusinessPartner\\?\\$filter.*"))
                        .willReturn(aResponse().withBody(singleBusinessPartner)));
    }

    private void mockAddressesLookUp() throws IOException {
        String businessPartnerAddresses = readResourceFile("business-partner-address.json");

        BACKEND_SYSTEM
                .stubFor(get(urlMatching("/.*to_BusinessPartnerAddress.*"))
                        .willReturn(aResponse().withBody(businessPartnerAddresses)));
    }

    private static String readResourceFile(String fileName) throws IOException {
        return Resources.toString(
                Resources.getResource("BusinessPartnerControllerTest/" + fileName),
                StandardCharsets.UTF_8);
    }
}

Note:
We kept the actual testing logic as brief as possible for demonstration purposes. In proper integration tests, you should make sure that your controller actually returns what you would expect, not just finish with a 200 code.

Once again, please note a few details about these tests:

  1. We are using the SAP Cloud SDK’s MockUtil class to mock a Destination that points to our Wiremock backend service.
  2. For our tests, we not only have to mock the actual retrieval of the Business Partner, but we also have to make sure our Wiremock server handles requests to the /$metadata endpoint and serves a correct .edmx specification (done in the mockMetadataLookUp method).

In the following chapters, we will demonstrate how the mentioned pieces of code – both for the productive controller as well as for the tests – can be transformed with the new major version of the SAP Cloud SDK.

1. Replace Deprecated API Usages

One of our main goals for the new major version of the SAP Cloud SDK was to get rid of outdated APIs and modules. Therefore, chances are high that consumers will face compilation issues after updating to the SAP Cloud SDK 4 in case deprecated APIs are still used. To avoid these issues, we recommend replacing usages of deprecated APIs already before actually updating to version 4.

For that, make sure you are using the latest patch version for the SAP Cloud SDK 3 and check whether your compiler warns about deprecated API usages.

Update Cloud SDK 3 Patch Version
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.sap.cloud.sdk</groupId>
            <artifactId>sdk-bom</artifactId>
-           <version>3.60.0</version>
+           <version>3.75.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
</dependencyManagement>
$ mvn clean install

[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] migration - Root                                                   [pom]
[INFO] migration - Application                                            [jar]
[INFO] migration - Integration Tests                                      [jar]

...

[WARNING] /application/src/main/java/com/sap/example/controllers/BusinessPartnerController.java: /application/src/main/java/com/sap/example/controllers/BusinessPartnerController.java uses or overrides a deprecated API.
[WARNING] /application/src/main/java/com/sap/example/controllers/BusinessPartnerController.java: Recompile with -Xlint:deprecation for details.

...

[WARNING] /integration-tests/src/test/java/com/sap/example/BusinessPartnerControllerTest.java: /integration-tests/src/test/java/com/sap/example/BusinessPartnerControllerTest.java uses or overrides a deprecated API.
[WARNING] /integration-tests/src/test/java/com/sap/example/BusinessPartnerControllerTest.java: Recompile with -Xlint:deprecation for details.

In our demo application, we are indeed seeing warnings about following usages of deprecated APIs:

  • BusinessPartnerController#fetchPartnersWithId: The .execute(HttpDestinationProperties) API is deprecated
  • BusinessPartnerContoller#correspondenceLanguageEqualsAcceptLanguage: The RequestAccessor class is deprecated.
  • BusinessPartnerControllerTest: The MockUtil class is deprecated

Having said that, let us begin refactoring our application to get rid of these warnings first.

Tip: Refactoring

Before refactoring code, make sure you have sufficient test coverage for the classes you are planning to touch. During refactoring, run your tests after each change to check that everything still works as expected. If all tests still pass, commit your changes before continuing so that you can jump back to the last working state in case something goes wrong.

Refactoring is done best in tiny baby-steps instead of one big-bang change.

Replace MockUtil

Let’s first focus on replacing the usage of the deprecated MockUtil class. As you have seen, it is used only to mock a Destination. Luckily, the same behavior can be achieved easily with productive SAP Cloud SDK APIs.

Mocking a Destination
public class BusinessPartnerControllerTest {
    private static final String DESTINATION_NAME = "Destination";
    private static final UUID BUSINESS_PARTNER_ID = UUID.fromString("00163e2c-7b39-1ed9-91d0-1182c32dc6ff");
-   private static final MockUtil mockUtil = new MockUtil();

    @Rule
    public final WireMockRule BACKEND_SYSTEM = new WireMockRule(wireMockConfig().dynamicPort());

    @Autowired
    private MockMvc mvc;

+   @After
+   @Before
+   public void resetDestinationAccessor() {
+       DestinationAccessor.setLoader(null);
+   }

    ...

    private void mockDestination() {
-       mockUtil.mockDestination(DESTINATION_NAME, URI.create(BACKEND_SYSTEM.baseUrl()));
+       DefaultDestinationLoader destinationLoader = new DefaultDestinationLoader().registerDestination(
+               DefaultHttpDestination.builder(BACKEND_SYSTEM.baseUrl()).name(DESTINATION_NAME).build());
+   
+       DestinationAccessor.setLoader(destinationLoader);
    }

With the MockUtil class entirely removed from our tests, we can also get rid of the corresponding dependency.

Integration-Tests/pom.xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
-<dependency>
-    <groupId>com.sap.cloud.sdk.testutil</groupId>
-    <artifactId>testutil-core</artifactId>
-    <scope>test</scope>
-</dependency>

Replace .execute

Replacing the .execute(HttpDestinationProperties) API is almost effortless. In our demonstration, we just have to exchange the call with the stable .executeRequest(HttpDestinationProperties) API.

fetchPartnersWithId With .executeRequest
private List fetchPartnersWithId(UUID partnerId, HttpDestination destination) {
    try {
        return service
                .getAllBusinessPartner()
                .filter(BusinessPartner.BUSINESS_PARTNER_UUID.eq(partnerId))
-               .execute(destination);
+               .executeRequest(destination);
    } catch (ODataException e) {
        throw new IllegalStateException("Unable to fetch business partners.", e);
    }
}

When switching to the new .executeRequest API, you will notice your IDE complaining about the catch (final ODataException e) clause. This is because the .executeRequest method does not throw an com.sap.cloud.sdk.odatav2.connectivity.ODataException.

In fact, the new API does not throw a checked exception at all. Therefore, we can remove the entire try ... catch ... block, so that the method afterwards looks as below:

Final fetchPartnersWithId Implementation
private List fetchPartnersWithId(UUID partnerId, HttpDestination destination) {
    return service
            .getAllBusinessPartner()
            .filter(BusinessPartner.BUSINESS_PARTNER_UUID.eq(partnerId))
            .executeRequest(destination);
}

Replace RequestAccessor

The RequestAccessor provides convenient access to the ServletRequest sent by the user to our application. Unfortunately, the ServletRequest is very limited when it comes to performing (long-running) asynchronous operations as part of the request processing. Therefore, we introduced a new accessor for getting the most prominent part of the ServletRequest: The HTTP headers.

As a consequence, we can replace our usage of the RequestAccessor with the new RequestHeaderAccessor like so:

correspondenceLanguageEqualsAcceptLanguage With RequestHeaderAccessor
private boolean businessPartnerSpeaksMyLanguage(BusinessPartner partner) {
    String correspondenceLanguage = partner.getCorrespondenceLanguage();

-   return RequestAccessor
-           .tryGetCurrentRequest()
-           .map(request -> request.getHeaders("Accept-Language"))
-           .map(values -> (List) Collections.list(values))
+   return RequestHeaderAccessor
+           .tryGetHeaderContainer()
+           .map(headers -> headers.getHeaderValues("Accept-Language"))
            .filter(values -> !values.isEmpty())
            .getOrElse(Collections.singletonList("en"))
            .stream()
            .anyMatch(language -> language.equals("*")
                    || language.substring(0, 2).equalsIgnoreCase(correspondenceLanguage));
}

2. Update the SAP Cloud SDK Version

Updating the SAP Cloud SDK version is very simple and, in fact, has already been done in this blog post. This time, however, we are updating to the new major version:

<dependencyManagement>>
    <dependencies>
        <dependency>
            <groupId>com.sap.cloud.sdk</groupId>
            <artifactId>sdk-bom</artifactId>
-           <version>3.75.0</version>
+           <version>4.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
</dependencyManagement>

Let’s see whether everything is still working:

$ mvn clean verify

...

[ERROR] 'dependencies.dependency.version' for com.sap.hcp.cf.logging:cf-java-logging-support-logback:jar is missing.

...

[ERROR] /application/src/main/java/com/sap/example/controllers/BusinessPartnerController.java:[17,46] package com.sap.cloud.sdk.odatav2.connectivity does not exist
[ERROR] /application/src/main/java/com/sap/example/controllers/BusinessPartnerController.java:[54,24] cannot find symbol
[ERROR]   symbol:   class ODataException

As shown above, there are still a few errors, which require our attention. So let’s fix them one by one.

Declare Dependency Versions

As part of our efforts to clean up the SAP Cloud SDK, we decided to also slim down the sdk-bom. This means that we have reduced the managed dependencies down to those that are actually used in our “core” modules. Therefore, if your application is relying on dependency versions that were previously managed by the sdk-bom, you need to check whether that is still the case.

The compiler error shown in the previous chapter is an indicator that we stopped managing the version of some dependencies our demo application uses. Fixing this is usually rather straight forward.

Just locate the dependency and add the version tag to it:

<dependency>
    <groupId>com.sap.hcp.cf.logging</groupId>
    <artifactId>cf-java-logging-support-logback</artifactId>
+   <version>3.6.3</version>
</dependency>

Tip: Finding Dependency Versions

Finding the latest version of a specific Maven dependency can be done by using the Maven Central Search.

All you have to do is to enter the module identifier, which consists of the group and artifact id. In the above example, searching for com.sap.hcp.cf.logging:cf-java-logging-support-logback will reveal that the latest version is 3.6.3.

Adjust Exceptions

In the previous chapters, we already migrated some exception that were related to the .execute(HttpDestinationProperties) API. Apparently, this was not enough as indicated by the second compiler issue from above.

Luckily, fixing this issue is easy once again. All we need to do is to adjust the import statement in our BusinessPartnerContoller class:

import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
import com.sap.cloud.sdk.cloudplatform.requestheader.RequestHeaderAccessor;
-import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
+import com.sap.cloud.sdk.datamodel.odata.client.exception.ODataException;

This exception might be thrown by the getBusinessPartnerAddressOrFetch() method (used in the addresses endpoint).

The get...OrFetch APIs received an under-the-hood improvement to no longer throw the checked exception we were catching previously. Instead, now they might throw an com.sap.cloud.sdk.datamodel.odata.client.exception.ODataException, which is a runtime exception. Therefore, we are free to apply the same change as for the .execute replacement: We can remove the try ... catch ... block and our application will still compile.

Summary

That’s it!

We successfully migrated our demo application from an outdated SAP Cloud SDK 3 version to the new major version by following these steps:

  1. Update to the latest available minor version for the SAP Cloud SDK 3.
  2. Replace usages of deprecated APIs.
  3. Update the SAP Cloud SDK to the new major version.
  4. Add version tags to dependencies that are no longer managed within the sdk-bom.
  5. Adjust import statements for the ODataException.

Share Your Feedback

Do you have any questions on the new features? Or are you struggling with the upgrade?

Don’t hesitate to share your feedback in the comments below, ask a question in the Q&A or to create a dedicated issue on our GitHub.

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.