1 2 3 20 Previous Next

SMP Developer Center

294 Posts

Dear All,

 

From SMP 3.0 onward we have two ways for registering a user with SMP Server.

1. MAF Logon

2. Using OData SDK

 

A lot of people go for using the MAF Logon. MAF Logon comes with a pre defined UI Screen for Logon functionality so the developers do not need to do anything to onboard users. They can just import the MAF projects inside their IDE (Android Studio/Eclipse for Android)/ Xcode for iOS and just run the application.

You can learn how to onboard users using MAF Logon from this link

 

But there could be a requirement that you do not want to use the SAP Standard screen and have your own custom login screen, to maintain UI consistency all over the application. For this we can onboard users using OData SDK. The tutorial on how to onboard users using OData SDK can be accessed using this link

 

You might run into difficulties to authenticate users against any sort of Authentication mechanism that you use like HTTP/HTTPS etc but with NoAuthenticationLoginModule it will onboard your user but when you login to the Management Cockpit it will show the user registered as no_sec_identity.

 

Follow the below set of code to successfully onboard your user against an authentication mechanism that you have configured against your application id

 

Assuming that you have your own custom screen with a field for username and password and a Login Button you can add this set of code on the button click event of your Login Button

 

private LogonCore logonCore;  Define this as a global parameter at the start of your activity.

And within the button click event add this line of code

 

 

 

LogonCoreContext context = logonCore.getLogonContext();

try

{

context.setHost(smpHost);  //address of your SMP Server or relay server

context.setPort(smpPort);    // port of the SMP Server or port 80 if you are using the relay server

context.setHttps(false);       // Set to TRUE if you are using https

context.setFarmId("0");     //Set the FARM ID in case you are using the Relay Server

 

//

context.setResourcePath("ias_relay_server/client/rs_client.dll);   //User this if you are using the Relay Server

 

context.setBackendUser(smpUserId);               //The field that the user will input his username

context.setBackendPassword(smpPassword);   // The field that user will input his credentials

context.setSupUserName(smpUserId);

context.setSupPassword(smpPassword);

context.setChannel(Channel.REST);               //This is a very important parameter to add else you would not be able to pass these parameters

logonCore.register(context);

}

 

With this you can see your user is authenticated against the security provider and now you can use all functionalities like SSO, Role Mapping etc relevant to your security provider.

 

These are all with inputs from Claudia Pacheco Pavel Penaz Prashanth Sutram

 

Hope this helps you all and saves all of your time to debug the error.

 

Thanks,

Regards,

Rakshit Doshi


In December 2014, SAP enabled the HANA Cloud Platform mobile services (HCPms) trial for all hanatrial accounts. HCPms is a real Mobile-as-a-Service offering and it shares some source-code baselines with SMP 3.0 on premise. In consequence, mobile apps written with the SMP 3.0 OData SDK can run on both HCPms and SMP 3.0 on premise with little or no change. In order to understand the differences and similarities visit this blog SAP HANA Cloud Platform mobile services released

 

If you haven’t done it yet, you can enable your HCPms trial with the steps described in this blogHow to enable HANA Cloud Platform Mobile Services Trial

 

The process of creating your application configuration is different.John Polus hosted a webinar on this topic Webinar: Getting Started with the HANA Cloud Platform Mobile Services (HCPms) and Using the HANA Cloud Connector to Access Backend Systemsand created two guides to help you go through this process.

  1. Installing and Configuring the HANA Cloud Connector for On-premise OData Access
  2. Creating Application Definitions in Hana Cloud Platform Mobile Services

 

The good news is all the Android material I have created for SMP 3.0 SDK for SMP3.0 on premise is also valid for HCPms. I have onboarded with HCPms and have accessed both online store and offline store using the projects and guides I published on Mobile Application Development Platform for Developers - Native Apps. No changes required.

 

In order to onboard with HCPms, you need to enter the values displayed in the following screenshot.

Host: hcpms-<your user>trial.hanatrial.ondemand.com

Login and Password: Your credentials

Port: 443

No need to check the "unsecure connection" checkbox, because HCPms is an HTTPS connection.

HCPms.jpg

 

Before you run the android apps, I would recommend testing first the configuration of your application definition from a REST Client. I normally use POSTMAN plugin for Chrome, but feel free to use your favorite rest client plugin.

 

1. Onboard a user from the REST client

 

If you enter the following values, you should receive a “201 created” response if the application configuration is set up correctly

 

 

URL

https://hcpms-<your user>trial.hanatrial.ondemand.com/odata/applications/latest/<your app id>/Connections

HTTP Operation

POST

HTTP Header: Content-Type

application/atom+xml

HTTP POST Body

<?xml version="1.0" encoding="UTF-8"?>

<entry xml:base="http://localhost:8080/odata/applications/latest/com.sap.flight.kapsel/Connections"

xmlns="http://www.w3.org/2005/Atom" xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"

xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices">

  <content type="application/xml">

<m:properties>

</m:properties>

</content>

</entry>

Switch to Basic Auth Tab

 

Username

Your username

Password

Your password

Click the “Refresh Headers” button

 

 

The result should look like the screenshot below. Please take note of the response and copy the ApplicationConnectionId property available in the response, because you will need it in the second test.

HCPms-onboard.jpg

 

2. GET service document from the REST client

If you enter the following values, you should receive a “200 OK” response if the application configuration is set up correctly

 

 

URL

https://hcpms-<your user>trial.hanatrial.ondemand.com/<your app id>/

HTTP Operation

GET

HTTP Header: X-SMP-APPCID

The ApplicationConnectionId you received in the response body from the onboarding exercise.

Switch to Basic Auth Tab

 

Username

Your username

Password

Your password

Click the “Refresh Headers” button

 

 

The test should look like the screenshot below

HCPms_Get.jpg

 

 

 

 

 

Cheers

Claudia

If you are wondering how to send batch request in Android using the SMP 3.0 SDK SP05+, then this blog is for you.

 

A batch request allows a mobile client to send multiple operations in a single HTTP request.  The following diagram represents the objects needed to create a batch request with SMP 3.0 native SDK for Android:

  • The ODataRequestParamBatch class represents a batch request and it can contain 1 or more batch items.
  • A batch item is represented by the ODataRequestBatchItem interface and the item can be single read request (ODataRequestParamSingle) or a change set (ODataRequestChangeSet).
  • A change set (ODataRequestChangeSet) can contain only CUD requests and their execution is atomic (either all requests executed successfully, or all failed).

BatchRequest.jpg

 

In order to use batch request, the mobile client need to:

 

1. Create a batch request

 

Code snippet - Create Batch Request (ODataRequestParamBatch)
ODataRequestParamBatch requestParamBatch = new ODataRequestParamBatchDefaultImpl();


2. Create the batch item payload for CREATE and UPDATE operations.


Code Snippet - Create Batch Item Payload (ODataEntity)

ODataEntity newEntity = new ODataEntityDefaultImpl("RMTSAMPLEFLIGHT.Travelagency");

newEntity.getProperties().put("agencynum", new ODataPropertyDefaultImpl("agencynum", "12345678"));

newEntity.getProperties().put("NAME", new ODataPropertyDefaultImpl("NAME", "Flight Center Inc"));

newEntity.getProperties().put("STREET", new ODataPropertyDefaultImpl("STREET", "123 Main street"));

  ...


3. Create a batch item and add item to the batch request


3.1 Single READ request


Code Snippet - Create Batch Item: Single READ Request

// Create batch item

ODataRequestParamSingle batchItem = new ODataRequestParamSingleDefaultImpl();

batchItem.setResourcePath("TravelAgencies");

batchItem.setMode(ODataRequestParamSingle.Mode.Read);

batchItem.setCustomTag("something to identify the request");


// Add batch item to batch request

requestParamBatch.add(batchItem);


3.2 Change set with UPDATE operation


Code Snippet - Create Batch Item: Change Set

// Create batch item

ODataRequestParamSingle batchItem = new ODataRequestParamSingleDefaultImpl();

// Allocate OData Entity

batchItem.setResourcePath("TravelAgencies(‘12345678’)");

batchItem.setMode(ODataRequestParamSingle.Mode.Update);

batchItem.setCustomTag("something to identify the request");

batchItem.setPayload(newEntity);

// Add headers

Map<String, String> createHeaders = new HashMap<String, String>();

createHeaders.put("accept", "application/atom+xml");

createHeaders.put("content-type", "application/atom+xml");

batchItem.setOptions(createHeaders);

// Create change set

ODataRequestChangeSet changeSetItem = new ODataRequestChangeSetDefaultImpl();

// Add batch item to change set.

// You can add more batch items to the same change set as long as they are CUD operations

changeSetItem.add(batchItem);

 

// Add batch item to batch request

requestParamBatch.add(changeSetItem);


4. Send Batch request: Batch requests are submitted as a single HTTP post request to the batch endpoint of a service. The HTTP status code of a batch response is generally “202 Accepted” and it only indicates that the overall request has been accepted for processing, not that every operation successfully completed. There will be a status code returned for each part of the multipart batch request.


Code Snippet - Send Batch Request

// Send request synchronously

ODataResponse oDataResponse = odataStore.executeRequest(requestParamBatch);

// Check http status response for batch request.

// Status code should be "202 Accepted"

Map<ODataResponse.Headers, String> headerMap = oDataResponse.getHeaders();

if (headerMap != null) {

   String code = headerMap.get(ODataResponse.Headers.Code);

}

 

// Get batch response

if (oDataResponse instanceof ODataResponseBatchDefaultImpl) {

  ODataResponseBatch batchResponse = (ODataResponseBatch) oDataResponse;

  List<ODataResponseBatchItem> responses = batchResponse.getResponses();

  for (ODataResponseBatchItem response : responses) {

      // Check if batch item is a change set

      if (response instanceof ODataResponseChangeSetDefaultImpl) {

       ODataResponseChangeSetDefaultImpl changesetResponse = (ODataResponseChangeSetDefaultImpl) response;

       List<ODataResponseSingle> singles = changesetResponse.getResponses();

            for (ODataResponseSingle singleResponse : singles) {

              // Get Custom tag

              String customTag = singleResponse.getCustomTag();

                        // Get http status code for individual responses

              headerMap = singleResponse.getHeaders();

              String code = headerMap.get(ODataResponse.Headers.Code);

 

                        // Get individual response

              ODataPayload payload = singleResponse.getPayload();

                         if (payload != null) {

                                     if (payload instanceof ODataError) {

                             ODataError oError = (ODataError) payload;

                             String uiMessage = oError.getMessage();

                      } else {

                                                  // TODO do something with payload

}

              }

       }

    } else {

       // TODO Check if batch item is a single READ request

    }

   }

}


Questions? let me know,


I would also recommend looking at this document to understand how SAP Gateway deal with Batch requests

How To Batch Multiple Operations Into A Single Request


Claudia

Great blog by Posted by Kiran Karunakaran in SAP for Mobile on March 13th announcing the release of the latest SAP Mobile Platform SDK. Read the blog and get all the details.

 

SAP Mobile SDK SP07 - What is new ?

 

Native SDK features

  • Technical cache (for Windows)
  • Certificate authentication directly with SAP Gateway in the MAF Logon Core (iOS, Android, and Windows)

 

SMP Hybrid SDK(Kapsel) features

  • Device and Platform support
  • Offline support for Windows
  • SAML support for Windows

 

Agentry Toolkit features

  • Open Scan SDK
  • Other Enhancements

 

Enjoy!

 

Jenny Lundberg

Mobile Platform Product Management

This  blog is about Integration Gateway in SAP Mobile Platform 3.0 (SMP).

 

In my previous tutorial, I’ve explained that since SP06, the SMP also supports READ operation for REST data source.

That tutorial was based on xml, so today I’d like to give an example how to do the same for JSON payload.

We’ll do the payload modification using a JSON parser.

 

 

Why is this blog necessary?

In JSON, the expected structure that we have to provide in our Custom Code, is different than in case of XML.

The current example REST service doesn’t have a READ URL, so a filter has to be specified as "Relative URL

 

As usual, please find the relevant source files attached to this Blog.

 

 

 

 

Prerequisite

 

You should have gone through my tutorial regarding READ operation for REST data source:

 

 

 

Preparation

 

You might wish to read this document in order to enhance your development experience for Groovy

 

 

REST service


In this tutorial, we’re using the following REST service

http://sansad.co/api/legislators

 

Documentation can be found here:

http://sansad.co/

 

Destination


In SMP, go to the Gateway Management Cockpit and create Destination for the REST service

 

 

 

The "Connection Test" should be successful

 

If you get an error message on connection test, you might consider the following:

 

Note that you might need to enter proxy settings in your SMP:

https://localhost:8083/Admin/ -> Settings-> System

 

Note that you might need to restart the SMP server after changing the proxy settings.

 

OData Model

 

 

Create Project

 

Create project of type SAP Mobile Platform OData Implementation Project.

 

 

Define OData Model


Within the OData Modeler, create one EntityType that looks as follows:

 

 

 

 

 

Bind data source


Bind the REST data source to the “Read” operation of our EntitySet “Customers”.

Specify the following “Relative URL”:

 

/api/legislators?mp_id={mp_id}

 

 

Please note:

The URL that is used by the REST service to read a single entry is, for example:

http://sansad.co/api/legislators?mp_id=2245RS

 

We can see that the REST service doesn’t address a single resource by a URI.

Instead, a filter parameter is used.

For us, it doesn’t matter. Since the filter parameter uses an ID, we can assume that the result will always be not more than ONE entry.

So we can safely use this URL in terms of a READ for our OData service implementation.

 

Custom Code

 

After finishing the binding wizard, we create the "Custom Code" script for Groovy language.

 

What do we have to do in the script?

 

The payload that is returned by the REST service looks as follows:

 

 

But the Integration Gateway Framework expects the following JSON structure:

 

 

          {

              "PropertyName1":"PropertyValue1",

              "PropertyName2":"PropertyValue2"

          }

 

 

 

 

The following is supported as well:

 

 

 

{

  "d":

        {

              "PropertyName1":"PropertyValue1",

              "PropertyName2":"PropertyValue2"

        }

}

 

 

In our concrete example project, we have to manipulate the REST response such that it looks like:

 

 

 

 

After we’ve modified the response string, we return it to the Integration Gateway Framework.

Which is done by setting the modified string as body to the “message” object that is passed to us as parameter.

 

 

Intermediate step

 

Before we start modifying the REST response, let’s do an intermediate step with a very simplified hard-coded response string.

The code looks as follows:

 

 

def Message processResponseData(message) {

  message.setBody(

"{" +

                              "\"mp_id\":\"Tazeen Fatma\"," +

                              "\"first_name\":\"Tazeen\"," +

                              "\"state\":\"Uttar Pradesh\"" +

                        "}");

 

  return message;

}

 

 

Whereas the following is also working:

 

 

def Message processResponseData(message) {

  message.setBody(

              "{" +

                    "\"d\":" +

                          "{" +

                                  "\"mp_id\":\"Tazeen Fatmaaaaaaaaa\"," +

                                  "\"first_name\":\"Tazeen\"," +

                                  "\"state\":\"Uttar Pradesh\"" +

                          "}"+

            "}");

 

  return message;

}

 

 

After generate, deploy, configure and run the service, you should see the result in the browser.

Check the Result-section below for info about how to run the service.

 

 

Note:

In this hard-coded response, we’re setting less properties than defined in our OData model. But this is OK, at runtime the remaining properties will be empty.

Remember: Our OData model can be bigger than the REST-service, but it must not be smaller (which would cause exceptions at runtime).

 

 

Implement the script

 

Now let’s do the generic implementation.

 

What we have to do:

 

As you can see from the screenshots above, we have to

 

  • Remove the “results” node
  • Remove the Array that is indicated by [ ]
  • Remove the last 2 nodes, "count" and "page"

 

Other than in the previous tutorial, we’ll be using a JSON parser.

Using the parser, we only have to traverse the node-tree until we reach the entry that carries the properties.

That’s it already.

Then only transform this entry back to string.

 

 

Note:

In this concrete example, I’ve reduced the amount of properties in the OData model.

Remember: it is not allowed that the OData model has less properties than the data source, which is the REST service.

Thus, we have to solve this problem, and we do it by removing the undesired properties from the REST response, before we pass it back to the Integration Gateway Framework

This can be easily done with the parser API.

 

 

In one of my previous tutorials, I’ve described in detail how to use the JSON parser.

 

We need to declare a dependency to the JSON parser library, which is available in SMP: com.google.gson

Open the MANIFEST.MF file and add the dependency:

 

 

 

 

Now you can implement the following code:

 

 

 

def Message processResponseData(message) {

    message = (Message)message;

    String restResponse = (String) message.getBody();

 

       // convert payload

    String convertedPayload = convertPayload(restResponse, message);

    message.setBody(convertedPayload);

 

      return message;

}

 

def String convertPayload(String payload, Message message){

    JsonParser parser = new JsonParser();

 

    JsonElement topElement = parser.parse(payload);

    JsonObject resultsJsonObject = topElement.getAsJsonObject(); // {"results":[{...

 

    JsonElement resultsJsonElement = resultsJsonObject.get("results");

    JsonArray resultsArray = resultsJsonElement.getAsJsonArray();

 

    // since we're performing a READ operation, we have exactly one entry

       JsonObject entryObject = (JsonObject)resultsArray.get(0);

 

       // now need to remove some properties, because they aren't defined in our OData model

    entryObject = doRefactoringForEntry(entryObject, message);

 

      return entryObject.toString(); // transform to string and return

}

 

// our OData model doesn't contain all the properties that the REST service has.

// this is not allowed. So we remove the undesired properties from the REST payload,

// in order to match the smaller model

def JsonObject doRefactoringForEntry(JsonObject entryObject, Message message){

    entryObject.remove("age");

    entryObject.remove("attendance_percentage");

    entryObject.remove("debates");

    entryObject.remove("elected");

    entryObject.remove("in_office");

    entryObject.remove("private_bills");

    entryObject.remove("questions");

 

      return entryObject;

}

 

 

 

The relevant line of code is

 

      JsonObject entryObject = (JsonObject)resultsArray.get(0);

 

 

This is the entry that matches the structure that is expected by Integration Gateway. So we can basically just call its toString() method and we get what Integration Gateway needs.

Additionally to the requirement from Integration Gateway, we also remove some of the properties, as explained above.

 

Please note that error handling is completely missing in our sample code.

 

 

Result

 

After doing generate&deploy in our Eclipse project, change to your browser and open the Gateway Management Cockpit.

Assign the destination that we’ve created in the preparation step.

Invoke our OData service.

 

Note that we haven’t implemented the QUERY, so we have to directly invoke a URL for READ of a single entry:

 

Example for a READ URL for our OData service:

https://localhost:8083/gateway/odata/SAP/<your_service_name>;v=1/Customers('2245RS')

 

 

The result is

 

 

 

 

 

Links

 

 

The prerequisite tutorial that does the same like this blog, but for XML format:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/03/06/integration-gateway-understanding-rest-data-source-7-read--xml

 

Introduction in REST data source part 3: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/12/integration-gateway-understanding-rest-data-source-3-query--xml-standard

 

Preparing Eclipse for Groovy scripting:

http://scn.sap.com/docs/DOC-61719

 

Introduction in REST datasource part 1: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/10/understanding-rest-data-source-in-integration-gateway-1-query-very-simplified

 

Introduction in REST data source part 2: Understanding the return structure in json

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/11/integration-gateway-understanding-rest-data-source-2-query--json-very-simplified

 

Installing SMP Toolkit:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/08/22/how-to-install-sap-mobile-platform-tools-for-integration-gateway-in-eclipse-kepler

 

Tutorial for OData provisioning in SMP:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/06/10/creating-an-odata-service-based-on-sap-gateway-soap-jdbc-and-jpa-data-sources-ba

The official documentation

http://help.sap.com/smp306svr/#section6

 

Data Integration

http://help.sap.com/saphelp_smp306svr/helpdata/en/7c/20051470061014a27cf7962ebae5f8/frameset.htm

This  blog is about Integration Gateway in SAP Mobile Platform 3.0 (SMP).

 

In the previous tutorials, we’ve learned how to deal with REST services as data source for an OData service in SMP.

These tutorials were based on the QUERY operation.

Now, since SMP SP06, the READ operation is supported as well.

This blog explains how to do the required steps.

 

This blog is a follow-up of the tutorial for the QUERY operation, so I’ll skip some of the basic explanations.

 

Why is this blog necessary?

For the READ operation, you have to know about 2 specialties:

  • How to define the relative URL in Eclipse
  • How to write the script to meet the expected structure of the response.

 

Please find attached the source files used in this tutorial.

 

 

 

Prerequisites

 

I expect that you've gone through my previous tutorial, explaining REST data source – QUERY operation – XML payload

 

Prerequisites are the same:

  • Eclipse with SAP Mobile Platform Tools installed
  • SMP SP06
  • Basic knowledge about OData provisioning using the Integration Gateway component of SMP

 

 

Preparation

 

REST Service

 

For this tutorial, we’ll be using the following REST service as example  service for the data source:

 

http://www.thomas-bayer.com/sqlrest/CUSTOMER

 

Please find some info at http://www.thomas-bayer.com

The service is free and doesn’t require any registration.

 

The reason for choosing this service is that it supports READ of single entries.

The URL for reading a single entry is e.g.

http://www.thomas-bayer.com/sqlrest/CUSTOMER/1

where the 1 is the identifier of a customer entry

 

Destination

 

In your SMP server, create a  Destination that points to this REST service.

Please refer to the screenshot for the settings:

 

 

 

After saving the destination, try the Test Connection button: it should be successful.

If you get an error message on connection test, you might consider the following:

 

Note that you might need to enter proxy settings in your SMP:

https://localhost:8083/Admin/ -> Settings-> System

 

Note that you might need to restart the SMP server after changing the proxy settings.

 

 

 

OData Model

 

 

Create Project

Create project of type SAP Mobile Platform OData Implementation Project.

 

Define OData Model

Within the OData Modeler, create one EntityType that looks as follows:

 

 

 

 

 

Bind data source

Select the odatasrv file and from the context menu choose "Set Data Source".

 

In the wizard, you have to first select the Data Source as REST and then select the "Read" operation for the EntitySet "Customers"

 

 

 

 

Note that the "Read" option is only available since SMP SP 06

 

Click Next and specify the following relative URL and press "Finish".

 

/sqlrest/CUSTOMER/{ID}

 

 

How are we supposed to build this relative URL?

 

Here we have to understand:

As we already know, any REST service is free to use its own patterns for providing resources.

Since no READ operation is explicitly specified for REST services (like for OData), any REST service out there in the internet can implement it in its own preferred way.

Which means that our SMP server cannot deduct the URI to find a single entry, only from the service URL (of the REST service)

So it's us who have to provide the information (about how the REST service does the READ) to the SMP server.

 

Let’s have a look at our example REST service.

How is the READ implemented there:

 

http://www.thomas-bayer.com/sqlrest/CUSTOMER/1

 

There’s the segment “CUSTOMER” which provides the list of entries

Afterwards a slash

Finally a number which is the value of the property "ID" of the thomas-bayer service

 

Translated into a generic expression:

 

CUSTOMER/<value-of-ID-field>

 

(value-of-ID-field means the 1 or 2 or 42 that we can enter at the end of the thomas-bayer URL)

 

And this is what SMP expects from us:

A generic expression that provides the key property between curly braces

So we have to provide the full URI of the READ, but with a variable instead of the concrete value

But the "full URI" has to be a "relative URL", because it is concatenated with the base URL that is defined in the Destination on the server.

 

 

Custom Code

 

After finishing the binding wizard, we’re ready to create the script.

 

Within the Project Explorer View, select the “Read” node and choose Define Custom Code from the context menu.

Choose Groovy as language.

 

Now we have to implement the processResponseData() method.

 

What do we have to do?

 

Background

The REST service that we’re using supports reading of a single entry.

The URL

http://www.thomas-bayer.com/sqlrest/CUSTOMER/1

returns the following response:

 

 

 

Note that the browser doesn’t display the real full payload (the xml header is hidden), so we have to press “view source” to get the real response payload:

 

 

 

 

 

In our custom code script, we’re supposed to provide the data in a specific structure, as we’ve learned in the previous tutorials.

In the case of READ, the expected structure looks as follows:

 

 

<EntitySet>

    <Entity>

          <Property1>“value of property1”</Property1>

          <Property2>“value of property2”</Property2>

          <Property3>“value of property3”</Property3>

    </Entity>

</EntitySet>

 

 

As you can see, the structure is the same like in the QUERY scenario.

Note that for reasons of consistency, the structure contains the EntitySet, although the payload of the READ operation doesn't contain it.

 

In our custom code script, we have to modify the structure of the REST response to match the structure that is expected by the SMP server.

 

 

Intermediate step

For those of you who like to do an intermediate step: before we start to generically modify the response of the REST service in order to meet the expected structure, we can provide a hard-coded response (as we did in the first REST tutorial).

Such implementation looks as follows:

 

 

def Message processResponseData(message) {

  

   message.setBody("<Customers>"+

                                         "<Customer>"+

                                               "<ID>111</ID>"+

                                               "<FIRSTNAME>Jack</FIRSTNAME>"+

                                         "</Customer>"+

                                  "</Customers>");

          

   return message;

}

 

 

After generate, deploy, configure and run the service, you should see the result in the browser.

Check the section below for info about how to run the service.

 

Note:

In this hard-coded response, we’re setting less properties than defined in our OData model. But this is OK, at runtime the remaining properties will be empty.

Remember: Our OData model can be bigger than the REST-service, but it must not be smaller (which would cause exceptions at runtime).

 

 

Implement the script

Now let’s do the generic implementation:

Modify the structure of the REST response to match the structure that is expected by SMP.

Fortunately, in our example the REST service payload is very similar to the expected structure.

 

In detail, what we have to do, is the following:

 

  • Remove undesired xml header:
    <?xml version="1.0"?>
  • Remove undesired attributes of the entry tag
    <CUSTOMER xmlns:xlink="http://www.w3.org/1999/xlink">
  • Rename the entry name to match our EntityType name
    </Customer>
  • Surround the entry with opening and closing tags of our EntitySet name
    <Customers>
  • Add the opening Customer tag
    <Customer>


 

 

This is the simple implementation of the method:

 

 

def Message processResponseData(message) {  

    String restResponse = message.getBody();

  

       int index = restResponse.indexOf("xlink\">")  + 7;

    restResponse = restResponse.substring(index);

    restResponse = restResponse.replaceAll("</CUSTOMER>", "</Customer>");

    restResponse = "<Customers><Customer>" + restResponse + "</Customers>" ;

 

    message.setBody(restResponse);

  

       return message;

}

 

 

Result

 

After doing generate&deploy in our Eclipse project, change to your browser and open the Management Cockpit.

Assign the destination that we’ve created in the preparation step.

Invoke our OData service.

 

Note that we haven’t implemented the QUERY, so we have to directly invoke a URL for READ of a single entry:

e.g.

https://localhost:8083/gateway/odata/SAP/<your_service_name>/Customers('42')

 

The result is:

 

 

 

Summary

 

There should be 2 basic learnings from this tutorial:

 

  • How to provide the relative URL when specifying the REST data source
  • How to write the custom code to meet the expected structure for READ operation in Integration Gateway

 

Those of you who have followed my blog explaining how to use xml parser for creating the expected structure may ask: Is there a follow-up blog explaining how to do this for the READ operation?

Well, I’m not intending to create such a blog, since the procedure is exactly the same.

 

 

 

Links

 

The prerequisite tutorial that does the same like this blog, but for QUERY operation:

Introduction in REST data source part 3: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/12/integration-gateway-understanding-rest-data-source-3-query--xml-standard

 

Preparing Eclipse for Groovy scripting: http://scn.sap.com/docs/DOC-61719

 

Introduction in REST datasource part 1: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/10/understanding-rest-data-source-in-integration-gateway-1-query-very-simplified

 

Introduction in REST data source part 2: Understanding the return structure in json

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/11/integration-gateway-understanding-rest-data-source-2-query--json-very-simplified

 

Installing SMP Toolkit:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/08/22/how-to-install-sap-mobile-platform-tools-for-integration-gateway-in-eclipse-kepler

 

Tutorial for OData provisioning in SMP:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/06/10/creating-an-odata-service-based-on-sap-gateway-soap-jdbc-and-jpa-data-sources-ba

In one of my previous blogs, I’ve described how I enhanced my development experience for Groovy scripting.

I needed it for implementing Custom Scripts for Integration Gateway, which in turn is required for creating OData services based on Integration Gateway in SAP Mobile Platform 3.0.

 

Today, I’d like to extend that description with 2 tips that are useful for scripting, but independent of the chosen language.

The background is that when implementing custom script for Integration Gateway, you’re dealing with objects that are provided by Olingo library, e.g. UriInfo

 

The following example shows the generated sample Implementation in Eclipse (SAP Mobile Platform Tools).

Even after following all steps described in the mentioned blog, I’m still not satisfied.

It’s nice to get the code completion, but the names of the parameters are missing:

 

 

 

 

 

This is just a little example about how tedious it is when the Javadoc is missing.

Now have a look below, how wonderful the working environment is looking in the following screenshot:

 

 

 

 

 

 

Boy, THAT’S a pleasure!

And it’s easy to achieve, I’ll tell you:

 

You only need to attach the Javadoc to your SMP project.

 

In this blog you’ll get the details.

 

 

 

 

Adding Javadoc

 

 

The first of 2 tipps is to make the Javadoc available in your project.

You want to have the Javadocs for the APIs that you're using.

If the Javadoc isn't provided within the libs, then you still have the possibility to attach it to the jar files in Eclipse.

In this blog, I focus on Onlingo, as it is useful to have for Integration Gateway scripting.

 

 

 

Prerequisites

 

  • Install SAP Mobile Platform 3.0 server on your local file system.
  • Install Eclipse Kepler.
  • Install SAP Mobile Platform Tools into Eclipse.
  • Follow my Blog post to enhance the environment.

 

 

 

Preparation

 

  • Download the Javadoc archive from the Olingo website at http://olingo.apache.org/doc/odata2/download.html
    Direct link vor V2.
    Save the zip file on your local file system.
  • Create a SAP Mobile Platform OData Implementation Project in Eclipse.
  • Create Custom Code for Groovy.
  • Apply the tipps that I’ve described in my blog post

 

 

 

Procedure

 

After converting your project to a plug-in project by following above steps, you can see the Plug-in Dependencies node after expanding your project:

 

 

 

 

Right click on the olingo-odata2-api.jar file.

Choose “Properties”, then select “Javadoc Location".

Select the checkbox “Javadoc in archive”, then enter the path to the downloaded Javadoc zip file

Leave the “Path within archive” empty.

You can press “Validate” in order to check if the path is correct.

 

 

 

 

In order to verify the result, you can proceed as follows:

 

Open the Javadoc View in Eclipse via Window -> show View -> other -> Java -> Javadoc

Within the Project Explorer View, expand the tree of your project.

Expand the olingo-odata2-api.jar

Select a .class file, e.g. UriInfo.class

Select a method.

Result: the Javadoc for this method is displayed in the Javadoc view.

 

 

 

 

 

Adding Source Attachment

 

 

 

A second enhancement of development experience that some of you may desire, is the ability of pressing F3 (or use hyperlink) to navigate to the source code of a used class.

Since Olingo is open source, we can download the sources and attach them to our project in Eclipse.

 

 

Preparation

 

The Olingo sources can be downloaded from here

 

Note:

From the homepage http://olingo.apache.org/ you can navigate to other downloads, like V4 in future.

 

 

 

Procedure

 

 

Expand your project to the Plug-In Dependencies node.

 

 

 

Right click on the olingo-odata2-api.jar file

Choose “Properties”, then select “Java Source Attachment” in the left panel.

Select the checkbox “External Location”, then enter the path to the downloaded source-zip file

 

 

 

 

 

 

In order to verify the result, you can proceed as follows:

 

Expand the tree of your project,

Expand the olingo-odata2-api.jar

Double-click a .class file, e.g. HttpHeaders.class

Result: the source file is opened and you can enjoy, e.g. forward navigating with F3

 

 

 

 

 

 

 

Note:

You may wish to repeat the steps for the olingo.odata2-core.jar file.

Intro

 

The new Eclipse based Integrated Test Environment, which takes over the functionalities of the old ATE (Agentry Test Environment), has been awaited for quite some time, and has been released for a while now. It is delivered in SMP 3.0 SDK SP06.

 

I have had a chance to try it out, and even though there is still room for improvements, I must say that it is a MAJOR leap in the right direction.

 

It is more or less a one-to-one migration of the old ATE to an Eclipse based Test Environment, meaning no new functionalities for inspection has been added. However there is one major difference: You can now interact with the UI for other platforms than the Windows based ones. The UI is using the WPF windows client, and when choosing a non-windows based OS, it will still render in the WPF UI.

 

Still it is not a perfect rendering of iOS and Android, as on native devices. Images and screen elements may appear differently in the Test Environment and the native device, both with regards to enablement, size, format and functionality. But it is very nice to be able to inspect data objects, rules and actions, while executing screen logic. As I said a major step in the right direction.

 

The usage of the Test Environment in Eclipse is a little clumsy in my opinion, and not well suited for execution in the same environment as you make Agentry and Java development. Therefore I would suggest to install it in a dedicated Eclipse environment.

 

Furthermore I hope that SAP in the future will extend its capabilities to support multiple sessions (Agentry applications and/or backends) in the same Eclipse environment.

 

Features of the Integrated Test Environment

 

The installation and setup of the Integrated Test Environment has been nicely described in the blog, so I will not elaborate on this.

 

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/02/eclipse-integrated-test-environment-for-sap-agentry-applications

 

Note on updating the Agentry Editor (Plugin):

When upgrading your (existing) Eclipse Environment to SP07 PL01 you will get some errors, if you have an older version installed already.

Perform these steps to successfully upgrade:

1. Close your Agentry Application Project (Right-click and choose Close Project).

2. Close the Test Environment view.

3. Restart Eclipse.

You can now successfully upgrade.

 

Startup of the Test Environment

 

The Test Environment will create some files for configuration and tracing, and these files requires an Agentry Project to exist and be open before you can start the Test Environment. There is no requirement that the project must be identical to Agentry Application you want to test, so it can be any Agentry project.

 

Pic01.jpg

Warning that no open Project exists.

 

Pic02.jpg

Menu item to start the Test Environment is inactive.

 

 

Create dummy Agentry project

To avoid any confusion I have created a dummy Agentry Project in my (dedicated) Eclipse Environment.

Simply choose File->New->Other…->Agentry Project->New Agentry Project, and create an Agentry application with no implementation.

 

Pic03.jpg

Pic04.jpg

After creation of the dummy application it is now possible to start the Test Client (Test Environment).

 

 

When starting the Test Environment you will need to provide credentials (username and password) as well as a URL to hit the Agentry instance on SMP.

 

Pic05.jpg

After startup you will see this screen.

Click "Menu" to go to the application.

From here you can also export the local DB of the application, as well as reset the client.

 

Pic06.jpg

In my case I am accessing a modified version of SAP Work Manager 6.1 which is displayed.

For the first time in Agentry history, it is possible to interact with the UI of an Android screen set

 

 

The Menus

 

Before you can access the menus you need to minimize the Application screen. If you are Alt-Tab'ing between screens, you must minimize every time you return to Eclipse.

 

Pic07.jpg

You cannot minimize or hide this window.

This is the reasons why I call the implementation "clumsy". It is not working smoothly, but still it is minor bugs.

 

After moving the screen out of the way you will gain access to the menus.

 

Pic08.jpgPic09.jpg

Pic10.jpgPic11.jpg

If you are familiar with the ATE, you will recognize all of the menu items, as they also existed here.

 

However a few notes on functionality:

  • I have not been able to get the Transaction display to function. Even when I am sure I have executed some Add Transactions, I don't see the data in the Transactions view.
  • View for Complex Table no longer shows number of records in table, which is a little bit annoying.
  • The Clear Log… functionality does not work. You must delete the debug.log file in the package explorer to clear it.

 

Pic12.jpg

Windows displayed by the Test Environment in Eclipse:

  • Top Left: The dummy Agentry Application.
  • Middle Left: Annoying window that cannot be minimized while inspecting a running application.
  • Top Middle: Debug log of an Action set for logging.
  • Bottom Left: Objects inspection.
  • Bottom Middle: Transaction inspection (which is always empty)
  • Bottom Right: Complex Table inspection.

 

All-in-all a very useful Agentry Integrated Test Environment.

There are some bugs to be fixed, but for a first version SAP has done a very good job.

 

Regards,

Søren Hansen

Dear All,

 

After all the struggle and trial and error i finally figured out the way on how to configure SSO for SMP 3.0. SSO is quite required as well know for role based authorization for SAP. Earlier during MBO it was quite simple to do this but this isnt the case with SMP 3.0.

I am writting this blog post so that who ever tries this in the future doesnt undergo so much pain.

 

For this blog post i m using the latest version of SMP 3.0 SP6.

So lets get started.

 

Log in to you SAP GUI and run the transaction STRUST.

 

Select the SNC note and download the SAP ECC Systems Certificate

 

Screen Shot 2015-02-27 at 11.17.26 AM.png

 

Export the file by clicking on "Export Certificate", click on "Base 64" to save the certificate file on a directory and copy the same on the SMP Server Console

 

Screen Shot 2015-02-27 at 11.21.05 AM.png

 

Once you have the certificate file exported, log in to the SMP Server Console and copy this file on that machine. Download Portecle from this site. We will use Portecle to import the above exported certificate into our SMP Key Store. Once downloaded , launch Portecle(its just a jar file so no need for installation) and open the SMP Keystore file.

 

File-->Open KeyStore File

 

 

 

Screen Shot 2015-02-27 at 11.26.28 AM.png

 

Go the SMP KeyStore path. Its located in ~Installation Directory/SAP/MobilePlatform3/Server/configuration with the name smp_keystore.jks. It will prompt you for a password. Give the password that you used during installation of SMP Server or use the default password changeit.

 

Once you log in you would see something like this.

 

Screen Shot 2015-02-27 at 11.33.54 AM.png

Go to Tools-->Import Trusted Certificate to import the SAP ECC Certificate we exported using STRUST.

 

Screen Shot 2015-02-27 at 11.34.59 AM.png

 

Screen Shot 2015-02-27 at 11.38.24 AM.png

 

Click on Import. It will show you the details of the certificate and then click on Next. Give an Alias for the Certificate and click on Finish.

So we have added the SAP ECC Certificate to SMP Key Store.

 

Next Step would be to create a Destination on Gateway Cockpit and also to create an Application on Management Cockpit

 

I would assume for this that you have already deployed an application on SMP. If you are not sure how to develop an SMP OData based application with SAP Netweaver Gateway i would request you to follow this blog written by my friend Jitendra Kansal

 

Please follow the steps from the blog post to create an Eclipse based Project, Deploying the project on SMP Server, Creating a Destination in Gateway Cockpit, Importing the IWBEP Service under the destination to our SMP Server and Assigning the Destination to the Deployed Service.

 

(Plz note for the above blog he is using the SAP Netweaver Demo Service from the cloud. If you do not have an account and have an on premise Netweaver Gateway and Do not know how to proceed to creating your own Service I will write a blog post for that soon . You can now check it under this blog post on how to create a Netweaver Gateway Project for on Premise)

 

So now we have a Gateway Destination already built.

Go to the same tab of Destination under Gateway Cockpit and open the destination in change mode

 

Make the changes as below.

Under SSO Mechanism, Add SSO, Edit the Technical User(Basic) and change the password to some wrong password and Save it

 

Screen Shot 2015-02-26 at 3.40.28 PM.png

 

Note :

1. My Destination URL here is different from the one in the blog post as I am using an onpremise url of the netweaver Gateway

2. Changing the password for the Technical User(Basic) is very important else the supplied credentials will not over ride the user credentials and everytime you call the service even with SSO it will always call the backend service with the user you configured in Technical User (Basic).

 

Screen Shot 2015-02-26 at 4.20.41 PM.png

 

So now we are done with the Gateway Configuration for the Destination to support SSO.

Next Thing that we would be doing is to configure an Application on Management Cockpit and then give it an Authentication Mechanism.

Login to the Management Cockpit using http://localhost:8083/Admin

 

Click on the Settings Tab and click Security Profiles

 

Click on New to create a new Security Profile.

We will name it as SAP_SSO2

 

Give the Authentication Provider as HTTP/HTTPS Authentication and provide the same URL that we used while defining the Gateway Destination

 

Screen Shot 2015-02-27 at 11.01.16 AM.png

 

For the Cookie Name give it as MYSAPSSO2

 

Screen Shot 2015-02-27 at 12.13.02 PM.png

 

Click Save to save this security Config. We will assign the same security config to our application connection.

Create a new application connection by clicking on new under the Application Tab

 

Under Backend Connection give the URL of the Service Document of your Deployed Application on Gateway Cockpit.

It would be something like https://localhost:8083/gateway/odata/SAP/<NameofyourdeployedApplication>;v=1

Check the Internal checkbox.

 

Go to the Authentication Tab and Select the authentication mechanism we just created SAP_SSO2

 

Screen Shot 2015-02-27 at 12.19.38 PM.png

 

Click on Save to save all the configuration.

Now we are all done with our configuration.

Time for Testing

 

You can use the Advanced Rest Client Extension of Google Chrome for this. You can use any other rest client for this purpose.

 

Go the the rest client and give the url for registration of user

 

URL : http://<hostaddress>:8080/odata/applications/latest/<applicationid we created>/Connections

 

Give the Headers

Content Type : application/xml

Operation      : POST

 

And post this under the Request Body

 

<?xml version='1.0' encoding='utf-8'?>


     <entry xmlns="http://www.w3.org/2005/Atom"

               xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"

               xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">

     <title type="text"/>

     <updated>2012-06-15T02:23:29Z</updated>

     <author>

     <name/>

     </author>

      <category
          term="applications.Connection"
          scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>

     <content type="application/xml">

     <m:properties>

     <d:DeviceType>IOS</d:DeviceType>

     <d:DeviceModel m:null="true" />

     </m:properties>

     </content>

     </entry>

 

It should look something like this

 

Screen Shot 2015-02-27 at 12.25.27 PM.png

 

Click on Send to post it. It will prompt you for credentials. Give the credentials to authenticate you against the URL that we configured for the security configuation. It will return with a status 201 which will create your user in Admin cockpit. You can check it against the application id that you should now have one registration.

 

Things to note are the cookies that are returned back. We have a cookie by the name X-SMP-APPCID.

 

Next we will test if we can now retrieve the data against that user to check if SSO is configured and SAP Returns on data for this user

 

With the same Rest Client give the below addres

 

URL : http://localhost:8080/<applicationid>/<AnyEntitySet>

In the header specify the cookie

X-SMP-APPCID and the value you got from the previous registration call

 

Operation : GET

 

Note : user GET if you entityset is used for reading operation. If there is a post operation involved you would also need an additional token of x-csrf-token.

 

Click on Send and you should receive Data for that particular user itself.

 

Screen Shot 2015-02-27 at 12.34.16 PM.png

 

Hope this helps all of the people who were facing difficulty in configuring SSO.

 

Reference from Tejesvi DVR, Suma S

 

Thanks,

Rakshit Doshi

 

 

 




Use Case:

To access SAP Gateway exposed services via SMP 3.0 using SSO Cookie based authentication

 

Pre-requisites:

SAP Gateway system is capable of handling cookies eg: MYSAPSSO2

 

 

Steps to Create MYSAPSSO2 Scenario

 

 

I) Enable SMP to access the backend https url:



  1. Open File->Open Keystor fFile in portecle
    Tool and go the Location where the smp_keystore.jks file is Located. i.e. SMP
    server->Configurations  and click ok to open the
    Keystore.jks, Password for the keystore is "changeit".

 

p1.png

 

 

   2)  Go to Tools-> Import Trusted Certificate
and select the Back End System's Certificate from your system and click on
import

 

p2.png

   3)  Click on the ok and yes buttons as shown below.

 

p3.png

 

 

 

p4.png

 

 

4) Enter the Alias Name for the
Certificate which is getting imported

 

5) Save the Keystore in the
portecle Tools once the certificate is imported. Otherwise the imported
certificate will not be Reflected in SMP server's Configurations.

p5.png

 

 

II) SMP Gateway Cockpit Steps:

 

  1. Log on to the SMP Admin Cockpit https://<host>:8083/gateway/cockpit and go to the Destinations-> New
    Destination

           Provide the Destination Name, select destination type as HTTP, provide Destination URL, and Select Authentication
      Type as SSO Mechanism

 

p6.png

 

Click on Add button to select from various SSO Mechanisms.

 

Select Technical User(Basic) authentication as authentication mechanism,
and click on Save button.

 

 

p7.png

 

 

Different SSO Mechanisms at the Gateway cockpit is mainly used to test the destination connection and to initially test
the services in the cockpit.

 

 

The security profile created at the SMP Admin will overwrite the destination configurations created at the gateway
cockpit.

 

 

In a productive scenario we need to consume the services exposed in gateway cockpit through SMP.

 

 

For different SSO Mechanisms refer to the link

 

 

http://help.sap.com/saphelp_smp306svr/helpdata/en/7c/2dd0d470061014a8bfb9194fa26274/content.htm

 

 

Once the destination is created, in the https://<host>:8083/gateway/cockpit Click on register button,
provide the destination created and search for the service required to be
registered.

 

p8.png

 

p9.png

   With this we have registered the service in gateway cockpit. If we need to access this service by cookie based SSO
mechanism through SMP follow the steps mentioned in the next section.

 

 

III) SMP ADMIN Cockpit Steps


1. Log on to the SMP Admin Cockpit https://<host>:8083/Admin and go to the Settings-> Security
Profile tab.

 

2.  Create a security profile with HTTP/HTTPS Authentication and provide the URL of the Back end System from where odata
service is hosted and MYSAPSSO2 Cookie is enabled. Click on Save as Shown Below.

 

 

p10.png

 

p11.png

3. In the SMP Admin Cockpit https://<host>:8083/Admin create the application for the service

document url exposed in gateway cockpit as shown in screen shots below:

 

p12.png

 

p13.png

 

Provide the odata service exposed through gateway cockpit, and mark the service as internal.

 

 

app3.PNG

 

app4.PNG


Once this is done, application is ready to be consumed.


IV) Onboarding through REST CLIENT

 

Onboard to the created application in the REST client/Mobile Application though a
device


     Onboarding URL:

    

          http://<host>:8080/odata/applications/latest/<application id created in SMP>/Connections     


     Onboarding Headers:


          Content-Type: application/xml

          x-smp-appcid: <application1>

 

     Onboarding Post Body:


     <?xml version='1.0' encoding='utf-8'?>


     <entry xmlns="http://www.w3.org/2005/Atom"

               xmlns:d="http://schemas.microsoft.com/ado/2007/08/dataservices"

               xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">

     <title type="text"/>

     <updated>2012-06-15T02:23:29Z</updated>

     <author>

     <name/>

     </author>

      <category
          term="applications.Connection"
          scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>

     <content type="application/xml">

     <m:properties>

     <d:DeviceType>IOS</d:DeviceType>

     <d:DeviceModel m:null="true" />

     </m:properties>

     </content>

     </entry>

    

     Onboarding Operation: POST


          While onboarding it will initially ask for the user details, and for the subsequent requests it will use the Cookie we have configured.

 

After onboarding the entities of the application can be accessed via the url

 

http://<host>:8080/ <application id created in SMP>/<entites>

Headers:

 

  Content-Type: application/xml

  x-smp-appcid: <application1>

 


 

 

 



 
 
 
 
 
 
 
 
 
 
 
 





Some of the features that can be enabled using custom script in Integration Gateway (IGW) are: modify the requests and responses of an Odata request, handle delta token , multipart and composite WSDL support, and SOAP authentication. There are many examples in SCN on how to write custom script in Integration Gateway. In this blog I am explaining how to easily debug the script using IGW's eclipse based tooling.

 

From SMP SP06, debugging of custom script written in JavaScript is possible (Groovy supports in future). I have created a simple project using the WSDL http://www.webservicex.net/medicareSupplier.asmx?WSDL. High level details of the project is given below. Here I am not explaining how to create a project but I am explaining how to debug the script used in the project.

 

Odata Entity:

ScreenHunter_36 Feb. 24 23.40.jpg

 

 

Custom Script written under function processRequestData

  //Import statements

   importPackage(com.sap.gateway.ip.core.customdev.logging);

   importPackage(com.sap.gateway.ip.core.customdev.util);

   importPackage(org.apache.olingo.odata2.api.uri);

   importPackage(java.util);

   importPackage(com.sap.gateway.core.ip.component.commons);

   importPackage(com.sap.gateway.ip.core.customdev.api);

   importPackage(org.apache.olingo.odata2.api.processor);

   //Getting value from header

   var context = message.getHeaders().get("odatacontext");

   var City = context.getRequestHeaders().get("City").get(0);

   var child = new LinkedHashMap();

   child.put("key:City", City);

   message.setBody(child);

   return message;

 

How to start debugging ?

  • Open prop.ini file of SMP server (C:\SAP\MobilePlatform3\Server)  and add the property -Drhino.debugger.port=9123

      ScreenHunter_36 Feb. 24 18.40.jpg

  • From eclipse, click on down arrow next to debug menu and choose debug configurations.

     Untitled2.png

If debug menu is not visible: Go to Windows > Customize Perspective. "Navigate to Commands group visibility" tab. Then select "Launch" option in the Availability Command Groups and click OK.

  • On the new window opened, provide below details:

     Untitled.png

  • Navigate to Source tab and provide the path to the script file and click on Debug.

    Untitled1.png

  • Open JavaScript file and set a break point as given below.

     Untitled3.png

 

  • Execute the Odata service from a rest client by passing City as header along with Authorization header.

ScreenHunter_36 Feb. 25 00.00.jpg

    

  • It triggers the break point set in the Javascript. To see the data send from rest client (ie. City) add Expression String(child) as given below. If Expressions tab not visible: Go to Windows > Show view > Expressions.

ScreenHunter_36 Feb. 24 23.59.jpg

 

Regards, Midhun

SAP Technology RIG

This  blog is (even more) about Integration Gateway in SAP Mobile Platform 3.0 (SMP).

It is meant for those of you who have already created OData services based REST data source through Integration Gateway in SMP.

If you’ve implemented the script as described in my previous tutorial and if you’ve wondered if there isn’t a different way to convert the payload than just doing string operations… (only) then you should have a look at the following blog.

 

I’ve demonstrated it for XML – this blog will now do the same for JSON payload.

 

Note that this is not an official guide, just my personal findings and ways to do my stuff.

It might not meet all your requirements – so please do let me know your concerns or suggestions for improvements.

BTW, this tutorial is based on SMP SP05.

 

 

Prerequisites

 

  • My previous tutorial where we’ve created an OData service based on a REST data source
  • SAP Mobile Platform 3.0 SP 05
  • Eclipse Kepler with SAP Mobile Platform Tools installed

 

 

 

 

Preparation

 

 

SAP Mobile Platform

  • As described in one of my previous tutorials  you need to create a destination in your SMP server, that points to the host of the REST service that is used in this tutorial:
    http://sansad.co  with "no Authentication"
    Try the “Test Connection” button: the result should be green success popup.

    Note: you might need to enter proxy settings at https://localhost:8083/Admin/  -> Settings -> System

 

Eclipse

  • Create OData Implementation Project
  • Create OData model: We have to create an OData model that matches the REST service.

    Our model looks as follows:


    You can create the properties manually, or import the edmx file that is attached to this blog.
    Bind the QUERY operation to our example REST service: /api/legislators
  • Create Custom Code script for Groovy
    If desired, do the few preparation steps to enhance your development experience for Groovy, as explained here
  • Add dependency to gson in the MANIFEST.MF file

 

 

That’s it for the preparation.

Let’s start implementing the script.

 

 

 

Implementing the data structure conversion using JSON parser

 

 

 

Overview

 

We will be implementing only the method processResponseData()

 

The steps are:

 

  1. Get the payload string
  2. Parse the JSON payload
  3. Refactor
  4. Transform the JSON-object to String
  5. Set the converted payload string

 

 

 

1. Step: Get the payload

 

This is the same step a usual:

 

 

       String payload = message.getBody().toString();

 

 

 

2. Step: Parse the JSON payload

 

For parsing JSON, there are several libraries that could be used.

In this tutorial, we’re using com com.google.gson, the reason is that it is included in SMP, so we don’t have to install any own library/bundle to our SMP .

 

There are also several approaches for parsing JSON, one possibility is shown in the code below.

 

 

def JsonElement parsePayload(String payload){

 

     JsonParser parser = new JsonParser();

 

     return parser.parse(payload);

}

 

 

 

3. Step: Refactor the JSON Elements

 

 

Quick recap: what do we have to do?

This is the response body that we get from the REST service (http://sansad.co/api/legislators):

 

 

 

And this is the structure that is expected/required by Integration Gateway:

 

 

{

"results":

     [

          {

               "PropertyName1":"PropertyValue1",

               "PropertyName2":"PropertyValue2"

          }

     ]

}

 

 

 

So, we have to re-structure the above REST-json-structure, in order to get the structure that is expected by Integration Gateway.

 

In detail:

  1. Delete the undesired (informative) nodes count and page
  2. Remove all the properties that we don’t have in our OData model

 

We use API that is provided by com.google.gson:

 

 

def JsonObject refactorJsonElement(JsonElement jsonElement){

 

     JsonObject feedJsonObject = jsonElement.getAsJsonObject(); // e.g. {"results":[{"age":60,"...

 

     //first refactoring

     doRefactoringForFeed(feedJsonObject);

 

     // second refactoring: need to remove some properties, because the aren't defined in our OData model

     JsonArray resultsArray = feedJsonObject.get("results").getAsJsonArray(); // it is a feed (array)

 

     // we're performing a QUERY operation, so loop over all entries and refactor each

     for(int i = 0; i < resultsArray.size(); i++){

          JsonElement entryElement = resultsArray.get(i);

          if(entryElement instanceof JsonObject){

               //remove undesired properties, corresponding to edmx

               doRefactoringForEntry((JsonObject)entryElement);

          }

     }

 

     return feedJsonObject;

}

 

 

  1. In a first step, we get the top-level node, that is the feed, and remove the 2 undesired child nodes.
    These 2 nodes are for information purpose and don't match to data structure



    And this is how it is removed:


    def JsonObject doRefactoringForFeed(JsonObject feedJsonObject){
           feedJsonObject.remove("count");
           feedJsonObject.remove("page");

                return feedJsonObject;
    }


  2. In a second step, we ask the feed node for the list of entries, loop over them and for each entry, we remove all the undesired properties.
    The “undesired properties” are those that are in the REST-service, but not in our OData model.
    In the following screenshot, only the desired properties are marked red:




    To make the sample code more comprehensive, I’ve hard-coded the  names of the undesired properties.
    (There’s also a way to do it generically, I’ll publish it in a separate blog.)


    def JsonObject doRefactoringForEntry(JsonObject entryObject){
        //remove undesired properties, corresponding to edmx
        entryObject.remove("age");
        entryObject.remove("attendance_percentage");
        entryObject.remove("constituency");
        entryObject.remove("debates");
        entryObject.remove("education_details");
        entryObject.remove("education_qualification");
        entryObject.remove("elected");
        entryObject.remove("gender");
        entryObject.remove("house");
        entryObject.remove("in_office");
        entryObject.remove("last_name");
        entryObject.remove("notes");
        entryObject.remove("private_bills");
        entryObject.remove("questions");
        entryObject.remove("term_end");
        entryObject.remove("term_start");

        return entryObject;
    }

 

 

4. Step: Transform the JSON object to String

 

Now that we have manipulated the JSON nodes as desired, we’re ready to transform it back to string.

 

 

def String transformToString(JsonObject jsonObject){

   

       return jsonObject.toString();

}

 

 

 

 

5. Step: Set the payload

 

The only thing that’s missing is to send the converted payload back to the Integration Gateway.

This is done same way like in the previous tutorials, by setting the header.

Here’s now our convertPayload() method, that invokes all the other methods described above.

We invoke this method in the processResponseData() callback method.

 

 

def void convertPayload(Message message){

   

     String payload = getResponsePayload(message);

 

     //parse the payload and transform into JsonObject

     JsonElement jsonElement = parsePayload(payload);

 

     // now do the refactoring: throw away useless nodes from backend payload

     JsonObject jsonObject = refactorJsonElement(jsonElement);

 

     // transform back to string

     String structuredJsonString = transformToString(jsonObject);

   

     if(structuredJsonString == null){

          log.logErrors(LogMessage.TechnicalError, "Error: failed to convert the backend payload to structured XML");

          //TODO proper error handling

          return;

     }

   

     // finally

     message.setBody(structuredJsonString);

     message.setHeader("Content-Type", new String("json"));

}

 

 

 

Final: Run

 

Now we can generate&deploy in Eclipse, and after configuring the destination, we can run and test the service.

 

Note:

Our sample code is of course not enterprise-ready, but that’s normal of course.

More relevant is that our sample code doesn’t contain error handling, I mean, if the REST service doesn’t return the expected xml-payload, then we have to react appropriately and provide an empty feed, or a meaningful error message in the browser, set the proper HTTP Status code, things like that.

 

I’m pasting the full script below, and I’m also attaching the relevant files to this blog post.

 

 

 

Full source code

 

 

 

import com.google.gson.JsonArray

import com.google.gson.JsonElement

import com.google.gson.JsonObject

import com.google.gson.JsonParser

import com.sap.gateway.ip.core.customdev.logging.LogMessage

import com.sap.gateway.ip.core.customdev.util.Message

 

 

 

/*

* ***************************

*  FRAMEWORK CALLBACK METHODS

* ***************************

*/

 

/**

  Function processRequestData will be called before the request data is

  handed over to the REST service and can be used for providing

  filter capabilities. User can manipulate the request data here.

*/

def Message processRequestData(message) {

 

     return message;

}

 

 

/**

  Function processResponseData will be called after the response data is received

  from the REST service and is used to convert the REST response to OData

  response using String APIs. User can manipulate the response data here.

*/

def Message processResponseData(message) {

 

     convertPayload(message);

 

     return message;

}

 

 

/*

* ***************************

*  RESPONSE HANDLING

* ***************************

*/

 

 

def void convertPayload(Message message){

   

     String payload = getResponsePayload(message);

 

     //parse the payload and transform into JsonObject

     JsonElement jsonElement = parsePayload(payload);

 

     // now do the refactoring: throw away useless nodes from backend payload

     JsonObject jsonObject = refactorJsonElement(jsonElement);

 

     // transform back to string

     String structuredJsonString = transformToString(jsonObject);

   

     if(structuredJsonString == null){

          log.logErrors(LogMessage.TechnicalError, "Error: failed to convert the backend payload to structured XML");

          //TODO proper error handling

          return;

     }

   

     // finally

     message.setBody(structuredJsonString);

     message.setHeader("Content-Type", new String("json"));

}

 

 

 

def String getResponsePayload(Message message){

     String payload = message.getBody().toString();

   

     //TODO do some checks on the payload here

   

     return payload;

}

 

 

def JsonElement parsePayload(String payload){

 

     JsonParser parser = new JsonParser();

   

     return parser.parse(payload);

}

 

 

def JsonObject refactorJsonElement(JsonElement jsonElement){

   

     JsonObject feedJsonObject = jsonElement.getAsJsonObject(); // {"results":[{"age":60,"...

   

     //first refactoring

     doRefactoringForFeed(feedJsonObject);

   

     // second refactoring: need to remove some properies, because the aren't defined in our odata model

     JsonArray resultsArray = feedJsonObject.get("results").getAsJsonArray(); // it is a feed / array

   

     // we're performing a QUERY operation, so loop over all entries and refactor each

     for(int i = 0; i < resultsArray.size(); i++){

          JsonElement entryElement = resultsArray.get(i);

          if(entryElement instanceof JsonObject){

               //remove undesired properties, corresponding to edmx

               doRefactoringForEntry((JsonObject)entryElement);

          }

     }

 

     return feedJsonObject;

}

 

 

def JsonObject doRefactoringForFeed(JsonObject feedJsonObject){

     feedJsonObject.remove("count");

     feedJsonObject.remove("page");

   

     return feedJsonObject;

}

   

   

def JsonObject doRefactoringForEntry(JsonObject entryObject){

     //remove undesired properties, corresponding to edmx

     entryObject.remove("age");

     entryObject.remove("attendance_percentage");

     entryObject.remove("constituency");

     entryObject.remove("debates");

     entryObject.remove("education_details");

     entryObject.remove("education_qualification");

     entryObject.remove("elected");

     entryObject.remove("gender");

     entryObject.remove("house");

     entryObject.remove("in_office");

     entryObject.remove("last_name");

     entryObject.remove("notes");

     entryObject.remove("private_bills");

     entryObject.remove("questions");

     entryObject.remove("term_end");

     entryObject.remove("term_start");

   

     return entryObject;

}

 

 

def String transformToString(JsonObject jsonObject){

   

     return jsonObject.toString();

}

 

 

 

 

 

 

Further Reading

 

 

Preparing Eclipse for Groovy scripting:

http://scn.sap.com/docs/DOC-61719

 

Introduction in REST datasource part 1: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/10/understanding-rest-data-source-in-integration-gateway-1-query-very-simplified

 

Introduction in REST datasource part 2: Understanding the return structure in json

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/11/integration-gateway-understanding-rest-data-source-2-query--json-very-simplified

Introduction in REST datasource part 3: converting xml payload of a REST service for usage in Integration Gateway

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/12/integration-gateway-understanding-rest-data-source-3-query--xml-standard

 

Introduction in REST datasource part 4: implementing filter in custom script for Integration Gateway

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/17/integration-gateway-understanding-rest-data-source-4-filtering

 

Introduction in REST datasource part 5: converting xml payload using xml parser instead of string operations

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/18/integration-gateway-understanding-rest-data-source-5-using-xml-parser

 

Installing SMP toolkit:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/08/22/how-to-install-sap-mobile-platform-tools-for-integration-gateway-in-eclipse-kepler

 

Tutorial for Integration Gateway:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/06/10/creating-an-odata-service-based-on-sap-gateway-soap-jdbc-and-jpa-data-sources-ba

 

Tutorial about scripting in Integration Gateway:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/10/17/integration-gateway-using-custom-scripting-to-map-entity-sets-to-data-sources

This  blog is (once more) about Integration Gateway in SAP Mobile Platform 3.0.

It is meant for those of you who have already created OData services based on REST data source through Integration Gateway in SAP Mobile Platform 3.0 (SMP).

If you’ve implemented the script as described in my previous tutorial and if you’ve wondered if there isn’t a different way to convert the payload than just doing string operations… (only) then you should have a look at the following blog.

 

In the bottom of my heart… I was wondering if string operations are the only way to convert a payload from one format to another one.

The REST services that I had tried, are providing nicely structured xml payload in their response body.

So why not parse it with an XML parser?

 

In this blog, I’m  describing how I’ve written a prototype for manipulating the payload of a REST service with an XML parser (instead of string operations).

This is not an official guide.

It might not meet all your requirements – so please do let me know your concerns or suggestions for improvements.

This tutorial is based on SMP SP05.

 

 

 

Prerequisites

 

  • My previous tutorial where we’ve created an OData service based on a REST data source
  • SAP Mobile Platform 3.0 SP 05
  • Eclipse Kepler with SAP Mobile Platform Tools installed

 

 

 

Preparation

 

SAP Mobile Platform

As described in one of my previous tutorials you need to create a destination that points to the host of the REST service that is used in this tutorial:

http://services.gisgraphy.com

with "no Authentication".

 

Eclipse

Create OData Implementation Project.

Create OData model or better: import the model which we used in the previous tutorial

Create binding to our example REST service: /street/streetsearch?lat=41.89&lng=-87.64

Create Custom Code script for Groovy.

 

The following tutorial shows how to implement the script.

 

 

 

 

Implementing the data structure conversion using XML parser

 

 

 

 

Overview

 

We will be implementing only the method processResponseData()

 

The steps are:

 

  1. Get the payload string
  2. Parse the xml / convert to DOM
  3. Refactor the DOM
  4. Transform the DOM to String
  5. Set the converted payload string

 

 

1. Step: Get the payload

 

This is the same step a usual:

 

 

       String payload = message.getBody().toString();

 

 

 

2. Step: Parse the XML

 

 

In this tutorial, we’re using w3c.dom - API

But before we can parse the response body, we have to transform it to InputSource.

This is the code:

 

 

def Document parsePayload(String payload) {

 

     InputStream inputStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));

     InputSource inputSource = new InputSource(inputStream);

 

     DocumentBuilder parser;

     try {

          parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();

  

          // now parse

                  return parser.parse(inputSource);

     } catch (ParserConfigurationException e) {

          log.logErrors(LogMessage.TechnicalError, "Error: failed to create parser");

                  return null;

     } catch (SAXException e) {

          log.logErrors(LogMessage.TechnicalError, "Exception ocurred while parsing the response body");

                  return null;

     }

}

 

 

 

 

3. Step: Refactor the Document

 

Quick recap: what do we have to do?

This is the response body that we get from the REST service:

 

 

 

 

And this is the structure that is expected by Integration Gateway:

 

<EntitySetName>

    <EntityName1>

          <PropertyName1>“value of property1”</PropertyName1>

          <PropertyName2>“value of property2”</PropertyName2>

          <PropertyName3>“value of property1”</PropertyName3>

    </EntityName1>

    <EntityName2>

          <PropertyName1>“value of property1”</PropertyName1>

          <PropertyName2>“value of property2”</PropertyName2>

          <PropertyName3>“value of property1”</PropertyName3>

    </EntityName2>

</EntitySetName>

 

 

So, we have to re-structure the above REST-xml-structure, in order to get the structure that is expected by Integration Gateway.

 

In detail:

Rename the root node <results> to <StreetSet>

Rename the data nodes <result> to <Street>

Delete the info nodes <numFound> and <QTime>

Delete the attribute xmlns=”…”  in the root node

 

Fortunately, the w3c.dom package provides support for modification of xml nodes.

My proposal for implementation:

 

 

def Document refactorDocument(Document document){

 

     if(document == null){

          log.logErrors(LogMessage.TechnicalError, "Could not load xml-document");

         return;

     }

 

     //find nodes

     Node resultsElement = document.getFirstChild();

     NodeList childrenOfResults = resultsElement.getChildNodes();

 

     // rename the root node: <results xmlns="http://gisgraphy.com">

     document.renameNode(resultsElement, resultsElement.getNamespaceURI(), "StreetSet");

 

     // remove xmlns-attribute from root node: example: <results xmlns="http://gisgraphy.com">

     NamedNodeMap attributesMap = resultsElement.getAttributes();

     if(attributesMap.getNamedItem("xmlns") != null){

         try {

          attributesMap.removeNamedItem("xmlns");

          } catch (DOMException e) {

               log.logErrors(LogMessage.TechnicalError, "Failed removing attribute.");

          }

     }

 

     // nodes to delete:

     Node numNode;

     Node qtNode;

 

     // rename all nodes of the REST service:   <result>...

     for(int i = 0; i < childrenOfResults.getLength(); i++){

          Node childNode = childrenOfResults.item(i);

          String nodeName = childNode.getNodeName();

  

                  if(nodeName.equals("numFound")){ // store this node for later deletion

               numNode = childNode;

          } else if(nodeName.equals("QTime")){ // store this node for later deletion

               qtNode = childNode;

          } else{ // rename this node

               document.renameNode(childNode, childNode.getNamespaceURI(), "Street");

          }

     }

 

     // delete 2 undesired nodes <numFound>50</numFound> and <QTime>..</QTime>

     if(numNode != null){

          resultsElement.removeChild(numNode);

     }

     if(qtNode != null){

          resultsElement.removeChild(qtNode)     

     }

 

     // done with refactoring

     return document;

}

 

 

 

4. Step: Transform the Document to String

 

Now that we have manipulated the Document as desired, we’re ready to transform it back to String.

Here’s kind of default code, nothing to explain here:

 

 

def String transformToString(Document document, String encoding) {

 

     // Explicitly check, otherwise method returns an XML Prolog

     if (document == null) {

          log.logErrors(LogMessage.TechnicalError, "Document is null.");

                  return null;

     }

 

     // create the transformer

     TransformerFactory transformerFactory = TransformerFactory.newInstance();

     Transformer transformer = null;

     try {

          transformer = transformerFactory.newTransformer();

     } catch (TransformerConfigurationException e) {            

          log.logErrors(LogMessage.TechnicalError, "Failed creating Transformer.");

                  return null;

     }

 

     // configure the transformer

     transformer.setOutputProperty(OutputKeys.METHOD, "xml");

     transformer.setOutputProperty(OutputKeys.INDENT, "yes");

     transformer.setOutputProperty(OutputKeys.ENCODING, encoding);

     transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

     transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

 

     // prepare the output

     OutputStream outputStream = new ByteArrayOutputStream();

     OutputStreamWriter outputStreamWriter = null;

     try {

          outputStreamWriter = new OutputStreamWriter(outputStream, encoding);

     } catch (UnsupportedEncodingException e) {

          log.logErrors(LogMessage.TechnicalError, "Failed creating OutputStreamWriter");

                  return null;

     }

 

     // Finally do the transformation

     BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);

     try {

          transformer.transform(new DOMSource(document), new StreamResult(bufferedWriter));

     } catch (TransformerException e) {

          log.logErrors(LogMessage.TechnicalError, "Transformation failed");

                  return null;

     }

 

     return outputStream.toString();

}

 

 

 

5. Step: Set the payload

 

The only thing that’s missing is to send the converted payload back to Integration Gateway.

This is done same way like in the previous tutorials, by setting the header.

Here’s now our convertPayload() method, that invokes all the other methods described above.

We invoke this method in the processResponseData() callback method.

 

 

def void convertPayload(Message message){

 

     String payload = getResponsePayload(message);

 

     //parse the payload and transform into Document

     Document document = parsePayload(payload);

 

     // now do the refactoring: throw away useless nodes from backend payload

     document = refactorDocument(document);

     String structuredXmlString = transformToString(document, "UTF-8");

 

     if(structuredXmlString == null){

          log.logErrors(LogMessage.TechnicalError, "Conversion failed");

          //TODO proper error handling

                  return;

     }

 

     // finally

     message.setBody(structuredXmlString);

     message.setHeader("Content-Type", new String("xml"));

}

 

 

 

Final: Run

 

Note:

Our sample code is of course not enterprise-ready, but that’s normal for a tutorial.

More relevant is that our sample code doesn’t contain error handling, I mean, if the REST service doesn’t return the expected xml-payload, then we have to react appropriately and provide an empty feed, or a meaningful error message in the browser, we have to set the proper HTTP Status code, things like that.

 

I’m pasting the full script below, and I’m also attaching the relevant files to this blog post.

Note that the attached files are based on SP05. So if you use them for a different version, you have to adapt them, e.g. update the dependencies in the manifest file.

 

Now we can generate&deploy in Eclipse, and after configuring the destination, we can run and test the service.

 

 

 

Full source code

 

 

import java.nio.charset.StandardCharsets

 

import javax.xml.parsers.DocumentBuilder

import javax.xml.parsers.DocumentBuilderFactory

import javax.xml.parsers.ParserConfigurationException

import javax.xml.transform.OutputKeys

import javax.xml.transform.Transformer

import javax.xml.transform.TransformerConfigurationException

import javax.xml.transform.TransformerException

import javax.xml.transform.TransformerFactory

import javax.xml.transform.dom.DOMSource

import javax.xml.transform.stream.StreamResult

 

import org.w3c.dom.DOMException;

import org.w3c.dom.Document

import org.w3c.dom.NamedNodeMap;

import org.w3c.dom.Node

import org.w3c.dom.NodeList

import org.xml.sax.InputSource

import org.xml.sax.SAXException

 

import com.sap.gateway.ip.core.customdev.logging.LogMessage

import com.sap.gateway.ip.core.customdev.util.Message

 

 

/*

* ***************************

*  FRAMEWORK CALLBACK METHODS

* ***************************

*/

 

 

/**

  Function processRequestData will be called before the request data is

  handed over to the REST service and can be used for providing

  filter capabilities. User can manipulate the request data here.

*/

def Message processRequestData(message) {

 

       return message;

}

 

/**

  Function processResponseData will be called after the response data is received

  from the REST service and is used to convert the REST response to OData

  response using String APIs. User can manipulate the response data here.

*/

def Message processResponseData(message) {

 

     convertPayload(message);

 

     return message;

}

 

 

/*

* ***************************

*  RESPONSE HANDLING

* ***************************

*/

 

def void convertPayload(Message message){

 

     String payload = getResponsePayload(message);

 

     //parse the payload and transform into Document

     Document document = parsePayload(payload);

 

     // now do the refactoring: throw away useless nodes from backend payload

     document = refactorDocument(document);

     String structuredXmlString = transformToString(document, "UTF-8");

 

     if(structuredXmlString == null){

          log.logErrors(LogMessage.TechnicalError, "Conversion failed");

          //TODO proper error handling

          return;

     }

 

     // finally

     message.setBody(structuredXmlString);

     message.setHeader("Content-Type", new String("xml"));

}

 

 

 

def String getResponsePayload(Message message){

     String payload = message.getBody().toString();

 

     //TODO do some checks on the payload here

 

     return payload;

}

 

 

/**

* Parse the response body into a Document

* */

def Document parsePayload(String payload) {

 

     if(payload == null || payload.length() < 1){

          log.logErrors(LogMessage.TechnicalError, "Cannot parse empty payload.");

                  return null;

     }

 

     InputStream inputStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));

     InputSource inputSource = new InputSource(inputStream);

 

     DocumentBuilder parser;

     try {

          parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();

  

          return parser.parse(inputSource);

     } catch (ParserConfigurationException e) {

          log.logErrors(LogMessage.TechnicalError, "Parser creation failed");

                  return null;

     } catch (SAXException e) {

          log.logErrors(LogMessage.TechnicalError, "Response parsing failed");

                  return null;

     }

}

 

def Document refactorDocument(Document document){

 

     if(document == null){

          log.logErrors(LogMessage.TechnicalError, "Failed to load document");

                  return;

     }

 

     //find nodes

     Node resultsElement = document.getFirstChild();

     NodeList childrenOfResults = resultsElement.getChildNodes();

 

     // rename the root node: <results xmlns="http://gisgraphy.com">

     document.renameNode(resultsElement, resultsElement.getNamespaceURI(), "StreetSet");

 

     // remove xmlns-attribute from root node: example: <results xmlns="http://gisgraphy.com">

     NamedNodeMap attributesMap = resultsElement.getAttributes();

     if(attributesMap.getNamedItem("xmlns") != null){

          try {

               attributesMap.removeNamedItem("xmlns");

          } catch (DOMException e) {

               log.logErrors(LogMessage.TechnicalError, "Failed to remove attrib.");

          }

     }

 

     // nodes to delete:

     Node numNode;

     Node qtNode;

 

     // rename all nodes of the REST service:   <result>...

     for(int i = 0; i < childrenOfResults.getLength(); i++){

          Node childNode = childrenOfResults.item(i);

          String nodeName = childNode.getNodeName();

  

          if(nodeName.equals("numFound")){ // store for later deletion

               numNode = childNode;

          } else if(nodeName.equals("QTime")){ // store for later deletion

               qtNode = childNode;

          } else{ // rename this node

               document.renameNode(childNode, childNode.getNamespaceURI(), "Street");

          }

     }

 

     // delete undesired nodes <numFound>50</numFound> and <QTime>..</QTime>

        if(numNode != null){

          resultsElement.removeChild(numNode);

     }

     if(qtNode != null){

          resultsElement.removeChild(qtNode)     

     }

 

     // done with refactoring

     return document;

}

 

 

/**

* Transforms the specified document into a String representation.

* Removes the xml-declaration (<?xml version="1.0" ?>)

* @param encoding should be UTF-8 in most cases

*/

def String transformToString(Document document, String encoding) {

 

     if (document == null) {

          log.logErrors(LogMessage.TechnicalError, "Document is null.");

                  return null;

     }

 

     // create the transformer

     TransformerFactory transformerFactory = TransformerFactory.newInstance();

     Transformer transformer = null;

     try {

          transformer = transformerFactory.newTransformer();

     } catch (TransformerConfigurationException e) {            

          log.logErrors(LogMessage.TechnicalError, "Transformer creation failed.");

                  return null;

     }

 

     // configure the transformer

     transformer.setOutputProperty(OutputKeys.METHOD, "xml");

     transformer.setOutputProperty(OutputKeys.INDENT, "yes");

     transformer.setOutputProperty(OutputKeys.ENCODING, encoding);

     transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");

     transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");

 

     // prepare the output

     OutputStream outputStream = new ByteArrayOutputStream();

     OutputStreamWriter outputStreamWriter = null;

     try {

          outputStreamWriter = new OutputStreamWriter(outputStream, encoding);

     } catch (UnsupportedEncodingException e) {

          log.logErrors(LogMessage.TechnicalError, "OutputStreamWriter creation failed");

                  return null;

     }

 

     // Finally do the transformation

     BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);

     try {

          transformer.transform(new DOMSource(document), new StreamResult(bufferedWriter));

     } catch (TransformerException e) {

          log.logErrors(LogMessage.TechnicalError, "Transformation failed.");

                  return null;

     }

 

     return outputStream.toString();

}

 

 

 

Further Reading

 

 

 

Preparing Eclipse for Groovy scripting:

http://scn.sap.com/docs/DOC-61719

 

Introduction in REST datasource part 1: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/10/understanding-rest-data-source-in-integration-gateway-1-query-very-simplified

 

Introduction in REST datasource part 2: Understanding the return structure in json

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/11/integration-gateway-understanding-rest-data-source-2-query--json-very-simplified

 

Introduction in REST datasource part 3: converting xml payload of a REST service for usage in Integration Gateway

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/12/integration-gateway-understanding-rest-data-source-3-query--xml-standard

 

Introduction in REST datasource part 4: implementing filter in custom script for Integration Gateway

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/17/integration-gateway-understanding-rest-data-source-4-filtering

 

Installing SMP toolkit:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/08/22/how-to-install-sap-mobile-platform-tools-for-integration-gateway-in-eclipse-kepler

 

Tutorial for Integration Gateway:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/06/10/creating-an-odata-service-based-on-sap-gateway-soap-jdbc-and-jpa-data-sources-ba

 

Tutorial about scripting in Integration Gateway:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/10/17/integration-gateway-using-custom-scripting-to-map-entity-sets-to-data-sources

This  blog is (once more) about Integration Gateway in SAP Mobile Platform 3.0.

It is meant for those of you who have already created OData services based on Integration Gateway in SAP Mobile Platform 3.0 (SMP).

If you want your OData service to support filtering, you have to implement it in the custom script.

This tutorial explains how it is done using the ExpressionVisitor interface, which is provided by Olingo.

By the way, this tutorial is based on SMP SP05.

 

We’re not going to start from scratch. So in order to follow this tutorial, you need to have the project in place that is created in my previous tutorial.

 

 

 

Prerequisites

 

 

 

 

Preparation

 

The following steps were done in the previous tutorial:

 

  • Create implementation project
  • Define OData model
  • Generate custom code for Groovy
  • implement the processResponseData() method
  • Deploy it to SMP
  • Create/configure the destination
  • Test the query operation.

 

 

 

Filter Implementation

 

Overview:


  • Retrieve the Objects UriInfo and FilterExpression.
  • Create Visitor class that implements the ExpressionVisitor interface
  • Implement the 4 required methods
  • Invoke the visitor
  • Use the result to manipulate the relative URI

 

 

Step: Callback  method processRequestData()

 

Implementing the callback method processRequestData() that is provided by the Integration Gateway Framework and which is generated into our custom script.

 

We have to understand:

This method is called, before the REST service (= the data source) is invoked.

 

The order is:

The user calls our OData service in SMP. E.g. https://<host>/gateway/odata/SAP/<SERVICE>/StreetSet?$filter=streetType eq ’PEDESTRIAN’

The OData call is received by Integration Gateway.

Integration Gateway requires our help, therefore it passes the OData-URI (which was called by the user) into our custom script, in the processRequestData() method

Here, we have to change the URI, because the filter statement in OData language is not understood by the REST service.

After changing the URI, we give it back to Integration Gateway.

Then Integration Gateway can safely call the REST service and will get the filtered data.

 

At this point we need to check the documentation of our REST service:

http://www.gisgraphy.com/documentation/user-guide.htm#streetwebservice

 

For this tutorial, I’ve chosen the streettype for filtering.

Example filter:

&streettype=PEDESTRIAN

 

The full URL:

http://services.gisgraphy.com/street/streetsearch?lat=52.52&lng=13.41&streettype=PEDESTRIAN

 

Note:

Please ignore that this URL actually contains 3 filters.

The background is that when we've created the Eclipse project, we’ve configured following URL

http://services.gisgraphy.com/street/streetsearch?lat=52.52&lng=13.41 as REST service-URL to provide data.

So, since the first 2 filters belong to the base URL (as we've defined it), we have to ignore these first 2 filters in our tutorial

So we have the base URL:

http://services.gisgraphy.com/street/streetsearch?lat=52.52&lng=13.41

and we add one filter to it:

&streettype=PEDESTRIAN

 

 

Step: Object UriInfo

 

The Olingo API provides the interface UriInfo

 

We need it, because this object contains information about the URI, which includes system query options.

The part of the URI, in which we’re interested, is the filter statement.

 

 

Step: Object FilterExpression

 

The Olingo API provides the interface FilterExpression

We need it in order to parse the OData filter statement.

We obtain the filter statement by asking the UriInfo for the FilterExpression:

 

 

       FilterExpression filterExpression = uriInfo.getFilter();

 

 

The filterExpression object contains the actual filter that is specified in the URL of the invoked OData service on SMP.

This is the $filter parameter, as specified in OData.

 

Example:

 

If the full URL of the OData service invocation is as follows:

 

https://<SMPserver>/gateway/odata/SAP/<YOUR_SERVICE>/StreetSet?$filter=streetType eq ’PEDESTRIAN’

 

then the filterExpression object contains the information about

 

filter=streetType eq ’PEDESTRIAN’

 

If the full URL of the OData service invocation is as follows (just as an example):

 

https://<server>/gateway/odata/SAP/<YOUR_SERVICE>/StreetSet?$format=json

 

then the filterExpression object is null

So we have to explicitly handle the case that no filter is desired:

 

     if (filterExpression == null){

          return;

     }

 

The filterExpression object is required to invoke the ExpressionVisitor.

 

 

 

Step: Interface ExpressionVisitor

 

The Olingo API provides the interface ExpressionVisitor

 

This interface has to be implemented by us.

Here it is where the filter implementation is realized.

The ExpressionVisitor is required, because a filter statement can be complex.

The Olingo framework traverses all the filter statements in the OData service call and passes the segments to us in the different methods of the interface.

 

In our script, we create an inner class that implement the ExpressionVisitor interface:

 

 

     def class RestExpressionVisitor implements ExpressionVisitor{

 

 

We let Eclipse add the methods which have to be implemented.

Most of them can be ignored.

 

In following steps, we'll have a closer look at the relevant methods.

 

visitProperty()


visitLiteral()


visitBinary()


visitFilterExpression()

 

 

They are invoked in this order.

 

 

Example:

 

What happens, if our OData service is invoked with $filter=streetType eq ’PEDESTRIAN’

 

The methods are invoked in the following order:

 

visitProperty()Here we have to deal with the property, e.g.streetType
visitLiteral()

Here we have to deal with the literal, e.g.

‘PEDESTRIAN’
visitBinary()

Here we have to deal with the operator, e.g.

eq
visitFilterExpression()Here we have to deal with our filterExpression, e.g.streetType eq ‘PEDESTRIAN’

 

 

Note:

These sample values on the right column depend on the implementation.

 

 

 

Second example:

What happens, if our OData service is invoked with $filter=streetType eq ’PEDESTRIAN’ and name eq ‘East Lake Street’

 

 

 

visitProperty()Deal with the property, e.g.streetType
visitLiteral()

Deal with the literal, e.g.

‘PEDESTRIAN’
visitBinary()

Deal with the operator, e.g.

eq
visitProperty()Deal with the second property, e.g.name
visitLiteral()Deal with the second literal, e.g.‘East Lake Street’
visitBinary()Deal with the operator, e.g.eq
visitBinary()Deal with the operator, e.g.and
visitFilterExpression()Deal with our filterExpression, e.g.streetType eq ‘PEDESTRIAN’ and name eq ‘East Lake Street’

 

 

Note:

Again, these sample values depend on the implementation

 

We can see that the visitor traverses all segments and finally calls for the final expression

 

 

Note that In this tutorial we cover only simple filter statement.

 

Now let’s implement the methods for our example

 

 

 

Step: Method visitProperty()

 

The method signature:

 

Object visitProperty(PropertyExpression propertyExpression, String uriLiteral, EdmTyped edmTyped)

 

In this method, we get the possibility to analyze the property mentioned in the filter expression.

In our example:

 

$filter=streetType eq ’PEDESTRIAN’

 

Here, the text streetType is the name of a property (in the OData model).

 

When implementing the visitProperty() method, we can access the property as an object as follows:

 

             EdmProperty edmProperty = (EdmProperty) edmTyped;

 

And the name of the property:

 

             String propertyName = edmProperty.getName();

 

So what?

In our REST example service we really need this method.

Why?

Because the parameter, that is supported by the REST service is: streettype

(see docu)

but the name of the property (in both the OData model and the REST service) is: streetType

 

See it? They differ.

Without converting the OData property name to the REST parameter name, the filtering doesn't work.

 

But we can make use of the visitProperty() method in order to do a mapping from property name to parameter name

 

            String restParameterName = null;

            if(propertyName.equals("streetType")){ // we get the OData property name

             restParameterName = "streettype"; // we convert it to REST parameter name     

       }else{

             restParameterName = propertyName;

       }

 

 

Our method has to return the name of the parameter that is used by the REST service.

See the sample implementation of the method here:

 

@Override

     public Object visitProperty(PropertyExpression propertyExpression, String uriLiteral, EdmTyped edmTyped) {

 

     if(! (edmTyped instanceof EdmProperty)){

          throw new RuntimeException("Unexpected type");

     }

    

     EdmProperty edmProperty = (EdmProperty) edmTyped;

    

     String propertyName = null;

     try {

          propertyName = edmProperty.getName();

     } catch (EdmException e) {

          ((ILogger)log).logErrors(LogMessage.TechnicalError, "Failed to read property");

          throw new RuntimeException("Unable to access name of property", e);

     }

    

     String restParameterName = null;

     if(propertyName.equals("streetType")){ //in the REST service, the param is different

          restParameterName = "streettype";

     }else{

          restParameterName = propertyName;

     }

    

     return restParameterName; // e.g. will lead to streettype=PEDESTRIAN

}

 

 

 

Step: Method visitLiteral()

 

The method signature:

 

Object visitLiteral(LiteralExpression literalExpression, EdmLiteral edmLiteral)

 

In this method, we get 2 objects, but we focus on the second only:

 

EdmLiteral can be used for obtaining the literal, which is the pure text of the filter.

 

What do we have to do?

As we’ve seen above, the syntax of our REST service for filtering is:

 

&<parametername>=<value>

 

 

Note that the value doesn’t require inverted comma.

-> here we have the difference to OData

 

So in our example the OData segment  ’PEDESTRIAN’

 

Has to be converted to the REST segment PEDESTRIAN

(without inverted commas)

 

This is easy to implement: just remove the inverted commas.

And it is even easier to implement, because the following statement returns the value without inverted comma

 

edmLiteral.getLiteral()

 

So our implementation looks as follows:

 

 

     @Override

     public Object visitLiteral(LiteralExpression literalExpression, EdmLiteral edmLiteral) {

          return edmLiteral.getLiteral();

     }

 

 

 

Step: Method visitBinary()

 

The method signature:

 

Object visitBinary(BinaryExpression binaryExpression, BinaryOperator operator, Object leftSide, Object rightSide)

 

In this method, we have to convert the operator.

 

As we’ve seen above, the syntax of our REST service for filtering is:

 

&<parametername>=<value>

 

 

The operator is: =

-> this is the difference to OData

 

So in our example the OData operator: eq

 

Has to be converted to the REST operator: =

 

This has to be done for all supported operators, so we have to care about each operator in a switch statement.

Note that Olingo provides an enum for the OData operators:

 

org.apache.olingo.odata2.api.uri.expression.BinaryOperator

 

We get it as parameter in the method.

At the end, we have to return the whole expression.

 

In our tutorial, let’s support only 2 operators (it depends of course on the chosen REST service), to allow the above service invocation.

 

public Object visitBinary(BinaryExpression binaryExpression, BinaryOperator operator, Object leftSide, Object rightSide) {

     String operatorForREST = "";

                  

     switch(operator){

          case BinaryOperator.EQ:

               operatorForREST = "=";

               break;

          case BinaryOperator.AND:

               operatorForREST = "&";

               break;

          default:

               throw new RuntimeException("Not supported: " + operator.toUriLiteral());

     }

                  

     return leftSide + operatorForREST + rightSide;

}

 

 

Step: Method visitFilterExpression()

 

The method signature:

 

Object visitFilterExpression(FilterExpression filterExpression, String expressionString, Object expression)

 

In this method, we have the chance to check the final filter expression.

The parameter expression contains our REST filter expression, as we've defined it in the previous visitor-methods.

E.g. streettype=PEDESTRIAN

 

What are we going to do here?

In a simple example, we would simply return the instance of expression.

Our REST sample service is a bit special, because our so-called base-URI already contains some parameters. As explained above, we use it simply to get some data. The fact that the REST service needs 2 parameters for geolocation, doesn’t disturb us.

It only forces us to do a little work in the visitFilterExpresission() method.

The reason is:

Our expression looks fine: streettype=PEDESTRIAN

But it has to be appended to the base URL, which already contains parameters:

 

http://services.gisgraphy.com/street/streetsearch?lat=52.52&lng=13.41

 

So before appending our expression, we have to insert a &

 

That’s all.

 

Our sample implementation:

 

public Object visitFilterExpression(FilterExpression filterExpression, String expressionString, Object expression) {

    

     if(expression instanceof String){

          String expressionAsString = (String)expression;

          if(! expressionAsString.startsWith("&")){

               expressionAsString = "&" + expressionAsString;

               return expressionAsString;

          }

     }


     return expression;

}

 

 

 

Step: Use ExpressionVisitor

 

In the above steps, we've created the RestExpressionVisitor class, that implements the required interface and the necessary methods.

Now we can instantiate and invoke it:

 

       RestExpressionVisitor visitor = new RestExpressionVisitor();

    Object restFilterObject = null;

       try {

          restFilterObject = filterExpression.accept(visitor);

    } catch (ExceptionVisitExpression e) {

          log.logErrors(LogMessage.TechnicalError, "Failed calling accept()");

                 return;

    } catch(ODataApplicationException oe){

          log.logErrors(LogMessage.TechnicalError, "Failed calling accept()");

                 return;

    }

 

 

The returned restFilterObject is the result of our effort: the REST-izised version of our OData-filter.

In our sample implementation, the resulting value is the following string:

 

&streettype=PEDESTRIAN&name=East Lake Street

 

What to do with it?

Remember that we’re implementing the callback method processRequestData(), where we’re supposed to manipulate the backend-URL, before it is called.

So we have to build the URI containing the filter string.

 

Usually, it is formed as follows:

 

<relativeURI> + ? + <filterString>

 

In our special case, the relativeURI already contains the ?

So we don’t need to add it.

We get the relativeURI string from the header, which is passed in the method signature in the message object.

In our example, the relative URI woud be:

 

/street/streetsearch?lat=41.89&lng=-87.64

 

The code looks as follows:

 

 

       String relativeUri = getRelativeUri(message);

       relativeUri = relativeUri + (String)restFilterObject;

 

 

and the code to obtain the relativeURI:

 

 

def String getRelativeUri(Message message){

     Map headers = message.getHeaders();

     Object object = headers.get("RelativeUri");

     if(object instanceof String){

          return (String)object;

     }

 

     return null;

}

 

 

 

The last step to do in the processRequestData() method is: to pass the new relativeUri to the Integration Gateway framework.

This is done by setting the header:

 

 

def void setRelativeUri(Message message, String newUri){

     message.setHeader("RelativeUri", newUri);

}

 

 

 

Final: Run

 

That’s it.

 

At least, that’s it for the filter implementation in the method processRequestData().

Which is the focus of this blog.

We haven’t talked about the processResponseData(), as this was the focus of my previous blog

Note that you also need to add the required import statements and also to add required bundles.

 

 

I’m pasting the full script below, and I'm also attaching the relevant files of my Eclipse project.

Note that the attached files are based on SP05. So if you use them for a different version, you have to adapt them, e.g. update the dependencies in the manifest file.

 

Please refer to my previous blogs for detailed information (see Links section).

 

 

 

Testing the filtering

 

After generate and deploy the designtime artifact and after configuring the service on SMP, you can try calling several URLs with different filters:

 

First test without filter:

https://<host>:8083/gateway/odata/SAP/<YOUR_SERVICE>;v=1/StreetSet

 

Try different streetType filters:

https://<host>:8083/gateway/odata/SAP/<YOUR_SERVICE>;v=1/StreetSet?$filter=streetType eq ’PEDESTRIAN’

https://<host>:8083/gateway/odata/SAP/<YOUR_SERVICE>;v=1/StreetSet?$filter=streetType eq ‘FOOTWAY’

 

And also name filters:

https://<host>:8083/gateway/odata/SAP/<YOUR_SERVICE>;v=1/StreetSet?$filter=name eq ‘East Lake Street’

https://<host>:8083/gateway/odata/SAP/<YOUR_SERVICE>;v=1/StreetSet?$filter=name eq ‘North Canal Street’

 

Try combined filter

https://<host>:8083/gateway/odata/SAP/<YOUR_SERVICE>;v=1/StreetSet?$filter=streetType eq ‘PEDESTRIAN’ and name eq ‘East Lake Street’

 

 

There should be different and meaningful response to these calls.

 

 

Full source code

 

 

import org.apache.olingo.odata2.api.edm.EdmException
import org.apache.olingo.odata2.api.edm.EdmLiteral
import org.apache.olingo.odata2.api.edm.EdmProperty
import org.apache.olingo.odata2.api.edm.EdmTyped
import org.apache.olingo.odata2.api.exception.ODataApplicationException;
import org.apache.olingo.odata2.api.uri.UriInfo
import org.apache.olingo.odata2.api.uri.expression.BinaryExpression
import org.apache.olingo.odata2.api.uri.expression.BinaryOperator
import org.apache.olingo.odata2.api.uri.expression.ExceptionVisitExpression;
import org.apache.olingo.odata2.api.uri.expression.ExpressionVisitor
import org.apache.olingo.odata2.api.uri.expression.FilterExpression
import org.apache.olingo.odata2.api.uri.expression.LiteralExpression
import org.apache.olingo.odata2.api.uri.expression.MemberExpression
import org.apache.olingo.odata2.api.uri.expression.MethodExpression
import org.apache.olingo.odata2.api.uri.expression.MethodOperator
import org.apache.olingo.odata2.api.uri.expression.OrderByExpression
import org.apache.olingo.odata2.api.uri.expression.OrderExpression
import org.apache.olingo.odata2.api.uri.expression.PropertyExpression
import org.apache.olingo.odata2.api.uri.expression.SortOrder
import org.apache.olingo.odata2.api.uri.expression.UnaryExpression
import org.apache.olingo.odata2.api.uri.expression.UnaryOperator
import com.sap.gateway.core.ip.component.commons.ODataExchangeHeaderProperty
import com.sap.gateway.ip.core.customdev.logging.ILogger;
import com.sap.gateway.ip.core.customdev.logging.LogMessage;
import com.sap.gateway.ip.core.customdev.util.Message
/*
* ***************************
*  FRAMEWORK CALLBACK METHODS
* ***************************
*/
/**
  Function processRequestData will be called before the request data is
  handed over to the REST service and can be used for providing
  filter capabilities. User can manipulate the request data here.
*/
def Message processRequestData(message) {
    handleFilter(message);
    return message;
}
/**
  Function processResponseData will be called after the response data is received
  from the REST service and is used to convert the REST response to OData
  response using String APIs. User can manipulate the response data here.
*/
def Message processResponseData(message) {
    convertResponse(message);
    return message;
}
/*
* ***************************
*  RESPONSE HANDLING
* ***************************
*/
def void convertResponse(Message message){
    String restResponse = message.getBody().toString();
    /* CONVERT PAYLOAD */
    int index = restResponse.indexOf("<result>");
    if(index == -1){ // in case of empty payload
        restResponse = "<StreetSet><Street><gid></gid></Street></StreetSet>"; // currently the only possibility for returning nothing
    }else{
        restResponse = restResponse.substring(index);
        restResponse = restResponse.replaceAll("<result>", "<Street>");
        restResponse = restResponse.replaceAll("</result>", "</Street>");
        restResponse = restResponse.replaceAll("</results>", "</StreetSet>");
        restResponse = "<StreetSet>" + restResponse;
    }
    message.setBody(restResponse);
    message.setHeader("Content-Type", new String("xml"));
}
/*
* ***************************
*  REQUEST HANDLING
* ***************************
*/
/**
*     supporting $filter for one property only
* */
def void handleFilter(Message message){
    //retrieve the FilterExpression
    UriInfo uriInfo = getUriInfo(message);
    FilterExpression filterExpression = uriInfo.getFilter();
    if (filterExpression == null){
        // this is the case if the service is invoked without $filter
        return;
    }
    // Invoke the ExpressionVisitor
    RestExpressionVisitor visitor = new RestExpressionVisitor();
    Object restFilterObject = null;
    try {
        restFilterObject = filterExpression.accept(visitor);
    } catch (ExceptionVisitExpression e) {
        log.logErrors(LogMessage.TechnicalError, "Error while calling accept() on custom ExpressionVisitor");
        return;
    } catch(ODataApplicationException oe){
        log.logErrors(LogMessage.TechnicalError, "Error while calling accept() on custom ExpressionVisitor");
        return;
    }
    if(restFilterObject == null){
        log.logErrors(LogMessage.TechnicalError, "Couldn't retrieve the converted filter expression for the backend REST service.");
        return ;
    }
    // add the filter statement (converted to REST-format)
    String relativeUri = getRelativeUri(message); // the REST service URL without hostname. In our example it is: /street/streetsearch?lat=52.52&lng=13.41
    relativeUri = relativeUri + (String)restFilterObject; // we know that it is a String, because we've implemented it in the Visitor below
    // encode
    relativeUri = relativeUri.replaceAll(" ", "%20").replaceAll("'","%27");
    // Finally, set the newly composed URI
    setRelativeUri(message, relativeUri);
}
/**
* Implementation of ExpressionVisitor
*
* Supports combination of 2 filters
* e.g.
 * https://<host>/gateway/odata/SAP/<YOUR_SERVICE>/StreetSet?$filter=streetType eq ‘PEDESTRIAN’ and name eq ‘East Lake Street’
*
* */
def class RestExpressionVisitor implements ExpressionVisitor{
    /*
     * This implementation converts the OData-like operators (eq)
     * to operators in the format, that's understood by the REST service
     * */
    @Override
    public Object visitBinary(BinaryExpression binaryExpression, BinaryOperator operator, Object leftSide, Object rightSide) {
        String operatorForREST = "";
           
        switch(operator){
            case BinaryOperator.EQ:
                operatorForREST = "=";
                break;
            case BinaryOperator.AND:
                operatorForREST = "&";
                break;
                default:
                    throw new RuntimeException("This filter operator is not supported: " + operator.toUriLiteral());
            }
           
        return leftSide + operatorForREST + rightSide;
    }
   
    @Override
    public Object visitFilterExpression(FilterExpression filterExpression, String expressionString, Object expression) {
   
        // we need to add & because the filter is not the first URL parameter
        if(expression instanceof String){
            String expressionAsString = (String)expression;
            if(    ! expressionAsString.startsWith("&")){
                expressionAsString = "&" + expressionAsString;
                return expressionAsString;
            }
        }
    }
    @Override
    public Object visitLiteral(LiteralExpression literalExpression, EdmLiteral edmLiteral) {
        return edmLiteral.getLiteral();
    }
    @Override
    public Object visitMember(MemberExpression arg0, Object arg1, Object arg2) {
        return null;
    }
    @Override
    public Object visitMethod(MethodExpression arg0, MethodOperator arg1, List<Object> arg2) {
        return null;
    }
    @Override
    public Object visitOrder(OrderExpression arg0, Object arg1, SortOrder arg2) {
        return null;
    }
    @Override
    public Object visitOrderByExpression(OrderByExpression arg0, String arg1,List<Object> arg2) {
        return null;
    }
    /*
     * We need to do a mapping from OData-property-name to REST-parameter-name
     * */
    @Override
    public Object visitProperty(PropertyExpression propertyExpression, String uriLiteral, EdmTyped edmTyped) {
        if(! (edmTyped instanceof EdmProperty)){
            throw new RuntimeException("Unexpected type");
        }
   
        EdmProperty edmProperty = (EdmProperty) edmTyped;
        String propertyName = null;
        try {
            propertyName = edmProperty.getName();
        } catch (EdmException e) {
            ((ILogger)log).logErrors(LogMessage.TechnicalError, "EdmException occurred while accessing filter statment property");
            throw new RuntimeException("Unable to access name of property", e);
        }
   
        String restParameterName = null;
        if(propertyName.equals("streetType")){ // in the REST service, the filter parameter is spelled differently than the property name
            restParameterName = "streettype";
        }else{
            restParameterName = propertyName;
        }
   
        return restParameterName; // in our example, this will at the end lead to the expression streettype=PEDESTRIAN
    }
    @Override
    public Object visitUnary(UnaryExpression arg0, UnaryOperator arg1,Object arg2) {
        return null;
    }
}
/*
* ***************************
*  HELPER
* ***************************
*/
def String getRelativeUri(Message message){
    Map headers = message.getHeaders();
    Object object = headers.get("RelativeUri");
    if(object instanceof String){
        return (String)object;
    }
        return null;
}
def void setRelativeUri(Message message, String newUri){
    message.setHeader("RelativeUri", newUri);
}
def UriInfo getUriInfo(Message message){
    Map headers = message.getHeaders();
    Object object = headers.get(ODataExchangeHeaderProperty.UriInfo.toString()); // need dependency to bundle com.sap.gw.rt.ip.commons.camel-commons
    if(object instanceof UriInfo){
        return (UriInfo)object;
    }
    return null;
}




 

 

 

Further Reading

 

Preparing Eclipse for Groovy scripting:

http://scn.sap.com/docs/DOC-61719

 

Introduction in REST datasource part 1: Understanding the return structure in xml

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/10/understanding-rest-data-source-in-integration-gateway-1-query-very-simplified

 

Introduction in REST datasource part 2: Understanding the return structure in json

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/11/integration-gateway-understanding-rest-data-source-2-query--json-very-simplified

 

Introduction in REST datasource part 3: converting xml payload of a REST service for usage in Integration Gateway

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/02/12/integration-gateway-understanding-rest-data-source-3-query--xml-standard

 

Installing SMP toolkit:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/08/22/how-to-install-sap-mobile-platform-tools-for-integration-gateway-in-eclipse-kepler

 

Tutorial for Integration Gateway:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/06/10/creating-an-odata-service-based-on-sap-gateway-soap-jdbc-and-jpa-data-sources-ba

 

Tutorial about scripting in Integration Gateway:

http://scn.sap.com/community/developer-center/mobility-platform/blog/2014/10/17/integration-gateway-using-custom-scripting-to-map-entity-sets-to-data-sources

Hi there,

In my last blog I promised to write a new one about how to use “mobile services”. I first thought about writing about our Offline OData innovation, but Midhun was much faster than me. So I take the topic of push notifications.

Notifications are adding real time capabilities to mobile applications. They help to speed up things. Typically it is triggered by a business event occurring in a back-end, so one of the most important parts for enabling real-time in mobile scenarios is to carefully think about the implementation of triggering events from the backend. Which business events are important, who are the recipients, what information should be sent (basically none, it should be a poke/pull implantation). If you have a clear understanding of those questions and hopefully some answers you can think about implementing push notifications.

Since all those questions are business related and I can’t answer them here, I'll focus to the technical implementation, which is always the same.

So for this we mock the business event just by using a rest client. That’s the easy part and I will later explain how to do that. The middle layer is obviously SAP HANA Cloud Platform mobile services and for this blog I choose Android to demonstrate the receiver part and I’ll use the Hybrid SDK (Cordova plugins provided by SMP SDK). Basically I followed this great guide (Getting Started with Kapsel - Part 4 -- Push) by Daniel Van Leeuwen, which covers most of the client related steps. I strongly encourage you to do the same if you did not yet. So stop reading here and create a hybrid, Android, mobile app, which is push enabled. Come back to this blog if you are ready.

 

Hi and welcome back.

I think you have recognised that Daniel used the SMP Admin UI to set the server sided configuration of Push. Instead of doing it for SMP, I will now show you how to do the same in the Admin Cockpit of “mobile services”.

 

Open the Admin Cockpit, and select the “Applications” tile.

pic1.png

As you can see I have already created an Application Configuration that points to a public OData service. The AppID (com.sap.hcpms.push.demo) can be named as you like. Here on this screen you can see a PushURL. This URL is the “old” API that supports SAP Gateway subscription based notifications. This kind of notifications is suitable for scenarios where your backend keeps track of the registered devices. Today we will concentrate on the enhanced and more flexible Push API. So right now, just ignore this URL.

At this point you need to either create a new Application Configuration or “Configure” an existing one.

Navigate to the “Push” tab (first click “Configure).

As you can see, you con configure various native push services here. HANA Cloud Platform mobile services supports native Android (GCM), iOS notifications (APNS), Blackberry, Windows Phone and Windows Desktop (Metrrrr…, sorry Windows Store Apps).

Since we want to use Android today, we need to provide an API Key and a Sender ID. In order to get those you need to follow the instructions here:

http://developer.android.com/google/gcm/gs.html

Your Sender ID is your Project Number – a simple numerical value like 123456789123.

The API key is a bit longer, it is also described how to get this under the link above.

We know put the values, generated by Google into the Admin Cockpit and store the values. That’s it. It’s done, really. This is how it should look like:

pic2.png

Save your changes.

 

One more thing:

Not everybody is allowed to send notification to the server. Even the Administrator can do this. You need a user with a special role in order to issue notifications. Typically a technical user that is known to the backend is granted the so called "Notification User" role.

In order to do this you need to navigate to the HCP account page.

Select "Services".

 

Notification_1.png

Now select the little "Configure Roles" icon on the far right hand side.

Now select the "Notification User" in the upper part of the screen.

Click on "Assign..." and put your user ID into the dialog.

Then confirm the dialog.

The screen should look like the following:

 

Notification_2.png

You need to repeat these steps for any user you want to grant access to the notification API.

 

We can now start our client application and register against HANA Cloud Platform mobile services.

Once we are registered we need to register with Google to get a “registration_id”, this is handled by the push plugin in the client (it uses the native push libraries by Google to do so). This registration id needs to be sent to the mobile services in order to make HCPms aware of the particular device (we knew the device before, but not in the context of GCM). This is also done by the push plugin, so you don’t have to care about it.

In summary, “mobile services” knows how to act like a GCM server and it knows all about the clients. So you could say it behaves like a message broker.

Now we can mock the back-end event using POSTMAN (Google Chrome plugin) or any other REST client (I like the RESTClient from Wiztools as well – more control for cookies, curl for the hackers among you).

This is a valid back-end call:

 

POST /restnotification/application/com.sap.hcpms.push.demo/ HTTP/1.1
Host: hcpms-d055161trial.hanatrial.ondemand.com
Content-Type: application/json
Authorization: Basic bm90bXlwYXNzd29yZA==
Cache-Control: no-cache

{"alert":"New Vacation Approval Request", "data":"It works!!" }

 

 

Notice that we use /restnotifications instead of /Notification. The new REST API provides a lot more options – try it out.


tl;dr

1. Follow:Getting Started with Kapsel - Part 4 -- Push

2. Follow: Getting Started on Android | Android Developers

3. Create Application config in HCPms cockpit and provide Sender ID and API Key

4. Test you application using a rest client.

 

Have Fun,

Martin

Actions

Filter Blog

By author:
By date:
By tag: