Wednesday, 6 June 2018

Migration Guide: Moving to Version 2.0.0 of the SAP S/4HANA Cloud SDK

With the release of version 2.0.0 of the SAP S/4HANA Cloud SDK, along with dropping support for Java 7, we were able to incorporate several improvements and add some great new features to the Java libraries of the SDK based on your feedback! Along with the modernization of the technology stack, we took the chance to clean up our code base and remove obsolete and unwanted functionality that – in a few cases – required breaking changes to our API.

In order to provide you a smooth transition to our latest release, this migration guide aims to help you in moving your project to version 2.0.0 of the SAP S/4HANA Cloud SDK. In particular, this blog post will outline the relevant changes in detail and offer code examples that demonstrate the steps that are necessary for a successful and seamless migration.

Leverage Java 8


Move from Guava’s to Java’s Functional Primitives

In addition Replace Guava’s functional primitives like FluentIterable, Optional, Function, Predicate and Supplier with corresponding native Java 8 types. This usually means that you have to adjust the existing Guava imports and replace them with their Java 8 counterparts. For instance, differences between Guava’s and Java’s Optional are described in the Guava Javadoc. An example for the required changes is depicted below.

Previously

import com.google.common.base.Optional;
...

ProxyConfiguration proxyConfiguration =
    DestinationAccessor.getDestination("dest")
        .getProxyConfiguration()
        .orNull();

Now

import java.util.Optional;
...

ProxyConfiguration proxyConfiguration =
    DestinationAccessor.getDestination("dest")
        .getProxyConfiguration()
        .orElse(null);

The interface Executable no longer inherits from Callable and is now annotated with @FunctionalInterface. RequestContextExecutor has been adjusted accordingly to accept both Callable and Executable as Lambda expressions. This enables a simpler use of this class:

Previously

new RequestContextExecutor().execute(new Executable() {
    @Override
    public void execute() throws Exception {
        ...
    }
);

Now

new RequestContextExecutor().execute(() -> {
   ...
});

Move from Joda-Time to Java’s Date and Time

With version 2.0.0, the SDK replaces the use of Joda-Time as well as the legacy Java 7 date and time classes with the corresponding  java.time classes of Java 8. In particular, in the Java virtual data model (VDM), the deprecated Calendar type has been replaced with LocalDateTime, LocalTime, and ZonedDateTime. When adapting your code, you will be able to see the new expected type on the respective entity fields.

Change Obsolete or Unwanted Behavior


CacheKey

The potentially risky best-effort cache keys and HystrixUtil command key methods have been removed. The CachingCommand now uses CacheKey.ofTenantAndUserIsolation() by default. While the best-effort isolation allowed to generically construct a cache key with the highest possible level of isolation, it beared the potential risk of an unexpected cache isolation, for example due to a security configuration mistake. Therefore, we decided to require an explicit specification of the expected isolation instead. If the requested isolation level cannot be provided, an exception is thrown instead of silently reverting to a weaker isolation.

Previously

// weaker than expected isolation possible at runtime
CacheKey cacheKey = CacheKey.newBestEffortIsolatedKey();

Now

 // explicitly specify and enforce isolation at runtime
CacheKey cacheKey = CacheKey.ofTenantAndUserIsolation();

MockUtil

In previous versions, the SDK used to implicitly mock an audit log with calling MockUtil.mockDefaults(). Since this represented a deviation of behavior between tests, a local container, and code that runs on SAP Cloud Platform, the audit log is no longer mocked by this method. In contrast, if you need to mock an audit log, you now have to explicitly callMockUtil.mockAuditLog().

Previously

private static final MockUtil mockUtil = new MockUtil();

@Test
public void test()
{
    mockUtil.mockDefaults(); // implicitly mocked an audit log
}

Now

private static final MockUtil mockUtil = new MockUtil();

@Test
public void test()
{
    mockUtil.mockDefaults(); // no longer mocks an audit log
    mockUtil.mockAuditLog(); // mock the audit log explicitly now
}

Clean up API of SAP S/4HANA Cloud SDK


Cloud Platform Abstractions

Within version 2.0.0, the APIs of the Cloud Platform abstractions have been improved with accessor methods returning Optionals in TenantAccessor, UserAccessor, and SecretStoreAccessor. For example, in order to retrieve the current tenant, previous versions only offered a method getCurrentTenant() that could throw a TenantNotAvailableException. Since the flow of an application can depend on the availability of a tenant, the previous APIs required developers to rely on the anti-pattern of using exceptions for controlling the code flow. Therefore, the SDK now offers new methods such as getCurrentTenantIfAvailable() which avoid the use of an exception here.

Previously

Tenant getCurrentTenant()
    throws TenantNotAvailableException,
        TenantAccessException;
Now

Optional<Tenant> getCurrentTenantIfAvailable()
    throws TenantAccessException;
 
Tenant getCurrentTenant()
    throws TenantNotAvailableException,
        TenantAccessException;

ErpConfigContext and ErpEndpoint

For historic reasons, previous versions of the SDK offered the class ErpEndpoint as well ErpConfigContext. Since both classes eventually turned out to be very similar, the purpose and use of both classes became confusing for developers. In particular, since an ErpEndpoint always had to be constructed from a given ErpConfigContext, developers often had to wrap an ErpConfigContext into an ErpEndpoint without an obvious reason. Therefore, we decided to only offer ErpConfigContext to represent the context for transparently connecting to SAP S/4HANA systems via different protocols.

Previously

new DefaultBusinessPartnerService()
    .getAllBusinessPartner()
    .execute(new ErpEndpoint(new ErpConfigContext()));

Now

new DefaultBusinessPartnerService()
    .getAllBusinessPartner()
    .execute(new ErpConfigContext());

RequestContext Handling

In previous versions of the SAP S/4HANA Cloud SDK, SDK-internal RequestContextListeners were initialized using ServletContextListeners. The downside of this was that RequestContextListeners would not be initialized when being used within certain test setups or when using another ServletContextListener. Therefore, the SDK-internal RequestContextListener instances are now initialized using the ServiceLoader mechanism, allowing developers to more easily use SDK components within tests and during application startup.

Previously

@WebListener
public class MyServletContextListener implements ServletContextListener
{
    @Override
    public void contextInitialized( ServletContextEvent servletContextEvent )
    {
        // explicitly register RequestContextListeners
        new RequestContextExecutor().withListeners(
            new DestinationsRequestContextListener(),
            new ScpNeoDestinationsRequestContextListener(),
            new TenantRequestContextListener(),
            new UserRequestContextListener()
        ).execute(...);
    }

    ...
}

Now

@WebListener
public class MyServletContextListener implements ServletContextListener
{
    @Override
    public void contextInitialized( ServletContextEvent servletContextEvent )
    {
        new RequestContextExecutor().execute(...);
    }

    ...
}

Furthermore, the method requestContextExecutor() has been removed from the class MockUtil. It is now possible to simply use new RequestContextExecutor() instead.

Previously

private static final MockUtil mockUtil = new MockUtil();

@Test
public void test()
{
    mockUtil.requestContextExecutor().execute(...);
}

Now

@Test
public void test()
{
    new RequestContextExecutor().execute(...);
}

RequestContext now uses a custom class Property to represent a property with a certain value or exception if the value could not be determined.

Previously

Optional<Object> getProperty( String name )
    throws RequestContextPropertyException;

Now

Optional<Property<?>> getProperty( String name )
    throws RequestContextPropertyException;

Boostrap Hystrix on SAP Cloud Platform Neo

Previous versions of the SDK relied on a ServletContextListener to register a Neo-specific concurrency strategy for Hystrix in ScpNeoHystrixBootstrapListener. This is now obsolete since the SDK internally uses the standard Java ServiceLoader mechanism offered by Hystrix to register this strategy.

Previously

@WebListener
public class MyServletContextListener implements ServletContextListener
{
    @Override
    public void contextInitialized( ServletContextEvent servletContextEvent )
    {
        // ensure correct use of HystrixConcurrencyStrategy on Neo
        new ScpNeoHystrixBootstrapListener().bootstrap();

        ...
    }

    ...
}

Now

@WebListener
public class MyServletContextListener implements ServletContextListener
{
    @Override
    public void contextInitialized( ServletContextEvent servletContextEvent )
    {
        // nothing special required here anymore
        ...
    }

    ...
}

Apply Consistent Naming


Constructor Methods

In order to align the naming of constructor methods in the the SAP S/4HANA Cloud SDK with common terminology of Java 8, we simplified CacheKey with unified of constructor methods.

Previously

CacheKey.newGlobalKey();
CacheKey.newTenantIsolatedKey();
CacheKey.newUserIsolatedKey();
CacheKey.newUserOrTenantIsolatedKey();
CacheKey.newTenantIsolatedOrGlobalKey();
CacheKey.newBestEffortIsolatedKey();
// and many more

Now

CacheKey.ofNoIsolation();
CacheKey.ofTenantIsolation();
CacheKey.ofTenantAndUserIsolation();
CacheKey.of(nullableTenantId, nullableUserName);

Furthermore, unified namings have been applied to ConvertedObject with of constructor methods and fixing the misspelled term “convertable”, replacing it with the corrected term “convertible”.

Previously

ConvertedObject.fromConverted(nullableValue);
ConvertedObject.fromNull();
ConvertedObject.notConvertable();

Now

ConvertedObject.of(nullableValue);
ConvertedObject.ofNull();
ConvertedObject.ofNotConvertible();

Virtual Data Model

The methods for retrieving the value of association properties are no longer named getXOrNull. Instead, these methods are now called getXIfPresent to better reflect the return type Optional.

Previously

Optional<Customer> customer =
    businessPartner.getCustomerOrNull(); // returns Optional, not null

Now

Optional<Customer> customer = businessPartner.getCustomerIfPresent();

Improvements


Mocking of Tenant and User

The SDK now offers the environment variables USE_MOCKED_TENANT and USE_MOCKED_USER for fine-granular control over mocking of the current tenant and user at runtime.

◈ When these environment variables are set to true, the application will use a mocked tenant (with an empty tenant identifier "") and/or mocked user (with an empty user name "") instead of an actual tenant or user, respectively.

◈ This should not be used in production, but only for testing purposes. As a consequence, security errors are logged when you use these environment variables.

◈ The behavior is similar to the ALLOW_MOCKED_AUTH_HEADER environment variable on Cloud Foundry, but in contrast to this variable it works on both Neo and Cloud Foundry and takes precedence over an actual tenant and user.

Mocking of Cloud Platform Abstractions

MockUtil now uses the AuditLog, CloudPlatform, GenericDestination, Destination, RfcDestination, Tenant, User, and SecretStore type of the respective Cloud platform for mocking based on the chosen dependency of Cloud Foundry or Neo. This makes it easier to run the same code within tests, a local container, and on SAP Cloud Platform and allows to more easily test application logic that relies on the environment-specific implementation of a platform abstraction.

Previously

private static final MockUtil mockUtil = new MockUtil();

@Test
public void test()
{
    final Tenant currentTenant = mockUtil.mockCurrentTenant();

    // currentTenant was mocked as an instance of Tenant
    assertTrue( currentTenant instanceof ScpCfTenant ); // would fail
}

Now

private static final MockUtil mockUtil = new MockUtil();

@Test
public void test()
{
    final Tenant currentTenant = mockUtil.mockCurrentTenant();

    // currentTenant is now mocked as an instance of ScpCfTenant
    assertTrue( currentTenant instanceof ScpCfTenant ); // works!
}

No comments:

Post a Comment