Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
sander_wozniak
Participant

Disclaimer: This blog post is only applicable for the SAP Cloud SDK version of at most 2.19.2. We plan to continuously migrate these blog posts into our List of Tutorials. Feel free to check out our updated Tutorials on the SAP Cloud SDK.


This article dives into the inner workings of the cloud platform abstractions provided by the SAP Cloud SDK, which enable the convenient local testing and running of a Cloud application.

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

Goal of this blog post


This blog post takes an in-depth look into the cloud platform abstractions of the SAP Cloud SDK and how they simplify the platform-agnostic testing and development of applications on SAP Cloud Platform for both Neo and Cloud Foundry environments.

For a better understanding of this article, we suggest to read the following tutorials first:

Cloud Platform Abstractions


One of the challenges of efficiently building high-quality cloud applications is the ability to quickly test and run the respective piece of software locally, without the need to perform complex configuration procedures a-priori. Enabling developers to build and test software locally furthermore reduces the need for time-consuming "develop-deploy-test" cycles that are introduced when pushing an application to the Cloud after every small change during development.

In order to address this challenge for building applications on SAP Cloud Platform, the SAP Cloud SDK internally uses several cloud platform abstractions that provide a local environment that is easy to set up, configure, and mock according to the requirements of a developer. While these platform abstractions usually go unnoticed, this article provides deeper insight into what types of abstractions are available and how they are realized on a technical level.

In order to supply appropriate cloud platform abstractions for building cloud applications, we identified common services that are at the heart of multi-tenant applications on SAP Cloud Platform. Note that while this collection does not cover all existing SAP Cloud Platform services, they represent a subset that serves as the foundation of basically any cloud application that integrates with SAP systems or services. Since these services are deeply rooted in the code of such an application, the given abstractions avoid tight coupling to platform specifics that would otherwise be hard to test or adjust.

Before taking a look "under the hood" of the SDK, the following sections outline what cloud platform abstractions are currently available.

Cloud Platform Environment


Each cloud platform offers environmental runtime information, such as an application's name. Since SAP Cloud Platform Neo and Cloud Foundry use different environment variables, the SAP Cloud SDK provides a common interface for accessing certain runtime information in a well-defined manner. The following example shows how to retrieve such information within an application:
import com.sap.cloud.sdk.cloudplatform.CloudPlatformAccessor;
import com.sap.cloud.sdk.cloudplatform.CloudPlatform;
...

final CloudPlatform platform = CloudPlatformAccessor.getCloudPlatform();

// returns the name of the current application
platform.getApplicationName();

Here, the CloudPlatform interface looks as follows:
public interface CloudPlatform
{
String getApplicationName();
}

Note: While the interface currently only includes the name of the application, additional environment variables are planned to be included in an upcoming release of the SAP Cloud SDK.

When running an application in a local container such as the Neo runtime, TomEE, or Tomcat, this information can be adjusted by simply setting the respective environment variables. For the local Neo runtime, these variables can be found here. For Cloud Foundry, environment variables can be found here. For instance, in order to adjust the name of the application when running in a local container, you can set the environment variable HC_APPLICATION for the local Neo runtime (using mvn scp:push) or the variable VCAP_APPLICATION when targeting Cloud Foundry (e.g. using mvn spring-boot:run, mvn tomee:run, or mvn jetty:run).

Within tests, one might consider to adjust such settings by simply specifying environment variables. However, while environment variables are easy to retrieve via System.getenv("variable_name"), setting environment variables requires more effort. When running tests using a mechanism like the Maven Surefire plugin, which is able to spawn a new JVM for each test class, setting environment variables becomes possible, but still remains cumbersome. Therefore, the SAP Cloud SDK provides a more convenient way for mocking the cloud platform environment as follows:
public class MyTest
{
private static final MockUtil mockUtil = new MockUtil();

@BeforeClass
public static void beforeClass()
{
// mocks defaults for the cloud platform environment
// and returns the mocked "CloudPlatform" instance
mockUtil.mockCurrentCloudPlatform();

// mocks a custom application name
mockUtil.mockCurrentCloudPlatform("my-app");
}
}

Destination Configuration & Connectivity


An essential component for building S/4HANA side-by-side extensions is the ability to connect to SAP S/4HANA Cloud and On-Premise. In multi-tenant applications, SAP Cloud Platform offers the transparent retrieval of configuration for connecting to various types of systems within the application without the need to manage the current tenant context. When invoking external systems and services, the multi-tenancy concept of the connectivity services takes care of picking up a tenant's configuration, so that requests are dispatched to the systems and services configured by each tenant dynamically at runtime. Furthermore, SAP Cloud Platform supports various authentication flows such as Basic authentication, OAuth 2.0 flows, as well as principal propagation to S/4HANA Cloud and On-Premise systems, propagating logon information from the IdP-managed users on SAP Cloud Platform to the system-specific business users with their individual authorizations in the S/4HANA system. Within the Neo environment, connectivity services are accessing via a JVM-centric connectivity configuration API, while, within Cloud Foundry, environment variables and platform-provided REST services are offered to access connectivity configuration independent of a certain technology or language such as Java. Given these different concepts, the SAP Cloud SDK provides a common interface for Java development to enable convenient mocking and easy access to connectivity configuration independent of the Cloud environment.

For example, let us assume the given destination configuration in the SAP Cloud Platform cockpit:



Now, in order to access this configuration from within an application, the DestinationAccessor class offers various methods for retrieving the relevant information. For instance, you may want to determine which type of destination you are working with:
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationType;
...

final DestinationType destinationType =
DestinationAccessor.getDestinationType("my_destination");

if( destinationType == DestinationType.HTTP ) {
// HTTP destination
...
} else if( destinationType == DestinationType.RFC ) {
// RFC destination
...
} else {
// other destination types
...
}

Note that for a description of all supported destination types, refer to the respective chapter of the SAP Cloud Platform documentation.

The SAP Cloud SDK provides a GenericDestination class for accessing destination configuration generically via the getGenericDestination() function:
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.GenericDestination;
...

final GenericDestination destination =
DestinationAccessor.getGenericDestination("my_destination");

// obtain destination properties generically:
destination.getPropertiesByName();

In order to provide an improved level of convenience and type-safety, the SAP Cloud SDK further offers interfaces for HTTP and RFC destinations. For example, HTTP destination configuration is accessible via the getDestination() method:
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
...

final Destination destination = DestinationAccessor.getDestination("my_destination");

// retrieve destination properties:
destination.getUri();
destination.getAuthenticationType();
destination.getProxyType();
destination.getProxyConfiguration();

// request headers, including authentication headers
destination.getHeaders(URI.create("http://uri-of-request"));
...

Accordingly, RFC destination configuration can be retrieved via the getRfcDestination() function:
import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
import com.sap.cloud.sdk.cloudplatform.connectivity.RfcDestination;
...

final RfcDestination rfcDestination =
DestinationAccessor.getRfcDestination("my_destination");

// retrieve destination properties:
rfcDestination.getPropertiesByName();

In addition to the ability to retrieve connectivity configuration as outlined above, the SAP Cloud SDK also offers to easily obtain a pre-configured HttpClient for a given HTTP destination:
import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
import org.apache.http.client.HttpClient;
import org.apache.http.HttpResponse;
...

final HttpClient httpClient = HttpClientAccessor.getHttpClient("my_destination");
final HttpResponse response = httpClient.execute(new HttpGet());

Within tests, destinations can easily be mocked using the following mocking methods:
import com.sap.cloud.sdk.testutil.MockUtil;
...

public class MyTest
{
private static final MockUtil mockUtil = new MockUtil();

@Test
public void testMethod()
{
// mocks a HTTP destination using the MockDestinationBuilder
// and returns the mocked Destination instance
final Destination mock = mockUtil.mockDestination(
MockDestination.builder()
.name("my_http_destination")
.uri(URI.create("http://my-service"))
.isTrustingAllCertificates(true)
.build());

// mocks a HTTP destination "ErpQueryEndpoint"
// redirecting to the default ErpSystem
mockUtil.mockErpDestination();

// mocks an RFC destination redirecting to the
// ErpSystem with the given system alias
mockUtil.mockRfcDestination("my_rfc_destination", "systemAlias");

// test your code which uses the DestinationAccessor
// ...
}
}

Tenant Context


In order to be able to access the context of the current tenant within a multi-tenant application, Neo provides a JVM-centric tenant API similar to the connectivity configuration API. In contrast, SAP Cloud Platform Cloud Foundry extracts the tenant information from the "zid" field of the JSON web token (JWT) which is contained in the "Authorization" HTTP header of all requests that are redirected via the application router to the respective backend service. For details on these tokens, please have a look at our blog post on securing Cloud Foundry applications. Again, given the various approaches for retrieving the current tenant, the SAP Cloud SDK offers a common interface:
import com.sap.cloud.sdk.cloudplatform.tenant.TenantAccessor;
import com.sap.cloud.sdk.cloudplatform.tenant.Tenant;
...

final Tenant currentTenant = TenantAccessor.getCurrentTenant();
final String tenantId = currentTenant.getTenantId();

To access the tenant identifier independent of the current cloud platform environment, the following Tenant interface is offered:
public interface Tenant
{
/**
* @return The identifier of the tenant.
*/
String getTenantId();
}

For testing, the MockUtil class supplies mocking a default tenant out of the box when using the mockDefaults() method. In addition, tenants can be mocked flexibly as follows:
import com.sap.cloud.sdk.testutil.MockUtil;
...

public class MyTest
{
private static final MockUtil mockUtil = new MockUtil();

@BeforeClass
public void beforeClass()
{
// invokes mockCurrentTenant() to mock a default tenant
mockUtil.mockDefaults();
}

@Test
public void test()
{
// mocks the current tenant with the given identifier
mockUtil.mockCurrentTenant("my-tenant");

// mocks a tenant with the given identifier
mockUtil.mockTenant("my-tenant");

// sets the current tenant to a previously mocked one
mockUtil.setCurrentTenant("my-tenant");

// clears all mocked tenants
mockUtil.clearTenants();
}
}

User Context


Corresponding to the tenant context, retrieving the context of a user is realized differently across the Neo and Cloud Foundry environments. While Neo uses a Java-based user management API, on Cloud Foundry, this information is encoded within the respective JSON web tokens, for example in the field "user_name" for the name of the currently authenticated user. Information about the current user can be accessed as follows:
import com.sap.cloud.sdk.cloudplatform.security.user.UserAccessor;
import com.sap.cloud.sdk.cloudplatform.security.user.User;
...

final User currentUser = UserAccessor.getCurrentUser();
final String userName = currentUser.getName();

In addition to obtaining general user information, the SAP Cloud SDK also offers a clear interface for checking user authorizations independent of whether they are modeled as roles on SAP Cloud Platform Neo or OAuth 2.0 scopes on Cloud Foundry:
public interface User
{
/**
* @return The name of the user.
*/
String getName();

/**
* @return The locale configured by the user.
*/
Optional<Locale> getLocale();

/**
* Checks whether the user has the given authorization.
*/
boolean hasAuthorization( final Authorization authorization );

/**
* @return The authorizations of the user.
*/
Set<Authorization> getAuthorizations();

/**
* @return The attribute of a user by the attribute name.
*/
Optional<UserAttribute> getAttribute( final String name );
}

Note that authorizations (e.g. roles or OAuth 2.0 scopes) are generically represented using the following interface:
public interface Authorization
{
/**
* @return The name of this authorization.
*/
String getName();
}

Out of the box, implementations of this interface are provided for the notion of Role and Scope.

In order to enable convenient testing with respect to user information, the MockUtil class provides appropriate mocking methods:
import com.sap.cloud.sdk.testutil.MockUtil;
com.sap.cloud.sdk.cloudplatform.security.Role;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.Locale;
import java.util.Map;
import java.util.Set;
...

public class MyTest
{
private static final MockUtil mockUtil = new MockUtil();

@BeforeClass
public void beforeClass()
{
// invokes mockCurrentUser() to mock a default user
mockUtil.mockDefaults();
}

@Test
public void test()
{
// mocks the current user with the given name
mockUtil.mockCurrentUser("my-user");

// mocks a user with the given name, locale, authorizations, and attributes
final Set<Authorization> authorizations = Sets.newHashSet(new Role("my-role"));

final Map<String, UserAttribute> attributes = Maps.newHashMap();
final String attributeName = "some-attribute";
attributes.put(attributeName, new SimpleUserAttribute<>(attributeName, 42));

mockUtil.mockUser("my-user",
Locale.EN,
authorizations,
attributes);

// sets the current user to a previously mocked one
mockUtil.setCurrentUser("my-user");

// clears all mocked users
mockUtil.clearUsers();
}
}

Note: In addition to the aforementioned platform abstractions, the SAP Cloud SDK by default also mocks an audit logger that redirects the log messages to the default logger within tests. Furthermore, a secret store holding passwords and key stores can be mocked to represent the keystore and password storage services on SAP Cloud Platform Neo. For SAP Cloud Platform on Cloud Foundry, both of these services are - at the time of this writing - still under development.

Under the Hood


Given the outlined cloud platform abstractions and the ability to easily mock them within tests, this section takes an in-depth look into the implementation of these platform abstractions within the SAP Cloud SDK, as well as the underlying rationale for designing them in this particular way.

In order to provide such abstractions, several interfaces were designed to reflect the common denominators between SAP Cloud Platform Neo and Cloud Foundry. Subsequently, implementations of these interfaces must be made available for the desired cloud platform. This challenge can be addressed with an inversion-of-control-based mechanism like dependency injection or an alternative option such as the service locator. While both approaches can be valid, we decided to rely on a service locator first and add support for dependency injection later on. This decision allowed us to quickly ensure a high degree of compabitility with complex combinations of a wide variety of libraries and frameworks. Furthermore, setting up and debugging code using dependency injection can be slightly more cumbersome, at least for less experienced Java developers. The resulting need for an explicit dependency to the service locator is acceptable when using well-defined accessor classes. Ensuring testability with accessors is possible by adjusting the code as outlined below. Finally, different cloud platforms come with various platform-specific libraries. In order to avoid conflicts and pollution of the dependencies of a project, it is desirable to use a separate dependency for the implementation of each cloud platform. Having exactly one dependency providing an implementation at a time (e.g., one per supported cloud platform like SAP Cloud Platform Neo or Cloud Foundry), a service locator offers a very straight-forward way of enforcing and communicating this restriction to developers. This helps to avoid configuration errors that can lead to issues that are hard to find and understand.

Update: In addition to accessing cloud platform abstractions via their respective accessor classes, the SAP Cloud SDK will bring support for dependency injection using JSR 330 annotations on all cloud platform abstraction facades starting with release 1.3.0. If you are interesting in learning more about our latest changes, refer to our release notes!

Choosing a service locator comes with certain disadvantages, which in our case was the need to internally rely on static variables that are used for defining the implementation of the interface of a platform abstraction. Therefore, MockUtil instances have to be declared as a static member within a test class. Nevertheless, in our experience, this is usually not a problem since the only actual restriction of this is the limited ability to run methods of a single test class in parallel. The requirement of a parallel test class execution, however, tends to be a less common requirement, since clean test code and modularization allows to carve out sufficiently small test classes that can be already executed in parallel within different JVMs. Accordingly, the use of static member variables is acceptable for the mocking of the cloud platform environment.

In order to enable testing of accessor classes, the following structure is used:
public final class TenantAccessor
{
/**
* Returns the {@link TenantFacade} instance.
* For internal use only.
*/
@Nullable
@Getter
private static TenantFacade tenantFacade =
FacadeLocator.getFacade(TenantFacade.class);

/**
* Returns the {@link TenantFacade} instance.
*/
private static TenantFacade facade()
{
final TenantFacade tenantFacade = TenantAccessor.tenantFacade;

if( tenantFacade == null ) {
throw new ShouldNotHappenException(...);
}

return tenantFacade;
}

/**
* Replaces the default {@link TenantFacade} instance.
* This method is for internal use only.
*/
public static void setTenantFacade( final TenantFacade tenantFacade )
{
TenantAccessor.tenantFacade = tenantFacade;
}

/**
* @return The current {@link Tenant}.
*/
public static Tenant getCurrentTenant()
{
return facade().getCurrentTenant();
}
}

As you can see, a private static instance of a TenantFacade is kept in the accessor:
public interface TenantFacade
{
/**
* Returns the current {@link Tenant}.
*/
Tenant getCurrentTenant();
}

For each environment that is to be supported, this facade has to be implemented. Then, a FacadeLocator class is used to access the current implementation. The FacadeLocator internally uses Java's ServiceLoader, which allows to define the implementing class of the respective facade in a properties file within the library that provides the implementations for a platform. With this, the FacadeLocator can ensure that exactly one implementation exists in the current dependency configuration, avoiding setup issues with mixed platform dependencies. In addition, the ServiceLoader allows to simply declare a dependency to provide the right implementation without any additional configuration. For instance, the following dependency provides tenant interfaces:
<dependency>
<groupId>com.sap.cloud.s4hana.cloudplatform</groupId>
<artifactId>tenant</artifactId>
</dependency>

A developer can now decide to target SAP Cloud Platform Cloud Foundry by choosing:
<dependency>
<groupId>com.sap.cloud.s4hana.cloudplatform</groupId>
<artifactId>tenant-scp-cf</artifactId>
</dependency>

In case SAP Cloud Platform Neo is the target platform, the following dependency is requried:
<dependency>
<groupId>com.sap.cloud.s4hana.cloudplatform</groupId>
<artifactId>tenant-scp-neo</artifactId>
</dependency>

With this, it is also possible to support both platforms within the same Maven project by simply introducing two Maven profiles that allow to specify the target platform:
<profiles>
<profile>
<id>platform-scp-cf</id>
<activation>
<property>
<name>platform</name>
<value>scp-cf</value>
</property>
</activation>
<dependencies>
<dependency>
<groupId>com.sap.cloud.s4hana.cloudplatform</groupId>
<artifactId>scp-cf</artifactId>
</dependency>
</dependencies>
</profile>

<profile>
<id>platform-scp-neo</id>
<activation>
<property>
<name>platform</name>
<value>scp-neo</value>
</property>
</activation>
<dependencies>
<dependency>
<groupId>com.sap.cloud.s4hana.cloudplatform</groupId>
<artifactId>scp-neo</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>

The target can then be chosen by building the project with the following parameter:
mvn clean install -Dplatform=scp-cf

On a side note: You may have noticed that by using the offered cloud platform abstractions as outlined here, it also becomes a lot easier to transition back and forth between the Neo and Cloud Foundry environment of SAP Cloud Platform. However, please keep in mind that security configuration has fundamental differences that must be taken care of in this case.

In order to enable convenient testing as outlined above, within the MockUtil class, first, the FacadeLocator is mocked to have full control over the instances that are retrieved during the lookup in the accessor's static initialization block. Then, mocked facades are provided for each platform abstraction that are returned by the mocked FacadeLocator, allowing to control the behavior of the mocked facades. While this design requires some more implementation effort on the SAP Cloud SDK side, it remains transparent to a developer that uses it.

Conclusion


In this blog post, we highlighted the cloud platform abstractions offered by the SAP Cloud SDK. Apart from an overview of existing abstractions, we outlined examples of how these abstractions enable easy and convenient mocking of a SAP Cloud Platform environment and provided an in-depth view into the inner working of the SDK for realizing these abstractions.

Stay tuned for upcoming blog posts about the SAP Cloud SDK!
10 Comments