1 2 3 21 Previous Next

SMP Developer Center

305 Posts

Hello SMP Administrators,

 

Let me start with an example. There are 3 users: A, B &C . SAP Mobile Platform administrator wants to grant admin role to user A, read-only access role (ie.Helpdesk) to user B and notification role to user C. He find this task very easy & simple. He logins to admin cockpit, adds two authentication providers System Login (Admin only) in admin security profile, provides Administrator and Helpdesk roles for A & B respectively. For User C, he adds a new System Login authentication provider but in Notification security profile. His job is done. User A got admin access to Admin cockpit, User B read access and User C can get notification via SMP.

     Untitled.png

Now, SMP Administrator has asked to grant admin roles to 3 more users, read-only access role to 10 more users and notification role to 20 more users.

But this time SMP administrator doesn't want to repeat what he has done before. He doesn't want to do it manually and wants to do it in a general way so that if in future, he gets any more requests for granting roles, he doesn't have to do much on SMP. He came to know that there is LDAP setup installed at organization. Could be like below:

 

     ldapstru.PNG

 

He thought of implementing LDAP/AD authentication provider in SMP security profile and mapping to the LDAP groups to which a user belongs.

 

    

 

Implementing LDAP/AD authentication provider in SMP Admin security profile:

 

  1. Login to Admin Cockpit > Settings > Security profiles > Admin (Cannot be deleted) > Edit
  2. Add a new authentication provider "Directory Serive (LDAP/AD)"

(below settings are as per above LDAP setup..there could be changes as per yours)

     ldap.png

 

Creating Users & groups in LDAP

 

1. I have added user a, b & c and created 3 different groups smp_admin_grp, smp_helpdesk_grp & smp_notification_grp.

2. Assigned a , b & c as uniqueMember in respective groups.

 

ldaprole.png

Modifying admin-role-mapping xml file

 

 

All predefined logical roles are there in SMP admin-role-mapping.xml file. Now we need to map newly created physical roles to predefined logical roles. Open admin-role-mapping.xml file can be found under C:\SAP\MobilePlatform3\Server\configuration\com.sap.mobile.platform.server.security\CSI .

 

Alert : Take a backup of same file before modifying it.

Map physical roles to predefined logical roles (as highlighted in bold)

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

<rm:Mappings xmlns:rm="http://www.sybase.com/csi/3.1/mapping">

    <DefaultMapping>

        <LogicalName>Administrator</LogicalName>

        <MappedName>Administrator</MappedName>

        <MappedName>smp_admin_grp</MappedName>

    </DefaultMapping>

 

       <!-- Avatar Deployer Role Mappings -->

       <DefaultMapping>

             <LogicalName>NodeManager.deploycontent</LogicalName>

             <MappedName>Administrator</MappedName>

       </DefaultMapping>

       <DefaultMapping>

             <LogicalName>GenerationAndBuild.generationandbuildcontent</LogicalName>

             <MappedName>Administrator</MappedName>

       </DefaultMapping>

 

       <DefaultMapping>

             <LogicalName>IntegrationOperationServer.read</LogicalName>

             <MappedName>Administrator</MappedName>

             </DefaultMapping>

    <DefaultMapping>

        <LogicalName>Developer</LogicalName>

        <MappedName>Developer</MappedName>

    </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Helpdesk</LogicalName>

        <MappedName>Helpdesk</MappedName>

        <MappedName>smp_helpdesk_grp</MappedName>

       </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Notification User</LogicalName>

        <MappedName>Notification User</MappedName>

    </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Impersonator</LogicalName>

        <MappedName>Impersonator</MappedName>

    </DefaultMapping>

</rm:Mappings>

 

 

Note: For Notification user role, you have to add a new authentication provider in Notification (cannot be deleted) security profile > Add> Directory service (LDAP/AD)

 

               ldapnotificaiton.png

 

     Once done, open Notification-role-mapping.xml file (C:\SAP\MobilePlatform3\Server\configuration\com.sap.mobile.platform.server.security\CSI) and map notification physical role to logical role as highlighted below.

  

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

<rm:Mappings xmlns:rm="http://www.sybase.com/csi/3.1/mapping">

    <DefaultMapping>

        <LogicalName>Administrator</LogicalName>

        <MappedName>Administrator</MappedName>

    </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Developer</LogicalName>

        <MappedName>Developer</MappedName>

    </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Helpdesk</LogicalName>

        <MappedName>Helpdesk</MappedName>

    </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Notification User</LogicalName>

        <MappedName>Notification User</MappedName>

        <MappedName>smp_notification_grp</MappedName>

    </DefaultMapping>

 

    <DefaultMapping>

        <LogicalName>Impersonator</LogicalName>

        <MappedName>Impersonator</MappedName>

    </DefaultMapping>

</rm:Mappings>

       

 

 

Few things to know

 

  1. By default, each logical role name is mapped to a physical role of the same name.

 

     <DefaultMapping>

     <LogicalName>Administrator</LogicalName>

     <MappedName>Administrator</MappedName>

     </DefaultMapping>

 

   2. By default, Admin security profile assigns smpAdmin user to the Administrator role. (Admin Cockpit credentials)

   3. As per documentation,

SMP includes <MappedName>Administrator</MappedName> in admin-role-mapping.xml, if you do not have a physical role/group called Administrator, delete this mapping from file to avoid unnecessary authorization checks and improve performance.

 

I agree with this point. But imagine if i remove this default mapping and if LDAP server is down, i will not even able to login with smpAdmin. In my opinion, let it be there, no need to remove.

4. You can configure security profiles in Management cockpit but role-mapping configuration has to be done manually by editing .xml file.

5. In SMP cluster, you can configure security profile from any active node, once you are done with changes (either in Management cockpit or editing .xml file or both), CSI pushes this changes to the shared database, which then propagates the changes to the cluster nodes.

6. Helpdesk role is usually granted to analyze root causes of issues/problems. Cannot perform any administrator related tasks.

7. There are also predefined Integration Gateway roles and these roles are mapped to Administrator logical role. But there is no read-only access role defined for Gateway management cockpit.

 

<!-- Avatar Deployer Role Mappings -->

       <DefaultMapping>

             <LogicalName>NodeManager.deploycontent</LogicalName>

           <MappedName>Administrator</MappedName>

    <MappedName>smp_admin_grp</MappedName>

       </DefaultMapping>

   

       <DefaultMapping>

             <LogicalName>GenerationAndBuild.generationandbuildcontent</LogicalName> in case of generate & deploy integration content to SMP

           <MappedName>Administrator</MappedName>

   <MappedName>smp_admin_grp</MappedName>

       </DefaultMapping>

 

  

<DefaultMapping>

             <LogicalName>IntegrationOperationServer.read</LogicalName> >>> needed in case of connecting to SMP server from eclipse kepler

          <MappedName>Administrator</MappedName>

   <MappedName>smp_admin_grp</MappedName>

</DefaultMapping>

 

8. The Developer role appears in the role-mapping.xml file, but is not implemented in SAP Mobile Platform.

 

I hope you find above information useful. Feel free to comment in case of any clarification and feedback.

 

Regards,

JK

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

The OData specification describes the concept of navigation, in order to easily navigate from one resource to another one.


REST services can also support a kind of “navigation”, where the result set of one resource depends on another resource.

If we use such a REST service as data source in our Integration Gateway project, we can add the navigation capability to our OData service.

 

In this tutorial, I’d like to show an example for an easy-to-implement bidirectional navigation.

This tutorial is based on SMP SP07.

 

 

 

Prerequisites

 

I expect that you've gone through my previous tutorials , explaining REST data source, QUERY  and READ  operation.

Please check the Links section for more info.

 

Furthermore, you need:

 

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

 

 

 

Preparation

 

 

REST Service

 

For this tutorial, we need a REST service with at least 2 resources that supports that kind of “navigation”.

I’m using a service that is not public, I hope this blog will be useful nevertheless.

I’ll post an update if that service becomes public some day in future…

 

Let’s have a look at how the backend-REST-service is used.

 

First, we invoke the URL for the “companies”:

 

https://<host>:<port>/rest/addressbook/companies

 

The result is a list of “companies”:

 

 

We can now choose one single “company”:

 

https://<host>:<port>/rest/addressbook/companies/2

 

The “company” is uniquely identified by the ID field, which is in the URL.

 

 

 

Now, this REST-service allows to add a segment to the URL, in order to retrieve the “contacts” that are responsible for this “company”:

 

https://<host>:<port>/rest/addressbook/companies/2/contacts

 

The result is a list of “contacts”, since there can be more than one “contact” for a “company”.

This is a one-to-many relationship.

In the details of a “contact”, we can see the ID of the “company” for which he is responsible.

 

 

 

After navigating from a “company” to its “contacts”, let’s now check the other way ‘round:

Open one single “contact”

 

https://<host>:<port>/rest/addressbook/contacts/46

 

 

 

Go to the “company” for which this “contact” is responsible:

 

https://<host>:<port>/rest/addressbook/contacts/46/company

 

Here we have a many-to-one relation, so the result is a single entry.

This company is the same that we’ve seen above.

 

 

 

 

Eclipse Project

 

 

We need a SAP Mobile Platform OData Implementation Project that already has the QUERY and READ operations implemented for both resources of the backend-REST-service.

And it should be up and running on our SMP server.

 

 

 

OData Model

 

The 2 EntityTypes are modeled according to the 2 resources of the backend REST service.

Additionally, we have an association between them.

Note that is has to be a bidirectional association, because we want to navigate from the "Company" to the "Contacts" and as well from a "Contact" to its "Company".


 

 

 

 

As we’ve seen above, a company can have many contacts, but a contact can only belong to one company.

So we specify a one-to-many relation.


 

 

 

 

And we create a Referential Constraint, which maps the property ID (of the Company) to the property COMPANY_ID (of the Contact)

 

 

 

 

 

That’s it for the modelling.

 

Next, we create the bindings and the Custom Code for QUERY and READ operations for both EntitySets.

Since this is explained in my previous blogs, we can skip all explanations about it.

 

 


Custom Code

 

 

After testing the QUERY and READ operations of our OData service at runtime, we can start taking care about the navigation.

 

Background

 

We invoke the URL for a company, e.g. https://localhost:8083/gateway/odata/<ns>/<service>/Companies('2')

In the response we can see that the Integration Gateway framework has generated a link element:

 

 

 

 

What happens, if we invoke that link?

https://localhost:8083/gateway/odata/<ns>/<service>/Companies('2')/ContactSet

 

Let’s try it.

 

 

Implementing the navigation for one-to-many relation

 

First we have to understand, that our script for the contact-QUERY is invoked.

The last segment of the URL is the ‘ContactSet’, which means that a collection of contacts is requested.

Therefore, when navigating from company to contacts, the contact-QUERY script is invoked (the default name Contacts_REST_Query.groovy)

Note that this is the case because we have a 1-to-many association between Company and Contact

 

First, the processRequestData() method is invoked.

And here we have to do the actual work.

The Integration Gateway framework doesn’t know how the backend REST service is designed.

Integration Gateway only knows that the user requested “give me the contacts which are relevant for the-company-with-ID-2”

But it doesn’t know how to get exactly this amount of data.

 

Therefore, we have to tell, how this is done, because we do know it: the URI has to look as follows:

companies/<valueOfTheID>/contacts

And we tell it in the processRequestData() method.

 

Summarizing:

Our task is to modify the backend-REST-service-URI that is called by Integration Gateway.



Implementing the processRequestData() method

 

We’re already familiar with such tasks.

We hock into the HTTP request, before it is fired agains the backend-REST-service.

In the processRequestData() method, we have to obtain the URI and modify it.

 

Note:

Our contact-QUERY script is invoked in both cases, with navigation and without navigation.

E.g.

https://localhost:8083/gateway/odata/<yourNamespace>/<yourService>/Contacts

https://localhost:8083/gateway/odata/<yourNamespace>/<yourService>/Companies('2')/ContactSet

 

Both URLs lead to our script being invoked.

So we have to consider it in our implementation.

 


Steps that have to be performed:

 

1) Obtain the UriInfo object

We need it in order to get the information about the navigation, which is in the URL.

 

     a) We need to know from where the navigation comes.

         In our example, we have only 2 EntityTypes, so the navigation starts always from a “Company”, but for other services it would be different.

         This is realized with the method UriInfo.getStartEntitySet()

 

     b) We need to know the ID of the “Company”, because depending on it, the amount of “Contacts” is different

        This information is contained in the UriInfo.getKeyPredicates()

 

2) Obtain the relative URI

We need it, in order to modify it.

 

This is how the URL that is invoked by the user of our OData service:

https://localhost:8083/gateway/odata/<yourNamespace>/<yourService>/Companies('2')/ContactSet

 

and this is how it has to look like when the backend-REST-service is called:

https://<host>:<port>/rest/address/companies/2/contacts

 

3) Set the relative URI

After correcting the relative URI, it has to be given back to Integration Gateway.

This is done by setting the header in the message object

 

And here’s the code snippet for our example.

 

 

def Message processRequestData(message) {

 

    UriInfo uriInfo = (UriInfo)message.getHeaders().get("UriInfo");

 

       // check if navigation is in place

    List<NavigationSegment> navSegments = uriInfo.getNavigationSegments();

       if(navSegments.size() < 1){

             // no navigation, just normal getEntitySet operation

             // this is the case for the call to

        //<...>localhost:8083/gateway/<...>/Contacts

             return message; // do nothing

    }else if (navSegments.size() > 1){

               /* this would be the case for e.g.

         * <...>/<srv>/Companies('2')/ContactSet('46')/Company/ContactSet

         * here the number of navSegments would be: 3

         * */

        log.logErrors(LogMessage.TechnicalError, "Not supported");

              return message; // ignore it for today

     }

 

 

       /* in our example, for the URL 

    * <...>localhost:8083/gateway/<...>/Companies('2')/ContactSet

    * the number of navSegments is: 1

    * */

       // handle the navigation

    String startEntitySetName = uriInfo.getStartEntitySet().getName();

 

       // we know that our EntityType has only one key field

    KeyPredicate keyPredicate = uriInfo.getKeyPredicates().get(0);

    String keyLiteral = keyPredicate.getLiteral();

 

       /* in our example, for the URL 

    * <...>localhost:8083/gateway/<...>/Companies('2')/ContactSet

    * the value of keyLiteral will be: 2

    * */

 

       // this is not really required for our service that has only 2 EntityTypes:

       if(startEntitySetName.equals("Companies")){ 

     

       String relativeUri = (String)message.getHeaders().get("RelativeUri");

             /* in our example, the value is: 

        * /rest/addressbook/contacts

       */

 

             // here we're doing the actual work: modify the URI

       String targetRESTuri = relativeUri.replace(

                                 "contacts", "companies/" + keyLiteral + "/contacts");

     

             // finally, set the manipulated REST-service-URI for the navigation

       ((Message)message).setHeader("RelativeUri", targetRESTuri);

            /* in our example, the final URI has to look like this:

       *  /rest/addressbook/companies/2/contacts

       */

    }

 

       return message;

}

 

 

 

Implementing the processResponseData() method

 

In terms of navigation, nothing has to be done here.

This method is already implemented for the QUERY operation, we don’t need to change or add anything.

 

 

 

Result

 

After deploy and configure the service, the navigation can be tried via a URL like this:

https://localhost:8083/gateway/odata/<yourNamespace>/<yourService>/Companies('2')/ContactSet

The result should be the same like in the backend:

https://<host>:<port>/rest/address/companies/2/contacts

 

 

Implementing the navigation for many-to-one relation

 

The first navigation example was for the 1-to-many relationship.

 

My sample backend-REST-service also supports to ask a single “contact” for the “company” that he is responsible for.

This is a many-to-1 relationship.

In our OData model, we’ve described this with a bidirectional association.

The OData modeler tool has generated a NavigationProperty called “Company” in the EntityType “Contact”.

Which means that we can invoke a URL like this:

https://localhost:8083/gateway/odata/<ns>/<service>/Contacts('46')/Company

 

Which corresponds to a backend-REST-service-URL like this:

https://<host>:<port>/rest/address/contacts/46/company

 

The implementation is very similar, the difference is:

Since here we have a many-to-1 relation, the script that is called is the Company-READ.

 

Again the response doesn’t need to be changed, it is just the READ implementation that is used to represent the target entry.

As for the request, the implementation is very similar like above

 

 

 

def Message processRequestData(message) {

 

  UriInfo uriInfo = (UriInfo)message.getHeaders().get("UriInfo");

   List<NavigationSegment> navSegments = uriInfo.getNavigationSegments();

   if(navSegments.size() == 1){

    String startEntitySetName = uriInfo.getStartEntitySet().getName();

    String keyLiteral  = uriInfo.getKeyPredicates().get(0).getLiteral()

     

       if(startEntitySetName.equals("Contacts")){

         // in our example: /rest/addressbook/companies/46

      String relativeUri = (String)message.getHeaders().get("RelativeUri");

            

         // modify the URI

         // in our example, we need: /rest/addressbook/contacts/46/company

     String targetRESTuri = relativeUri.replace("companies", "contacts") + "/company";

 

         // finally, set the manipulated REST-service-URI for the navigation

      ((Message)message).setHeader("RelativeUri", targetRESTuri);

    }

  }

 

  return message;

}






Summary

 

In this tutorial, we’ve learned how to realize navigation between 2 entities, based on REST data source.

It has been easy to realize, because the used backend-REST-service supported a kind of navigation that was easy to adapt to the OData way.

For other REST services it might be more tricky.

 

 

Links

 

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

 

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

 

Introduction in REST data source part 3: Implementing the QUERY operation

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 data source part 7: Implementing the READ operation

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

 

Overview of all REST blogs

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/08/integration-gateway-rest-data-source-overview-of-blogs

 

 

The official documentation: http://help.sap.com/mobile-platform/

You might have faced authentication errors many times while accessing an application id through SAP Mobile Platform and just wondered if there is some tool to troubleshoot the error apart from checking the security log. Yes, there is a way you can debug authentication errors against an authentication provider. Here, i will be using a tool called CSI (Common security infrastructure) tool.

 

Note: This guide is mainly for System Administrators and can be tested on same machine where SMP server is installed. I have tested it on Windows.

 

(I followed this guide but unfortunately it didn't work as per steps mentioned)

 

You can find 3 different security profiles (admin, default and Notification) present under SETTINGS > SECURITY PROFILES in Admin cockpit and for all these profiles you can see two different .xml file.

e.g. For Admin, admin.xml and admin-role-mapping.xml

      For default, default.xml and role-mapping.xml

      For Notification, Notification-role-mapping.xml, Notification-role-mapping.xml

 

Example 1:

 

Lets go with Admin profile first, we got a default username as smpAdmin & password as s3pAdmin (might be different in your case). Lets assume you are facing authentication error while accessing an application through the Admin profile. This CSI tool will help you in debugging the error:

 

Steps to be followed:

 

1. Create a temporary folder somewhere on server machine

2. copy below files into same folder

  • csi-tool.jar file from C:\SAP\MobilePlatform3\Server\tools\csi
  • csibootstrap.properties and csikeystore.jceks files from C:\SAP\MobilePlatform3\Server\configuration\com.sap.mobile.platform.server.security
  • admin.xml and admin-role-mapping.xml from C:\SAP\MobilePlatform3\Server\configuration\com.sap.mobile.platform.server.security\CSI

 

It should like this:

     1.PNG

 

4. Open admin.xml file, modified value for RoleMapFile as mentioned below

 

     2.PNG

 

5. Open a command prompt > Navigate to temporary folder and run this

 

java -Dcom.sybase.security.BootstrapConfigurationFile="C:\Users\Jitendra\Desktop\CSItest\csibootstrap.properties" -cp csi-tool.jar;C:\SAP\MobilePlatform3\Server\plugins\* -Djava.util.logging.config.file=logging.properties com.sybase.security.tools.CSILauncher csi.diag.authenticate --USERNAME "smpAdmin" --PASSWORD "s3pAdmin" --CONFIG_FILE C:\Users\Jitendra\Desktop\CSItest\admin.xml

You can a success message about true authentication.

3.PNG

Let me try passing wrong password

 

     4_error.PNG

 

You can try the same for other security profile(s) as well following steps 1-5

 

Example 2:

 

I have created a new security profile named as SAP_SSO2. You can see that there are 2 different xml files created for same profile: ie. SAP_SSO2.xml and SAP_SSO2-role-mapping.xml under C:\SAP\MobilePlatform3\Server\configuration\com.sap.mobile.platform.server.security\CSI

 

     ldap1.PNG

 

I have copied the same files into that temporary folder and modified the SAP_SSO2.xml file as per step 4.

 

5.PNG

 

java -Dcom.sybase.security.BootstrapConfigurationFile="C:\Users\Jitendra\Desktop\CSItest\csibootstrap.properties" -cp csi-tool.jar;C:\SAP\MobilePlatform3\Server\plugins\* -Djava.util.logging.config.file=logging.properties com.sybase.security.tools.CSILauncher csi.diag.authenticate --USERNAME "p1176845" --PASSWORD "******" --CONFIG_FILE C:\Users\Jitendra\Desktop\CSItest\SAP_SSO2.xml

 

(I have entered wrong password)

 

 

          sso2_incorrect.png

It is throwing '401 unauthorized' error as expected.

 

Note: This method is also quite valid for a security profile having more than one authentication providers.

 

Enable security logs for more troubleshooting. Set log level to DEBUG (Admin cockpit > Logs > Settings >Security)

 

Regards,

JK

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

It is a follow-up of the previous blog, where I explained how to implement the CREATE operation for your OData service, based on a REST data source.

Since SMP SP07, DELETE operation is supported by Integration Gateway.

 

If you’re new to the topic, check the Links section below where you can find the tutorials which get you up to speed.

 

 

 

Prerequisites

 

I expect that you've gone through my previous tutorials, explaining REST data source – QUERY and READ and CREATE operation – based on XML payload.

Please check the Links section for the relevant blogs.

 

Furthermore, you need:

 

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

 

 

 

Preparation

 

REST Service

 

For this tutorial, we need a REST service that supports writing scenario. I’m using a service that is not public, I hope this blog will be useful nevertheless.

I’ll post an update if that service becomes public some day in future…

 

 

 

OData Model

 

The OData model for this tutorial is the same like in the previous tutorial:

 

 

 

You can simply continue with the project that was created for the previous tutorial.

 

 

Bind data source

 

The Relative URI is the same like for the READ operation

 

/sap/rest/odata/address/companies/{ID}

 

 

Please check my previous tutorials for explanation about how to compose the Relative URI

 

 

 

 

 

 

 

 

Custom Code

 

Generate the Custom Code for Groovy.

 

For the DELETE operation, we don’t have to implement anything.

Why?

In the processRequestData() method, there’s no special requirement regarding request header or request body

In the processResponseData() method, no response body will be returned, according to OData specification.

 

 

Note:

If your backend requires additional handling, then of course you have to provide the implementation here

For example, my backend REST service requires the x-requested-with header to be set, while sending the request.

Therefore, I’m adding the following line to the processRequestData() method

 

 

 

def Message processRequestData(message) {

    message.setHeader("x-requested-with", "XMLHTTPRequest");

       return message;

}

 

 

 

Result

 

In order to test the DELETE  operation, proceed as described in the previous tutorial.

The response status code should be 204 No Content and the response body should be empty, as can be seen in the screenshot below.

 

 

 

Summary

 

In this tutorial, we’ve learned how to implement the DELETE operation in a Groovy script.

We’ve seen that it’s easy, as no implementation is required to fulfill the task

 

 

 

Links

 

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

 

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

 

Introduction in REST data source part 3: Implementing the QUERY operation

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 data source part 7: Implementing the READ operation

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 9: Implementing the CREATE operation

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/21/integration-gateway-understanding-rest-data-source-9-create-operation

 

Overview of all REST blogs

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/08/integration-gateway-rest-data-source-overview-of-blogs

 

The official documentation: http://help.sap.com/mobile-platform/

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

 

It is a follow-up of the previous blog, where I explained how to implement the CREATE operation for your OData service, based on a REST data source.

 

Since SMP SP07, the UPDATE operation is as well supported by Integration Gateway.

 

If you’re new to the topic, check the Links section below where you can find the tutorials which get you up to speed.

 

 

 

Prerequisites

 

I expect that you've gone through my previous tutorials, explaining REST data source – QUERY and READ and CREATE operation – based on XML payload.

Please check the Links section for the relevant blogs.

 

 

Furthermore, you need:

 

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

 

 

Preparation

 

REST Service

 

For this tutorial, we need a REST service that supports writing scenario. I’m using a service that is not public, I hope this blog will be useful nevertheless.

I’ll post an update if that service becomes public some day in future…

 

 

OData Model

 

The OData model for this tutorial is the same like in the previous tutorial:

 

 

 

 

 

 

 

You can simply continue with the project that was created for the previous tutorial.

 

Bind data source

 

The Relative URI is the same like for the READ operation

 

/sap/rest/odata/address/companies/{ID}

 

 

The reason is:

When doing an UPDATE operation directly on the backend REST service, we use the following URL:

https://<host>:<port>/rest/addressbook/companies/121

Since the number, which points to the company that has to be updated, is not fix, it has to be replaced by a variable.

The name of the variable has to be the name of the OData-property that identifies the company.

In our OData model, it is the key field, with name ID

That’s how we compose the last segment of the Relative URI, to be entered in the binding wizard in Eclipse:  /{ID}

 

 

 

 

 

 

Custom Code

 

Now generate the Custom Code for Groovy.

 

For the UPDATE operation, we have to implement only the processRequestData() method.

Why?

Because the OData specification doesn’t require that the UPDATE operation returns the payload of the modified resource.

Thus, since the response of our OData service will be empty for the UPDATE, we don’t have to provide anything in the processResponseData() method.

 

 

Implement processRequestData() method

 

Here I have some good news for you:

The implementation of the processRequestData() method of the UPDATE operation is exactly the same like for the CREATE operation.

Yes, you can just copy&paste the code (which of course leads to other questions, sure, how to avoid copy and paste).

 

So here’s again my implementation, based on string-operation (which looks better for demonstration)

 

 

 

def Message processRequestData(message) {

       message.setHeader("Content-Type", "application/atom+xml");

       message.setHeader("x-requested-with", "XMLHTTPRequest");

   

             // convert OData request body to a string that the backend REST service understands

       String odataRequestBody = message.getBody(String.class);

   

       odataRequestBody = odataRequestBody.replaceAll("<Companies>", "<asx:values>");

       odataRequestBody = odataRequestBody.replaceAll("</Companies>", "</asx:values>");

       odataRequestBody = odataRequestBody.replaceAll("<Company>", "<COMPANY>");

       odataRequestBody = odataRequestBody.replaceAll("</Company>", "</COMPANY>");

       odataRequestBody = "<asx:abap version=\"1.0\">" + odataRequestBody +"</asx:abap>";

   

       message.setBody(odataRequestBody);

       return message;

}

 

 

 

Result

 

In order to test the UPDATE operation, proceed as described in the previous tutorial.

The response status code should be 204 and the response body should be empty, as can be seen in the screenshot below.

 

 

 

 

 

 

Summary

 

In this tutorial, we’ve learned how to implement the UPDATE operation in a Groovy script.

We’ve seen that it’s easy:

The processRequestData() method is the same as in CREATE

 

 

 

 

Links

 

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

 

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

 

Introduction in REST data source part 3: Implementing the QUERY operation

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 data source part 7: Implementing the READ operation

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 9: Implementing the CREATE operation

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/21/integration-gateway-understanding-rest-data-source-9-create-operation

 

Overview of all REST blogs

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/08/integration-gateway-rest-data-source-overview-of-blogs

 

The official Documentation: http://help.sap.com/mobile-platform/

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 reading data, which is done via QUERY and READ operations.

 

Since SMP SP07, modifying operations are supported as well.

 

In this blog, we’ll have a look at the CREATE operation:

We’ll do the implementation in the script and in order to create data, we’ll execute a POST request from a browser-based REST client tool.

 

If you’re new to the topic, check the Links section below where you can find the tutorials which get you up to speed.

 

 

 

Prerequisites

 

I expect that you've gone through my previous tutorials, explaining REST data source – QUERY and READ operation – based on XML payload.

Please check the Links section for the relevant blogs.

The prerequisites are:

 

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

 

 

 

Preparation

 

REST Service

 

For this tutorial, we need a REST service that supports writing scenario. I’m using a service that is not public, I hope this blog will be useful nevertheless.

I’ll post an update if that service becomes public some day in future…

 

 

 

OData Model

 

Define OData model

 

The sample REST service that I’m using provides the following sample data:

 

 

So for my OData model, I create the following entity type:

 

 

 

 

Bind data source

 

For better testing, I create binding and scripts for QUERY and READ operation (please refer to my previous tutorials for details).

Then I deploy to SMP, create and assign destination and run the service.

Fine.

 

Now let’s do the binding for the CREATE operation.

 

How are we supposed to build this relative URL?

 

 

As in the previous tutorials, we wonder how the Request URL should look like.

The answer is again: this depends on the backend REST service.

 

Before implementing the OData service, I’ve checked how a POST request which is fired directly to the backend REST service should look like.

For my sample REST service, it looks as follows:

 

 

 

Request URL

the same URL as for the QUERY operation, the URL that lists the entry resources:

https://<host>:<port>/ rest/addressbook/companies



RequestBody

the same that is returned by a READ operation:


<asx:abap version="1.0">

  <asx:values>

    <COMPANY>

      <ID>40</ID>

      <NAME>Indian IT Trading Company</NAME>

      <STREET>Nariman Point</STREET>

      <POSTAL_CODE>400021</POSTAL_CODE>

      <CITY>Mumbai</CITY>

      <COUNTRY>IN</COUNTRY>

      <WEB_ADDRESS>http://www.it-trade.in</WEB_ADDRESS>

      <PHONE_NUMBER>4203876954</PHONE_NUMBER>

    </COMPANY>

  </asx:values>

</asx:abap>



 

 

Note: Request body

For my sample backend REST service, I don’t have to care about unique ID, as it is computed in the backend

 

 

What we learn from this exercise:

1)    The relative URL that we have to enter in the wizard is the same as the one that we enter for the QUERY operation.

       Note: this might be different for other REST services.

2)    The request body has to look like that

 

 

So now we can proceed in Eclipse, we do the “Select Data Source” -> REST -> CREATE -> Request URL:

 

 

 

 

 

Custom Code

 

Then we generate the Custom Code for Groovy.

 

For the CREATE operation, we have to implement both methods,

 

processRequestData()

 

and

 

processResponseData()

 

Why?

In the processRequestData() method, we have to adapt the POST request, before it is sent to the backend REST service

In the processResponseData() method, we have to adapt the response, because the OData specification requires that the POST operation should return the newly created entry in the response payload.

 

 

Implement processRequestData() method

 

After you’ve understood the QUERY and READ tutorials, you’ll guess what needs to be done in the processRequestData() method for the CREATE operation:

 

We get the OData request payload and we have to modify it such that it becomes a payload that is understood by the backend REST service.

 

What we get from the Integration Gateway Framework is not the full OData payload, it is only the data in the usual special structure.

What we have to return, is the full payload, as it is required by the backend REST service.

 

So everything sounds known to us: it is the same like the READ operation, just the other way round.

 

What we have to do:

 

1) The first thing we have to take care, is to set the Content-Type header.

It should be done before other tasks are done.

Why?

Because with this header we specify in which format we want to get the data from Integration Gateway.

If we write:

 

message.setHeader("Content-Type", "application/atom+xml");

 

Then we get the OData request payload as a String that contains an XML structure.

If we don’t set this header, we get a JSON structure.

 

 

2) Second, modify the request body

When our final OData service is used, a POST request will be executed and it will have the following request Body:

 

<entry 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"

          xml:base="https://localhost:8083/gateway/odata/<myNamespace>/<myService>/">

  <content type="application/xml">

    <m:properties>

      <d:ID>40</d:ID>

      <d:NAME>Indian IT Trading Company</d:NAME>

      <d:STREET>Nariman Point</d:STREET>

      <d:POSTAL_CODE>400021</d:POSTAL_CODE>

      <d:CITY>Mumbai</d:CITY>

      <d:COUNTRY>IN</d:COUNTRY>

      <d:WEB_ADDRESS>http://www.it-trade.in</d:WEB_ADDRESS>

      <d:PHONE_NUMBER>4203876954</d:PHONE_NUMBER>

    </m:properties>

  </content>

</entry>

 

 

Note:

As you know, we obtained this body by copy&pasting it from the response body of a READ request.

 

In our script, we get the relevant data structure as XML, which looks as follows

 

<Companies>

  <Company>

    <NAME>Indian IT Trading Company</NAME>

    <COUNTRY>IN</COUNTRY>

    <WEB_ADDRESS>http://www.it-trade.in</WEB_ADDRESS>

    <ID>40</ID>

    <CITY>Mumbai</CITY>

    <POSTAL_CODE>400021</POSTAL_CODE>

    <STREET>Nariman Point</STREET>

    <PHONE_NUMBER>4203876954</PHONE_NUMBER>

  </Company>

</Companies>

 

 

 

What we have to do is to transform it to the following XML-string, which is required by our backend REST service:

 

 

<asx:abap version="1.0">

  <asx:values>

    <COMPANY>

      <NAME>Indian IT Trading Company</NAME>

      <COUNTRY>IN</COUNTRY>

      <WEB_ADDRESS>http://www.it-trade.in</WEB_ADDRESS>

      <ID>40</ID>

      <CITY>Mumbai</CITY>

      <POSTAL_CODE>400021</POSTAL_CODE>

      <STREET>Nariman Point</STREET>

      <PHONE_NUMBER>4203876954</PHONE_NUMBER>

    </COMPANY>

  </asx:values>

</asx:abap>

 

 

 

And this is how my implementation looks like:

 

 

 

def Message processRequestData(message) {

       message.setHeader("Content-Type", "application/atom+xml");

       message.setHeader("x-requested-with", "XMLHTTPRequest");

  

            // convert OData request body to a string that the backend REST service understands

       String odataRequestBody = message.getBody(String.class);

  

       odataRequestBody = odataRequestBody.replaceAll("<Companies>", "<asx:values>");

       odataRequestBody = odataRequestBody.replaceAll("</Companies>", "</asx:values>");

       odataRequestBody = odataRequestBody.replaceAll("<Company>", "<COMPANY>");

       odataRequestBody = odataRequestBody.replaceAll("</Company>", "</COMPANY>");

       odataRequestBody = "<asx:abap version=\"1.0\">" + odataRequestBody +"</asx:abap>";

  

       message.setBody(odataRequestBody);

       return message;

}

 

 

 

Note:

If your backend requires additional handling, then here’s the right place to do it.

For example, my backend requires the x-requested-with header to be set.

 

Note:

I’m doing it with string operations, because I assume that it is easier to see what is happening there.

 

If you deploy your service after implementing the processRequestData() method, then your service will fail – however, the entry will be created in the backend, because the request is being sent and it is properly implemented.

 

But we have to implement the processResponseData() method as well, because we have to return the newly created entity.

In this tutorial we assume that the backend REST service returns the newly created entity.

If this is the case, we have to implement the processResponseData() method just the same as we did for the READ operation.

 

Since you can really just copy&paste the code from the READ script, I’m skipping the explanation here.

 

 

 

def Message processResponseData(message) {

  

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

  

       /* CONVERT JUST AS YOU DID IN READ operation */

       convertPayload(bodyString, message);

  

       return message;

}

 

 

After you’ve properly implemented the processResponseData() method, you can proceed with generate&deploy, then configure the service on SMP and invoke the service.

 

 

 

Result

 

In order to test the CREATE operation, we need to use a REST client tool.

 

Note:

Such REST clients are available e.g. as add-ons for Firefox (e.g. “RESTClient”) or Chrome (e.g. “Advanced Rest Client”) browsers

Please find below a screenshot of my POST request to the OData service created in the current tutorial.

 

 

 

 

 

 

 

Please find below the details of this request:

 

 

Header 1Header 2
Request URLhttps://<yourSMP>:8083/gateway/odata/<yourNameSpace>/<yourService>;v=1/<yourEntitySet>
HTTP verbPOST
x-csrf-token headeras provided by your SMP (see note below)
Content-Type header
application/atom+xml
Request body

<entry 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"

          xml:base="https://localhost:8083/gateway/odata/SAP/RESTPROJECT10_CREATE;v=1/">

  <content type="application/xml">

    <m:properties>

      <d:ID>40</d:ID>

      <d:NAME>Indian IT Trading Company</d:NAME>

      <d:STREET>Nariman Point</d:STREET>

      <d:POSTAL_CODE>400021</d:POSTAL_CODE>

      <d:CITY>Mumbai</d:CITY>

      <d:COUNTRY>IN</d:COUNTRY>

      <d:WEB_ADDRESS>http://www.it-trade.in</d:WEB_ADDRESS>

      <d:PHONE_NUMBER>4203876954</d:PHONE_NUMBER>

    </m:properties>

  </content>

</entry>

 

 

 

The screenshot also shows the response that we get after sending the request.

The HTTP status code should be 201

The response body should display the created entity

 

The backend REST service should as well display the newly created resource.

 

If you’ve got this response: congratulations, you’re through!

If not, I wish you quite some patience with trouble-shooting...

 

 

Note:

Regarding the x-csrf-token header:

I assume that you know how to obtain this token?

You have to first send a GET request to a valid URL, e.g. the EntitySet URL

This GET request should be executed with the header

x-csrf-token: fetch

(Name of the header is “x-csrf-token” and the value of the header is “fetch”)

 

After executing this request in your REST client, you should check the response headers.

You’ll find the header x-csrf-token with a value which is the token

Now copy the value and paste it into the request header, such that you have

x-csrf-token: <value of the token from the response>

 

Note:

Regarding fetching the header:

If you proceed as described above and don’t get the token then the reason is that it is always sent only once.

In order to force SMP to send it again, just logout from Gateway Management Cockpit and login again

 

 

 

Summary

 

In this tutorial, we’ve learned how to implement the CREATE operation in a Groovy script.

We’ve seen that there are no surprises:

  • The processResponseData() method is the same as in READ
  • The processRequestData() method is again the same, just the other way round
  • “Content-Type” header is required in the script.
  • The Relative URL in the Eclipse-Binding-Wizard is the same as for the QUERY

 

 

 

Links

 

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

 

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

 

Introduction in REST data source part 3: Implementing the QUERY operation

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 data source part 7: Implementing the READ operation

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

 

Overview of all REST blogs

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/08/integration-gateway-rest-data-source-overview-of-blogs

This blog post aims to collect all links to Blogs and Documents that provide tutorials and information about the REST data source for Integration Gateway in SAP Mobile Platform 3.0 (SMP).

Based on the component Integration Gateway in SMP, you can provide OData services, that expose data that is fetched from different data sources - like Database, JPA, and also REST-services.

In order to make use of a REST data source, the OData service developer has to write custom coding.

Which is explained in the following blogs.

 

 

Processing the response payload


The main task that has to be implemented in the script is to transform the payload of the REST service into a common data structure.

 

For better understanding and easier getting up to speed, the following blogs provide a very simple example with hard-coded payload

These blogs are based on the QUERY operation

 

REST service with xml payload

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

The same with JSON payload

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

 

QUERY operation: The next step: not hard-coded, using string operations

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

 

Alternative: using parser to avoid 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

The same with JSON parser

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

 

READ operation

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

The same with JSON

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

 

POST operation

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/21/integration-gateway-understanding-rest-data-source-9-create-operation


UPDATE operation

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/22/integration-gateway-understanding-rest-data-source-10-update-operation

 

DELETE operation

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/22/integration-gateway-understanding-rest-data-source-11-delete-operation




Advanced topics


Implementing $filter

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

 

Implementing navigation

http://scn.sap.com/community/developer-center/mobility-platform/blog/2015/04/23/integration-gateway-understanding-rest-data-source-12-navigation

 

 

 

Other topics

 

Preparing Eclipse for Groovy scripting:

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

SAP Mobile Platform supports multiple built-in authentication providers that authenticate users. The SMP administrators can create security profile and assign the authentication providers using Management Cockpit.

 

In this example I am going to show you how to use LDAP to authenticate from your mobile application.


When I started, I did't find any existing LDAP setup, so I have configured one. If you don't have an existing LDAP Server you can follow by blog, it's quite easy using Apache Directory Studio: Configuring LDAP -  A Basic Tutorial

Configuring LDAP security profile

     1. Login to SMP Admin Cockpit.

     2. Click on Security Tab. Under Security Profiles click on New button. It will open Edit Security Profile window.

      IGWREST_25 Apr. 04 07.55.jpg

     3. On Name field enter "LDAP", then click on Add button.

         It will open Add Authentication Provider window.

      IGWREST_25 Apr. 04 09.18.jpg

     4. Choose Authentication providers as Directory Service (LDAP/AD) and provide the below details:

 

PropertyValue
Control FlagOptional
Server Typesunone5
Provider URLldap://localhost:10389
Security ProtocolLDAP
Bind DNuid=admin,ou=system
Bind Passwordsecret
Authentication Search Baseou=users,ou=system
Skip Role Lookuptrue
Role Member Attributesuniquemember
User Role Membership AttributesnsRoleDN
Default Search Baseou=users,ou=system


     Leave all other fields with default values.

     After entering the values the screen should look like below:

      config.jpg

     IGWREST_24 Apr. 03 14.07.jpg

     5. Click on Save.

 

Configuring Application

     1. Go to Applications panel.

     2. Click on New button. It will open New Application window.

     3. On ID field enter LDAPAuth and click on Save. It will open a new window.

      IGWREST_25 Apr. 04 08.26.jpg

     4. On field Endpoint enter http://services.odata.org/V2/Northwind/Northwind.svc/

     5. Under SSO Mechanisms, click on Add. Then click on Save.

      IGWREST_25 Apr. 04 08.38.jpg

     6. Click on Authentication Tab. For field Profile Name choose LDAP.

      IGWREST_25 Apr. 04 08.44.jpg

     7. Click on Save.

Authenticate using LDAP

     1. Run Post man rest client on Chrome and provide below values. Pass LDAP credentials to register.

       IGWREST_25 Apr. 04 09.04.jpg

     2. Click on on Send button. It will register the user on SMP server.

         Success response from Postman Rest Client:

      IGWREST_25 Apr. 04 09.08.jpg

On SMP Admin cockpit you can find the registered user.

    IGWREST_25 Apr. 04 09.10.jpg

To  register user from a mobile app there is no LDAP specific device code needed if you are using MAF Logon or LogonCore class or REST API.

 

CC:

SMP Developer Center

 

Regards, Midhun

SAP Technology RIG

One day, I would like to see the client code behind the list selection step. I imagine, there is some comment along the lines of "//I hate myself and I want to die".

 

Sounds a bit harsh? True, but what would life be without some complaining every now and then. Now, let me explain, why I think this line of comment might be in there somewhere...

 

The ListSelection step is pretty straight forward. You define the Screen Set, Screen, List Control (a field on the Screen) and what line to be selected within that List Control. This should be all you need for the list selection step to work, right? With that information, there is no way you (the client) could miss the UI control. Also, the ListSelection step definitely knows about the Screen Set and thus the Object displayed by that Screen Set. Why do I emphasize that fact? Because for some reason that totally escapes me, the displayed object of the Screen Set seems to be important. You might think that if that object is really so important, the ListSelection step could just pass the information on the displayed Object to the List Control, but no... we are forced to provide the Screen Sets object as target in the Action containing the ListSelection step! Why on god's green earth?!?

 

 

 

 

Okay, enough trolling for now... to make this a bit more productive, let me give you (and my future self) an example on how to make the list selection step work in a non-trivial situation. (Just copy the following lines and replace the definition names with your definition names to better understand what might be going wrong in your case.)

 

Assuming objects on the client look like this

  • MainObject
    • ScreenSetObjects
      • ScreenSetObject 1
      • ScreenSetObject 2
    • ListObjects
      • ListObject 1
      • ListObject 2

  

and our Screen (Set) definitions look like this

  • ScreenSet "ListScreenSet" displays Object "ScreenSetObject"
    • Detail Screen "ListScreen"
      • Field "ListControl" of type "List Tile View" on collection ":>Main Object>"ListObjects" Property" where "ListObjects" contains Objects of type "ListObject"
      • Action Button "NextRowButton" with Action "NextRowAction" and Target "???"

  

and the Action looks like this

  • Action "NextRowAction" for object "???"
    • List Selection Step "ListSelection"
      • Screen Set "ListScreenSet"
      • Screen "ListScreen"
      • List Control "ListControl"
      • Select Rows "Next Row"

 

what would we have to put in the gaps (???) to make it work?

  • the target of the "NextRowButton" has to be the exact object that is being displayed in the "ListScreenSet" (here it is one of the "ScreenSetObject" objects)
  • and consequently, the "for object" of the "NextRowAction" has to be set to "ScreenSetObject"

 

And that is how to use List Selection steps...

 

Daniel

 

 

(Based on SMP 3.0 SP7 PL1, WPF Client)

15.pngHi everyone!


For those who have ever read any of my blogs, hello again..


Those previous blogs are for iOS native apps, but this one is for all the SMP3 OData SDK platforms. If you have gone through one of those platforms SDKs, you should have been on a good track of happy app development.


The common discussion while gaining development experience is about the offline store performance with large OData collections. Even though it is originally designed for fairly large volume for mobile devices, there are some tips you should be aware of -  hopefully this will help your development.

 

<updated>Apr 23th, 2015</updated>



Problem: Your app with offline store has some performance issue with large volume of data. The initial creation of the store and the refresh time takes longer than you had expected.

 

Solution: Try out the several practical tuning tips below.

 

Discussion:

 

When you encounter the situation, here's the checklist of tips & remarks:

 

1. Measure the OData size and time

2. The log trace of offline component with DEBUG level to figure out the bottleneck

3. How OData is implemented – Server Side Paging

4. How OData is implemented – Delta Tracking

5. Using Server Side Paging and Delta Query

6. Possible performance improvement with Caching

 

Let's take a look at one by one.


1. Measure the OData size and time


Current web browser provides very useful tools built-in. By using developer tools, we can measure the precise data size easily, such as content size, network time..etc to gain solid idea what’s going on. Here's my example (by Chrome) capture fetching 10000 entities. The content size is 20.3MB and it gets compressed in 618KB size during the transfer, and it took 18.20 seconds during HTTP GET. (Latency is like a ping call)

chrome.png

If you compare the payload sizes which has the same amount of the entity numbers but double numbers of parameters in each entity, you'll see the total size of data transfer gets nearly double (of course). So you can examine how the properties of each entity can affect the data transfer size and time.


And - Payload in JSON format is lot smaller than XML.

json.png

xml.png

 

2. The log trace of offline component with DEBUG level to figure out the bottleneck


Bump up the trace level for offline component. Go to "LOGS" > "SETTINGS" in admin console.

logs_setting.png

Change the log level of the Offline component to "debug" and the trace "Enabled".

debug.png

Now the offline component is ready to show the useful info.

log.png

The info provides very fine details of the offline store, you'll get clear idea what's happening behind the scenes. The timestamp data also tells you where's the bottleneck is.

details.png

 

3. How OData is implemented – Server Side Paging

skiptoken.png

As already discussed here, your offline store would fail to initialize if the OData collection in the defining request is too large. In that case you need to implement $skiptoken (aka Server Side Paging). Most of OData producer frameworks should support $skiptoken.

 

Once you implement it, in general, you should experiment a bit with the page sizes until you find a size that balances the backend performance and the response size. (For example, one of my personal experiment was 1000 entities in one page was slower than 5000.)

 

How to Implement $skiptoken with TravelAgency table in SAP Gateway

 

4. How OData is implemented – Delta Tracking

deltatoken.png

Delta Query implementation in the OData producer is effective way to boost up the overall end-to-end performance. A quick explanation from the offline store perspective is written here. Just like $skiptoken, most of the OData producers should support delta query, so please consult respective documentation on how to implement.


SAP Gateway could have two major options to implement Delta Tracking:

 

  • The approach based on Syclo xChange framework. Calculating deltas at modification time - The ABAP system tracks relevant changes when they occur. At request time, the deltas are already prepared and thus available. On the one hand this approach requires small coding and configuration steps. On the other hand it’s more scalable and has an optimized overall performance.

 

How To Implement Lightening Fast OData Services with Exchange Table

 

  • The approach based on delta determination at request time - where the system compares old and new state to find out which records have been changed/deleted. The implementation effort is small but it does not optimize the performance of the backend - this needs to “compare” all the entity to calculate the delta - that means, the more records you have in the full collection, the longer the response time of the request.


How to Implement Basic Delta Query Support in SAP Gateway


The H2G on Integration Gateway:  How To Use the Custom Coding for JDBC Connections in Integration Gateway

 

5. Using Server Side Paging and Delta Query


You want to use both? Good idea.

useboth.png

How To Use Delta Query and Server Side Paging with Soft State

 

6. Possible performance improvement with Caching


Together with Delta Tracking, Caching is also referred here:

 

"To minimize the load on the OData Producer and possibly improve performance, cache data that is shared by numerous users on SAP Mobile Platform Server. "


Essentially what you need to do is to tweak the parameters for the defining request by uploading .ini file to the SMP server. You'll find the params for cache is pretty optimized by default, so you might not see a real difference even if you use this feature - hence I described it as "possible" performance improvement.


One remark about the parameter is about "indexed_type". It is not for caching but it is used primarily for $orderby and for $filter clauses when executing requests from the client. So when your offline store deals with a high volume of data, your local $orderby or $filter query execution (remember - offline store CRUD operations get queued locally until the next flush or refresh, it acts like an OData producer) might get faster.


Tip: (Once you configured the Offline component Trace as written in 2) Log lines containing “MODATA_EP” describe the operations of the delta calculations in MobiLink. In particular lines with MODATA_EP_REQ_OPT show the options that are used. 

 


Okie that's all for now. I'll update this content whenever new & important info comes in!



See you in the next blog,

Ken


List of blogs


Client Hub is a mobile application that comes with SAP Mobile Platform SDK. When we have multiple enterprise applications in the device, Client Hub app helps to share common credentials between the apps. Hence the user doesn't need to enter same credentials multiple times to login to multiple apps that improves user experience.

 

Client Hub app has to be installed in the device along with other business apps - when the user enter the credentials for the first time in an enterprise application, it will be stored in the Client Hub (data-vault). And all other apps access these credentials for registration with SAP Mobile Platform, ie. other applications will not prompt the user to enter credentials for login.

Only condition is, all applications including Client Hub has to be signed with same certificate. In this blog I am explaining how to do it.

 

IGWREST_15 Mar. 30 01.14.jpg

Step 1 - Signing apps with same certificate

The source code of the Client Hub app can be found in the SMP 3 SDK installation path..SAP\MobileSDK3\ClientHub. Here I am using Android as an example. Client hub app is available for iOS too. Client Hub app works only with MAF Logon -  It supports both Odata and Kapsel apps.

 

Import the Client Hub project to Android SDK. Then Right click on the project > Export.

IGWREST_7 Mar. 29 11.13.jpg

Click Next until you reach Keystore selection screen. Choose "Create new keystore", and provide keystore name and password. Click Next and enter the details as given below.

IGWREST_9 Mar. 29 11.16.jpg IGWREST_9 Mar. 29 11.17.jpg

Click Next, then click on Finish. It creates Client Hub apk file and keystore.

IGWREST_15 Mar. 30 01.52.jpg

In the business application create clienthub.properties file inside res/raw folder and provide connection details as given below.

Untitled.png

Inside Manifest.xml file include permission for clienthub access -  <uses-permission android:name="com.sap.mobile.clientHub.CLIENTHUB_ACCESS_PERMISSION"/>

 

Finally, right click on the business application and choose Export. Instead of choosing "Create new keystore" choose "Use existing keystore" and browse the keystore created in the previous step. Repeat this step for all the business apps, and create the apk files.

IGWREST_15 Mar. 30 01.51.jpg

 

Step 2 - Installation and Testing

Install all the enterprise apps including Client Hub app in a device and test.

 

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



Troubleshooting Section: Testing batch request in a REST client in 3 steps

Testing the batch request from a REST client will help you discard any problems with the backend.

For more information about the REST client, visit  SMP 3.0 : REST API Application Development

 

Step1: Onboard user and copy Application connection id from the response

 

Request: POST

URL: http://<server>:<port>/odata/applications/latest/<appid>/Connections

Headers

Content-Type: application/atom+xml

Authorization: Basic <encoded login and password>

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>

<d:DeviceType>Android</d:DeviceType>

</m:properties>

  </content>

</entry>

 

batchTest1.jpg

Step2: Fetch CSRF Token

 

Request: GET

URL: http://<server>:<port>/<appid>/

Headers

Authorization: Basic <encoded login and password>

X-SMP-APPCID: <value received from step 1: onboard user>

X-CSRF-Token: Fetch

batchTest2.jpg

 

Step3: Send Batch request with two read operations

Request: POST

URL: http://<server>:<port>/<appid>/$batch

Headers

Content-Type: multipart/mixed; boundary=batch

Authorization: Basic <encoded login and password>

X-SMP-APPCID: <value received from step 1: onboard user>

X-CSRF-Token: <value received from step 2: fetch CSRF Token>

Body (mind the spacing between the --batch tags, it's very important)

 

--batch

Content-Type: application/http

Content-Transfer-Encoding: binary

 

GET CarrierCollection HTTP/1.1

 

 

--batch

Content-Type: application/http

Content-Transfer-Encoding: binary

 

GET TravelAgencies/?$filter= COUNTRY eq 'CA' HTTP/1.1

 

 

--batch--


batchTest3.jpg


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

Actions

Filter Blog

By author:
By date:
By tag: