Skip to Content
Technical Articles

Step-by-Step: Consume REST Service API with SAP Cloud SDK

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.

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

Introduction

We have already seen the SAP Cloud SDK providing capabilities for conveniently consuming OData and SOAP based API services. But some application may target a web service, which serves resources from an REST based endpoint, instead of OData or SOAP. If the REST based endpoint is well-defined, its specification can be declared as OpenAPI.

In this step-by-step guide we will choose a RESTful API from the SAP API Business Hub, take the attached OpenAPI interface file and generate Java code from it. By using the latest Maven project archetype of SAP Cloud SDK, we will have a well prepared start to implement and deploy a new application to the SCP, capable of consuming an OpenAPI service. We will setup a destination to the API service to test and proof the new application.

In the upcoming steps, we will use the Monitoring v2 service on SCP Neo. It provides up-to-date runtime metrics to connected applications and processes. Its API will be consumed by a newly created application running on SCP Cloud Foundry. However, the general strategy of this guide can be used for either platform deployment.

Prerequisites:

We assume you have completed the tutorial “HelloWorld on SCP CloudFoundry”, such that the following requirements are already met:

  • You have an SCP account, which is able to deploy simple Java web applications.
  • Java, Maven and an IDE of your choice are installed and up-to-date.
  • Cloud Foundry command line interface is installed, such that you can run cf version without an error.

Recommendations:

If you are new to the SAP Cloud SDK, you may want to start on the Cloud SDK Overview page. For an introduction on OpenAPI and SAP Cloud SDK, we recommend the post Access any REST service with Cloud SDK. Find more courses and guides in the official tutorials section.

Note: This tutorial does not require access to an SAP S/4HANA system.

 

Build an application powered by OpenAPI and Cloud SDK

Steps:

  1. Choose a REST service
  2. Build a new Spring application
  3. Enable OpenAPI code generation
  4. Run the code generation
  5. Setup the application
  6. Setup and run a mock server test
  7. Obtain credentials from SCP Neo
  8. Prepare configuration on SCP Cloud Foundry
  9. Deploy application
  10. Test

 

Step 1: Choose a service and download the OpenAPI interface file

In the beginning it is important to make sure the web service, which will be consumed, is actually offering an OpenAPI interface file. We need a JSON or YAML file, as a formal specification of the API. For this step-by-step guide we assume the “Monitoring v2” as target API.

Excursus: Explore a REST Service on SAP API Business Hub

We will now browse and download an OpenAPI file for “Monitoring v2”.

  1. Visit the SAP API Business Hub.Its services are declared as either ODATA, SOAP or REST. If you are interested in OData and SOAP, please take a look at what the SAP Cloud SDK can provide as additional functionality.
  2. Search for “Monitoring v2” and click on the respective row.On this API reference, you can find a documentation of multiple interactive endpoints. By exploring the colored fields, you can test the API and investigate request parameters and expected response bodies. With this Swagger UI -like interface you can test API calls against a sandbox system.
  3. Change to the “Details” page.Here you can find meta information about an API, as well as available URLs of the API. They heavily depend on your target landscape. Also details about authentication and links to in-depth documentation are referenced on this page.
  4. Click the “Download Specification” button
    This will trigger the download of the OpenAPI file to this API. The specification can be saved as JSON or YAML. Both of them can be parsed later by the code generator. Let’s choose YAML.

 

Step 2: Create a new Spring application

In order to quickly get started with a new web application project, let’s use the Spring Boot archetype prepared by SAP Cloud SDK. Open the command line and run the following Maven command to create a convenient multi-module project at the current directory:

mvn archetype:generate -DarchetypeGroupId=com.sap.cloud.s4hana.archetypes -DarchetypeArtifactId=scp-cf-spring -DarchetypeVersion=RELEASE

Note: Please change the RELEASE reference at the end of the command to your preferred version. This tutorial uses org.example.rest as base package.

A project based on the Spring Boot archetype is generated, containing the following elements:

  • ./
    • application/
    • cx-server/
    • integration-tests/
    • uni-tests/
    • pom.xml

If you are further interested in the details of the project structure, please find the related tutorial with in-depth information on the similar TomEE archetype.

 

Step 3: Enable OpenAPI code generation

  1. To enable the automatic code generation with OpenAPI, please rename and move the previously downloaded YAML file into the folder:
    ./application/src/main/resources/api.yaml
    
  2. Open the ./application/pom.xml file and add the following <plugin> next to the other declared plugins.
    <plugins>
        ...
    
        <plugin>
            <groupId>org.openapitools</groupId>
            <artifactId>openapi-generator-maven-plugin</artifactId>
            <version>3.3.4</version>
            <executions>
                <execution>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                    <configuration>
                        <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
                        <generatorName>java</generatorName>
                        <configurationFile>${project.basedir}/src/main/resources/api-options.json</configurationFile>
                        <generateApiTests>false</generateApiTests>
                        <generateModelTests>false</generateModelTests>
                        <generateApiDocumentation>false</generateApiDocumentation>
                        <generateModelDocumentation>false</generateModelDocumentation>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
    ​

    Note: You can easily change and update the plugin version to a recent state. Since this tool is still in ongoing development and gets improved on a regularly basis, we recommend using the latest version. You can find it in the Maven central repository.

  3. For later compilation of the generated code, an additional dependency needs to be declared in the same pom.xml file. Again, please feel encouraged to replace the version with the latest version from the Maven central repository.
    <dependencies>
        ...
    
        <!-- due to annotations in the generated code -->
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.22</version>
        </dependency>
    </dependencies>
  4. You may have already noticed the reference of /src/main/resources/api-options.json. In order to improve the readability of the code generator setup, we delegate some settings to a separate JSON file. Create the api-options.json with the following content, and put it next to api.yaml:
    {
      "java8" : true,
      "dateLibrary" : "java8",
    
      "modelPackage" : "org.example.rest.monitoring.model",
      "apiPackage" : "org.example.rest.monitoring.api",
      "invokerPackage" : "org.example.rest.monitoring.invoker",
    
      "serializableModel" : true,
      "withXml" : false,
      "booleanGetterPrefix" : "is",
      "useRuntimeException" : false,
      "hideGenerationTimestamp" : true,
    
      "library" : "resttemplate",
      "sourceFolder" : "/src/main/java"
    }

    You can modify it to your needs. But we recommend leaving the value "library" : "resttemplate" intact, to ensure support with SAP Cloud SDK.

Your application module will now look like this – with the new files highlighted:

Once the modifications are done, we are ready to start the code generation.

 

Step 4: Run the code generation

  1. Use the command line again, to execute the following Maven command:
    mvn generate-sources
    

    This phase will traverse the project module hierarchy and eventually starts the code generator in application.

  2. Once the Maven process finishes, you can find the generated code in the target directory of the application module:
    ./application/target/generated-sources/openapi/src/main/java/
    

    Depending on the library template chosen in the generator settings, the generated code will differ both, for API and invoker classes.

    Note: Make sure that your IDE identifies the files as generated sources, such that the classes can be resolved from your editors classpath.

  3. If you find your generated API methods having incorrect letter casing, it is likely due to an improper API naming convention from the API file. You could either fix the naming manually in the original API file or use an automatic routine to fix the specification during Maven build. Although we do not recommend manually changing generated code, you could still fix the issue by adjusting the values for operationId elements in the api.yaml. We advise using an automatic approach to handle the issue, please see the Appendix in the adjacent post Access any REST service with SAP Cloud SDK.

Your application module will now have generated sources:

Take a look into the Java files of the generated packages:

  • api contains a dynamic set of classes, depending on the OpenAPI interface. Each OpenAPI endpoint is mapped to a class, which can be instantiated. Given the interface, each API class features the respective operations as methods. The return type of these methods are defined in the model package.
  • invoker holds static classes for API querying, e.g. ApiClient and helper classes.
  • model incorporates classes, which are mapped to entities defined by the API. They enable the type-safe usage of API responses.

 

Step 5: Setup the application

  1. Prepare a dedicated Destination type
    Create a new java class MonitoringDestination.java to serve as placeholder for the destination identifier.

    package org.example.rest;
    
    import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationDeclarator;
    
    public class MonitoringDestination extends DestinationDeclarator {
        public final static String DESTINATION_NAME = "MonitoringEndpoint";
    
        public MonitoringDestination() {
            super(DESTINATION_NAME);
        }
    }
    
  2. Implement the OpenAPI related application beans
    Create a new class ConfigurationMonitoring.java to serve beans with request scope, to ensure tenant and user separation. With the MonitoringDestinationreference we can resolve service paths and the HttpClient, which automatically resolves authorization headers for us.

    package org.example.rest;
    
    import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
    import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
    import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
    import org.apache.http.client.HttpClient;
    import org.example.rest.monitoring.invoker.ApiClient;
    import org.springframework.context.annotation.*;
    import org.springframework.http.client.BufferingClientHttpRequestFactory;
    import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
    import org.springframework.web.client.RestTemplate;
    import org.springframework.web.context.WebApplicationContext;
    
    import java.net.URI;
    import java.net.URISyntaxException;
    
    @Configuration
    public class ConfigurationMonitoring {
        @Bean
        @Primary
        @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
        public ApiClient createApiClient() throws URISyntaxException
        {
            // resolve destination
            final Destination destination = DestinationAccessor.getDestination(MonitoringDestination.DESTINATION_NAME);
    
            // instantiate RestTemplate and ApiClient
            final RestTemplate restTemplate = createRestTemplate();
            final ApiClient apiClient = new ApiClient(restTemplate);
    
            // set root of API Client base path
            final URI uri = destination.getUri();
            final URI path = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null);
            apiClient.setBasePath(path.toString());
            return apiClient;
        }
    
        @Bean
        @Primary
        @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
        public RestTemplate createRestTemplate()
        {
            // create new HttpClient for destination
            final HttpClient httpClient = HttpClientAccessor.getHttpClient(MonitoringDestination.DESTINATION_NAME);
    
            // instantiate template with prepared HttpClient, featuring repeated response reading
            final RestTemplate restTemplate = new RestTemplate();
            final HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
            httpRequestFactory.setHttpClient(httpClient);
            restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory));
            return restTemplate;
        }
    }
    

    With @Primary we signal Spring to select this instantiation method with priority over the methods provided by default from the generated OpenAPI code.

    Note: If you plan to not use the bean or need access in a threaded environment (e.g. Hysterics commands), then you can just call the methods directly.

  3. Implement a response model
    Create a new class models/MonitorResponse.java to hold the values prepared by the controller.

    package org.example.rest.models;
    
    import com.fasterxml.jackson.annotation.JsonProperty;
    import javax.annotation.Nonnull;
    import org.example.rest.monitoring.model.AccountsAppsMetricsresponse;
    
    import java.util.List;
    
    public class MonitorResponse {
        @JsonProperty("monitor")
        private final List<AccountsAppsMetricsresponse> monitor;
    
        public MonitorResponse( @Nonnull final List<AccountsAppsMetricsresponse> monitor ) {
            this.monitor = monitor;
        }
    }
    
  4. Implement a service controller
    Create a new class controllers/MonitorController.java to listen on requests to our application. To keep the example simple, we are going to simply wrap the API response into our own model class MonitorResponse. You can later manipulate the result list and or use a different response model for further data processing.

    package org.example.rest.controllers;
    
    import javax.annotation.Nonnull;
    
    import org.example.rest.models.MonitorResponse;
    import org.example.rest.monitoring.api.JavaApplicationMetricsApi;
    import org.example.rest.monitoring.invoker.ApiClient;
    import org.example.rest.monitoring.model.AccountsAppsMetricsresponse;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.net.URISyntaxException;
    import java.util.List;
    
    @RestController
    @RequestMapping("/monitor")
    public class MonitorController {
        private final ApiClient apiClient;
    
        public MonitorController( @Nonnull final ApiClient apiClient ) {
            this.apiClient = apiClient;
        }
    
        @RequestMapping(method = RequestMethod.GET)
        public ResponseEntity<MonitorResponse> getMonitor(@RequestParam final String account, @RequestParam final String app) throws URISyntaxException {
            final List<AccountsAppsMetricsresponse> result = new JavaApplicationMetricsApi(apiClient).getAccountsSubaccountNameAppsAppNameMetrics(account, app);
            return ResponseEntity.ok(new MonitorResponse(result));
        }
    }
    

    As you see, our controller is listening for a GET /monitor request. Upon execution an instance of ApiClient is injected as part of the class constructor. Spring will resolve the bean from our previously introduced ConfigurationMonitoring configuration.

Your application module now contains classes for controller and model:

 

Step 6: Setup and run a mock server test

It’s time to verify the newly created code with an elaborate mock server test. We are now working in the integration-tests module.

  1. Create a new resource file
    ./integration-tests/src/test/resources/mocked_monitoring_response.json
    

    The following JSON payload is going to be used as mocked server response. Please note, while this example only features three metrics, in reality there are many more records for a given process.

    [
      {
        "account": "d012345trial",
        "application": "sampleapp",
        "state": "Ok",
        "processes": [
          {
            "process": "0123456789abcdef",
            "state": "Ok",
            "metrics": [
              {
                "name": "Used Disc Space",
                "state": "Ok",
                "value": 57,
                "unit": "%",
                "warningThreshold": 90,
                "errorThreshold": 95,
                "timestamp": 1551950283000,
                "output": "DISK OK - free space: / 3041 MB (39% inode=79%); /var 1459 MB (76% inode=98%); /tmp 1844 MB (96% inode=99%);",
                "metricType": "rate",
                "min": 0,
                "max": 8063
              },
              {
                "name": "Requests per Minute",
                "state": "Ok",
                "value": 0,
                "unit": "requests",
                "warningThreshold": 0,
                "errorThreshold": 0,
                "timestamp": 1551950284000,
                "output": "JMX OK - RequestsCountMin = 0 ",
                "metricType": "performance",
                "min": 0,
                "max": 0
              },
              {
                "name": "CPU Load",
                "state": "Ok",
                "value": 10,
                "unit": "%",
                "warningThreshold": 80,
                "errorThreshold": 90,
                "timestamp": 1551950283000,
                "output": "OK CPUValue: 10 (W> 80, C> 90) ",
                "metricType": "performance",
                "min": 0,
                "max": 0
              }
            ]
          }
        ]
      }
    ]
    
  2. Create a new test file
    ./integration-tests/src/test/java/[...]/MonitoringControllerLocalTest.java
    

    The following test class currently contains a test, whether the contents of the OpenAPI endpoint is correctly wrapped and forwarded by our controller.

    package org.example.rest;
    
    import com.github.tomakehurst.wiremock.junit.WireMockRule;
    import com.sap.cloud.sdk.cloudplatform.servlet.RequestContextExecutor;
    import com.sap.cloud.sdk.testutil.MockUtil;
    import org.apache.commons.io.Charsets;
    import org.apache.commons.io.IOUtils;
    import org.junit.Before;
    import org.junit.BeforeClass;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    
    import java.io.IOException;
    import java.net.URI;
    
    import static com.github.tomakehurst.wiremock.client.WireMock.*;
    import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
    import static java.lang.Thread.currentThread;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    @RunWith( SpringRunner.class )
    @WebMvcTest
    public class MonitoringControllerLocalTest
    {
        private static final String TEST_API_BASE_PATH = "/monitoring/v2";
        private static final String TEST_SCP_ACCOUNT_ID = "d012345trial";
        private static final String TEST_SCP_APPLICATION = "sampleapp";
    
        private static final MockUtil mockUtil = new MockUtil();
    
        @Rule
        public final WireMockRule wireMockServer = new WireMockRule(wireMockConfig().dynamicPort());
    
        @Autowired
        private MockMvc mvc;
    
        private static String TEST_API_RESPONSE;
    
        @BeforeClass
        public static void beforeClass() throws IOException {
            mockUtil.mockDefaults();
    
            // load expected response from mocked API service
            TEST_API_RESPONSE = IOUtils.toString(currentThread().getContextClassLoader().getResourceAsStream("mocked_monitoring_response.json"), Charsets.UTF_8);
        }
    
        @Before
        public void mockServerResponses() {
            stubFor(
                get(urlPathMatching(TEST_API_BASE_PATH + "/accounts/(\\w+)/apps/(\\w+)/metrics"))
                    .willReturn(okJson(TEST_API_RESPONSE)));
        }
    
        @Test
        public void testMonitor() throws Exception
        {
            final URI apiUrl = new URI(wireMockServer.baseUrl() + "/" + TEST_API_BASE_PATH);
            mockUtil.mockDestination(MonitoringDestination.DESTINATION_NAME, apiUrl, null);
    
            new RequestContextExecutor().execute(() -> {
                mvc.perform(MockMvcRequestBuilders.get("/monitor").param("account", TEST_SCP_ACCOUNT_ID).param("app", TEST_SCP_APPLICATION))
                    .andExpect(status().isOk())
                    .andExpect(content().json("{\"monitor\":"+TEST_API_RESPONSE+"}"));
            });
        }
    }
    

    Note: You can easily modify the chain of .andExpect(...) statements to improve test assertions.

    During the test the following things happen:

    • Since the test is run with SpringRunner and annotated with @WebMvcTestwe can use the autowired MockMvc instance to directly call our controller.
    • Our controller makes an HTTP request to the mock server with
      GET /monitoring/v2/accounts/d012345trial/apps/sample-application/metrics
      
    • The controller response is checked for an HTTP status 200 and the correct JSON content.
    • For the sake of simplicity we omit authorization checks at the current stage.

    Run the test.

Your integration-test module now contains a mock server test:

 

Step 7: Obtain credentials from SCP Neo

In case you don’t already have credentials to access a monitoring API with, you can simply create them yourself on SCP Neo.

  1. Use the SAP Cloud Platform Cockpit, to go to your account. In the left navigation, click on Services. In category DevOps make sure Monitoring is active.
  2. In the navigation, open Security > OAuth. In the content frame, find the Token Endpoint, note it down – you will need this in the next step.
  3. Next, change to the tab labeled Platform API.
  4. Click the Button Create API Client. Enter a description and select the Monitoring Service checkbox.
  5. Note down the Client ID and Client Secret. You will need them during the destination configuration.

 

Step 8: Prepare configuration on SCP Cloud Foundry

Open the SCP Cockpit and navigate to your Cloud Foundry account.

  1. Ensure service bindings
    Open the current account space, which your application is going to be uploaded to. Here, in the navigation on the left, under Services, click on Service Instances. Make sure you have an instance running for both services, xsuaaand destination. For the sake of this guide, let’s assume the xsuaa service instance is called "myxsuaa" and the destination service instance is called "mydestination".In case you are missing a service instance, go to Service Marketplace and setup it up. For xsuaa, the recommended service plan is application. For destination it is lite.
  2. Add the destination
    Leave the space, back to your Cloud Foundry account. In the navigation, under Connectivity click Destination, Click the button New Destination. Enter the Monitoring service destination values:

    • Name: MonitoringEndpointJust like described in your Java application, as field MonitoringDestination.DESTINATION_NAME
    • Type: HTTP
    • URL: https://api.[domain].ondemand.com/monitoring/v2/Enter the correct sub domain, depending on your landscape. You can find a list of supported URLs on the Service API page.
    • Proxy Type: Internet
    • Authentication: OAuth2ClientCredentials
    • Client ID + Token Service User: (Client ID from the previous step)
    • Client Secret + Token Service Password: (Client Secret from previous step)
    • Token Service URL: (Token Endpoint from previous step)

    Note: While Monitoring v2 has only one pair of credentials, other services may distinguish between Token Service User and Client ID.

You are done with the setup on SCP Cloud Foundry.

 

Step 9: Deploy application

  1. Run a complete Maven build with your project:
    mvn clean install
    

    All tests will be executed to make sure your application is working as expected.

  2. Open the manifest.yml
    Change the YAML such that XSUAA and destination services instance are bound upon application initialization. Add the following entries to the list of services:

    services:
      - myxsuaa
      - mydestination

    Note: This may differ from your file, in case the service instances were named differently.

  3. In the command prompt run the following statement with the Cloud Foundry commandline interface: For information on how to use the cf tool, please find the starter tutorial for applications on Cloud Foundry with the SAP Cloud SDK.
    cf push
    

    Note down the logged entry in urls above.

 

Step 10: Test

  1. Open the application URL. You will be greeted with the default landing page.Enter the following path.
    /monitor?account=D123456&app=sampleapp
    

    Replace the wildcards for the request parameters. For account enter the (sub) account Id for which the monitored application is running. Enter the application name for app.

    You should see the successful response from your application.

Congratulation!

You successfully consumed a service API from SCP Neo with an application on SCP Cloud Foundry, with the help of code generation and SAP Cloud SDK.

21 Comments
You must be Logged on to comment or reply to a post.
  • while executing mvn (Step 2: Create a new Spring application) I got: Archetype com.sap.cloud.s4hana.archetypes:scp-cf-spring:LATEST is not configured

    • Hi Micha,

      If “LATEST” is not working as a keyword, please consider using the latest version

      com.sap.cloud.s4hana.archetypes:scp-cf-spring:2.14.0

      In case this still does not work, try the “-U” modifier in the command, i.e.

      mvn -U archetype:generate -DarchetypeGroupId=com.sap.cloud.s4hana.archetypes -DarchetypeArtifactId=scp-cf-spring -DarchetypeVersion=2.14.0

       

      Best regards

      Alexander

       


       

      Further investigations led to a broken ~/.m2/settings.xml (maven configuration file). By exchanging it with a default template, we fixed the issue.

  • Hi Alexander,

     

    Excellent Blog. Does this also handles oAuth Handling automatically from destination or we need to do that. I am working on connecting to API’s on NEO from Cloud Foudry. I have created Token Endopoint in NEO. I have created destination for the API’s with Client id and Secret with Token Endpoint in CF along with SCOPE.

    Can you please share something on that.

     

    Regards,

    Rajiv

    • Hi Rajiv,

      The OAuth token handling works automatically. Internally the destination service of CF is negotiating the token. If you followed the steps described in this post for ClientCredentials based authentication, no further actions are required.

      Not sure about the context of “scope” you mentioned though.

      Best regards

      Alexander

  • Hi Alexander,

     

    Thanks for shairing this nice blog.I followed everything but after running command mvn clean install i am getting below error.

     

    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project org.example.rest-application: Compilation failure
    [ERROR] /D:/org.example.rest/application/src/main/java/org/example/rest/controllers/MonitorController.java:[29,98] cannot find symbol
    [ERROR] symbol: method getAccountsSubaccountNameAppsAppNameMetrics(java.lang.String,java.lang.String)
    [ERROR] location: class org.example.rest.monitoring.api.JavaApplicationMetricsApi

    Can you help me to sort out the issue or is any way to get the org.example.rest-application.jar file so i can try to push the application in my cloud foundry account and test.

     

    Thanks,

    Arpit Gupta

    Software Consultant

    • Hi Arpit,

      please be aware that this blog post targets older versions of the SAP Cloud SDK. From the error message I assume that in the mean time the API on the API Hub may have changed and therefore also the API classes that you generate here. That means that the usage of these needs to also change and the code snippets in this post are not up to date anymore. Since I don’t have the generated code at hand I can’t tell you if that is the case and to what extend changes are need to be made to the code.

      I recommend you use your favourite IDE to discover what methods api exposes and look for what is similar to the getAccountsSubaccountNameAppsAppNameMetrics that we used to call. Maybe only parameters have changed.

      JavaApplicationMetricsApi api = new JavaApplicationMetricsApi(apiClient);
      

      Then disable the tests for now and try to run it again. In order to adapt the tests please look up the method you call in the API hub and adapt the tests to meet the current specification.

       

      Best regards,
      Matthias

  • Hello Alexander,

    in our project, we want to build an application on SCP Neo that consumes services from different sources.

    I successfully created an app and deployed it on Neo accordingly to the following tutorial:

    https://developers.sap.com/group.s4sdk-neo.html

    The app was created with the following command:

    mvn archetype:generate -DarchetypeGroupId=com.sap.cloud.sdk.archetypes -DarchetypeArtifactId=scp-neo-javaee7 -DarchetypeVersion=RELEASE

    I was able to work with OData services using for example the DestinationAccessor and the ODataQueryBuilder.

    Now, I have one remaining issue: not all of the services that we want to use are OData, some of them are REST services. For example AIN (Asset Intelligence Network) which is also available in the API business hub:

    https://api.sap.com/api/EquipmentAPI/overview

    Could you please let me know if I can somehow use the approach described here to achieve what we need?

    As far as I could see this tutorial is intended for Cloud Foundry. I successfully performed this tutorial, but I was not able to deploy it to Neo. Probably I would need to make some changes to the Pom.xml, in order to get a WAR file that I could deploy on Neo. But I am no expert in that area.

    Thanks and best regards,

    Tobias

    • Hello Tobias,

      You are right, this tutorial is about consuming REST APIs with Cloud Foundry Apps for SAP Cloud SDK ver. 2

      I’ll take a look at this issue and discuss possible solutions with Alex. Can you please clarify the following:

      1. Which version of SAP Cloud SDK do you use? Version 2 or version 3?
      2. What’s the reason to use Neo instead of Cloud Foundry?
      3. Is there possibility to switch to Cloud Foundry?
      4. If you’re just starting I suggest using Cloud SDK ver.3 + Cloud Foundry, if it’s possible of course.
      5. Porting and App from Cloud Foundry to Neo should be relatively easy. Please, take a look at this blog about Cloud SDK 3.0. Especially at the section of new destinations concept.

      Thanks for patience and hope for a quick follow up from you. In the meanwhile I’ll investigate this in a bit more details.

      Best,

      Artem

      • Hello Artem,

        thanks so much already for your response and your investigations.

        Here are my answers:

        1. We are using version 3 of the SDK. I know that the blog is for v2, but I hoped to be able to apply the concepts nonetheless, with some adaptations of course. For V3 I could not find anything similar.
        2. It was a decision in the project in which I am working that Neo should be the platform to be used.
        3. Not at this point, potentially in the future. If you could help us get some insights into the future direction of the SDK maybe this could be helpful for the decision. Our understanding was that the SAP Cloud SDK can be used both for Cloud Foundry and Neo.
        4. We are already in the midst of the development, so at this point we need to stick with the current approach. For the future we could take this into account.
        5. Could you please share again which blog you were referring to?

        Best regards,

        Tobias

        • Hi Tobias, thanks a lot for clarifications. It brings a lot of clarity.

          My immediate hunch here would be:

          1. Create a Neo App with this tutorial
          2. Ensure you’re able to deploy it to your Neo environment
          3. Follow steps from this blog again to generate your Open API code
          4. Deploy and test again on SCP Neo

          I’ll discuss these steps with the team shortly to see what particularities are there to consider so that everything runs smoothly for you.

          Can you please give me additional details about what exactly fails when you deploy to Neo? Error log? Stack trace? Please, remove any sensitive data if any before posting it here.

          It would be also nice if you can host your tutorial’s code somewhere on GitHub and share a link with me.

          The blog article I was referring to is this one. You should be aware of it in case you already use SDK ver. 3.

          I would also encourage you to look at this page for SDK docs and Javadoc 

          Regarding Cloud Foundry (CF) vs Neo I’d recommend looking at this comparison.

          General consideration would be to start new development with Cloud Foundry as more developed and feature reaches the environment unless you have restriction binding to use Neo.

          I hope it helps and waiting for your follow up.

          ————- UPDATE 1:

          I discussed with the team and confirmed that all the code related to REST consumption should be compliant with SDK v.3 and cross-platform. Meaning it should work both on Neo and CF.

          Please, make sure that you bootstrap an App with Neo archetype because you won’t be able to deploy CF build to Neo platform. But the REST part should be fully reusable.

          We are happy to help further if you see any errors while following the mentioned steps.

          —————————

          Best,

          Artem

          • Hi Artem,

            thanks so much for your hints, they are very helpful.

            What I now did is to try to incorporate the code that was created for my API (https://api.sap.com/api/EquipmentAPI/overview) into my Neo project (DarchetypeArtifactId=scp-neo-javaee7).

            I now have a war file that I can successfully deploy to Neo and start. The OData calls are still working fine. For rest APIs I get error 403. Even though I think it is called using the destination, there seems to be some kind of authorization issue?

            Here is the error that I receive:

            Here is the code for calling Odata which is working fine:

            package com.sap.cloud.sdk.tutorial;
            
            import com.google.gson.Gson;
            import java.util.List;
            import java.util.Map;
            
            import org.apache.http.client.HttpClient;
            import org.slf4j.Logger;
            
            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 com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
            import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
            import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
            import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
            import com.sap.cloud.sdk.cloudplatform.logging.CloudLoggerFactory;
            import com.sap.cloud.sdk.odatav2.connectivity.FilterExpression;
            import com.sap.cloud.sdk.odatav2.connectivity.ODataException;
            import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryBuilder;
            import com.sap.cloud.sdk.odatav2.connectivity.ODataQueryResult;
            import com.sap.cloud.sdk.odatav2.connectivity.ODataType;
            
            @WebServlet("/ainservice")
            public class AinService extends HttpServlet {
            
                private static final long serialVersionUID = 1L;
                private static final Logger logger = CloudLoggerFactory.getLogger(AinService.class);
            
                @Override
                protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
            	    throws ServletException, IOException {
            	    final Destination destination = DestinationAccessor.getDestination("AIN");
            	    HttpDestination httpDestination = destination.asHttp();
            	    final HttpClient httpClient = HttpClientAccessor.getHttpClient(httpDestination);
            		
            	    try {
            			ODataQueryResult result = ODataQueryBuilder
            					.withEntity("/ain/ac.odata.svc/api/v1", "Equipment")
            					.select("id", "longDescription")
            					.build()
            					.execute(httpClient);
            			
            			response.setContentType("application/json");    
            			List<Map<String,Object>> productList = result.asListOfMaps();
            			response.getWriter().write(new Gson().toJson(productList));
            		
            		} catch (ODataException e) {
            			response.getWriter().write(e.toString());
            		}
            
                }
            }

            And the code for the Rest call where I get the error:

            package com.sap.cloud.sdk.tutorial;
            
            import org.apache.http.client.HttpClient;
            import org.example.rest.monitoring.api.AttributeValuesApi;
            import org.example.rest.monitoring.invoker.ApiClient;
            import org.example.rest.monitoring.model.EquipmentValueRead;
            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 com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
            import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
            import com.sap.cloud.sdk.cloudplatform.connectivity.HttpClientAccessor;
            import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
            import java.net.URI;
            import java.net.URISyntaxException;
            import org.springframework.web.client.RestTemplate;
            import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
            import org.springframework.http.client.BufferingClientHttpRequestFactory;
            
            @WebServlet("/ainrest")
            public class AinOdataServletNew extends HttpServlet {
            
                private static final long serialVersionUID = 1L;
            
                @Override
                protected void doGet(final HttpServletRequest request, final HttpServletResponse response)
                        throws ServletException, IOException {
                    
            			final Destination destination = DestinationAccessor.getDestination("AIN");
            			HttpDestination httpDestination = destination.asHttp();
            			final HttpClient httpClient = HttpClientAccessor.getHttpClient(httpDestination);
            			final HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
            			httpRequestFactory.setHttpClient(httpClient);        
            			final RestTemplate restTemplate = new RestTemplate();
            			restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(httpRequestFactory));
            			final ApiClient apiClient = new ApiClient(restTemplate);
            
            			final URI uri = httpDestination.getUri();
            			URI path = null;
            			try {
            				path = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null);
            			} catch (URISyntaxException e1) {
            				e1.printStackTrace();
            			}
            			apiClient.setBasePath(path.toString());  
            
            			AttributeValuesApi apiInstance = new AttributeValuesApi(apiClient);
            			EquipmentValueRead result = null;
            			result = apiInstance.equipmentEquipmentIdValuesGet("<EquipmentId>", "2");
            			response.getWriter().write(result.toString());
                }
            }

            So in both cases the same destination (AIN) is used. It is of type OAuth2ClientCredentials, and in general it works.

            It would be great if you could let me know if how I tried to do it is the correct approach. Maybe you also have an idea how to fix the issue.

            Thanks and best regards,

            Tobias

            /
          • Hi, Tobias, I’m happy we’re making some progress.

            I’ll take a look at what might be wrong with auth here. Let me a bit of time to get a better idea about what’s going on here and talk to my colleagues:)

            You can continue hacking in the meanwhile and please keep me posted if you have any additional findings!

            A small request from me to post an exception stack trace as text because somehow the image is not scalable in the comments. I saved it and could read it, but the text would be still faster and easier to handle.

            I’ll get back to you as soon as I have ideas about solving this!

            Best,

            Artem

          • Hey Tobias, I’m back with some suggestions and comments

            The good thing is that as far as I can see your code looks correct. It’s also confirmed by the fact that your request gets through and you see a 403 (Forbidden) in response.

            From here we can do the following:

            1. Can you please enable HttpClient logging and check the generated URL and all the headers? We have to ensure they are correct.
            2. Can you mock the same request with some API tool like Postman or Insomnia, or CURL? We have to make sure you can successfully query the underlying API to understand if the problem is in the code or some Authentification or Authorisation settings.
            3. Have you properly configured your equipment API service? Do you pass an API key (token)? It usually has to be a header “Authentification: Bearer <token>“. Also, please, check API reference for the request you make, I recommend looking at Java and CURL code snippets.
            4. Can you check you have sufficient permissions from this Equipment API and your token is authorized to do the requests you want to make?
            5. Let’s start with testing it from both your code with more verbose logging and using API tools to make the first request return a successful result. Then it will be just a task to glue it together.

            Please, get back to me after you have some results from my suggestions.

            I hope it helps!

            Best,

            Artem

          • Hi Artem,

            I activated some logs in the Neo Cockpit for my application and here is what I got:

            URL (seems correct to me):

            HTTP GET https://ain-live.cfapps.eu10.hana.ondemand.com/equipment(<EquipmentId&gt;)/values?status=2

            There is also an entry for the Authentification: Bearer <token>, however I have no idea where it is coming from:

            My destination seems to be ok:

            By the way, we are using a destination of type OAuth2ClientCredentials that was created according to the following link:

            https://help.sap.com/viewer/d89bac86aa294f75afdc40dca457dd7b/1911/en-US/b2f6f1acc25744a29c177f25d9bb3bce.html

            In general we can work successfully with this destination.

            In the cockpit it looks like this:

            I will try to make some tests with Postman as you suggested.

            Any further hint would be very much appriciated!

            Thanks and best regards,

            Tobias

          • Hi Artem,

            in the meantime I am one step further:

            I found out that my base path was wrong, it containted only the path to the application and not the complete API path.

            I changed it to:

            apiClient.setBasePath("https://ain-live.cfapps.eu10.hana.ondemand.com/ain/services/api/v1/");

            Now I can call it successfully (HTTP Status 200).

            I am getting the following from the apiInstance.equipmentEquipmentIdValuesGet function:

            class EquipmentValueRead {
                id: AC993C8145C84EEEB8EDE1AF7E9A5E1E
                templates: [class EquipmentValueReadTemplates {
                    templateId: C5E7F7F8D6EC49C7A5C8AFBF42E280A1
                    attributeGroups: [class EquipmentequipmentIdvaluesAttributeGroups {
                        attributes: [class EquipmentequipmentIdvaluesAttributes {
                            object: null
                        }, class EquipmentequipmentIdvaluesAttributes {
                            object: null
                        }, class EquipmentequipmentIdvaluesAttributes {
                            object: null
                        }, class EquipmentequipmentIdvaluesAttributes {
                            object: null
                        }, class EquipmentequipmentIdvaluesAttributes {
                            object: null
                        }, class EquipmentequipmentIdvaluesAttributes {
                            object: null
                        }]
                        attributeGroupId: 3F0FF9C36D9048678D6A6D6AA5FC9F4B
                    }]
                }]
            }

            I was expecting (hoping) I would get the same result as when I call the PAI e.g. in Postman:

            Do you have any idea what I have to change in order to get this?

            Thanks and best regards,

            Tobias

            /
  • Hi Tobias,

    Thanks for the extensive findings. I’ll check them out and get back to you ASAP. It seems like we’re getting closer if you can successfully query the API.

    • Hi Artem,

      in the meantime I figured out how to work with the objects that I get back from the API call.

      So everything is fine (for now 🙂

      Thanks for all your support and best regards,

      Tobias

      • Hi Tobias,

         

        Sorry for getting this long to get back. I had too much in my pipeline.

        I’d appreciate it if you share your findings with me.

        We’ll be updating this blog article and converting it into a tutorial. Your feedback and findings will help us a lot to make it better and easy to follow.

        I already see your discovery about the base path, what was it with processing the returned data that gave you empty results at the end? How did you solve it?

        Thanks in advance for sharing and have a great weekend!