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

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

It shows how to implement the $skip capability in an OData service based on Integration Gateway.

In my previous Blog, I’ve already described how to implement the system query option $top.

Everything described there, motivation, prerequisites,  preparation etc, is valid here as well.

So let us skip all introductory text and go directly to the implementation.

Implement $skip

What do we want to achieve?

With the query option $skip, the user of an OData service can specify the number of entries that should be ignored at the beginning of a collection.

So if a user specifies $skip=n then our OData service has to return the list of entries starting at position n+1

This is specified by the OData document that can be found here (scroll down to section 4.4.)

Example

The following "normal" query delivers an amount of e.g. 46 companies, where the first company has the ID '1'

https://localhost:8083/gateway/odata/SAP/<yourService>/Companies

Now the following query with the system query option $skip=1 delivers an amount of 45 companies, where the first entry in the list has the ID '2'

https://localhost:8083/gateway/odata/SAP/<yourService>/Companies?$skip=1

How to achieve it?

Just like in the previous blog, we get the data from the backend REST service and we modify it such that it has the xml structure that is expected by the Integration Gateway framework.

In order to modify the structure, we parse the xml and create a Document object.

This instance of Document is used to apply the system query options in our little series of blogs.

After refining the Document (apply system query options), it is converted back to a string, which is then set as body to the Message object.

In order to apply the $orderby we proceed as follows:

1. First obtain the value of the $skip that is given by the user of the service

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

       Integer skipOption = uriInfo.getSkip();

2. Then remove this amount of entries from the collection, starting from the beginning

            // retrieve the list of entries

       Node entitySetNode = document.getFirstChild();

       NodeList entities = entitySetNode.getChildNodes();

            // and remove the first entries according to $skip

            int i = 1;

            while (i <= skipNumber) {

             Node firstEntry = entitySetNode.getFirstChild();

             entitySetNode.removeChild(firstEntry);

             i++;

       }

The implementation for $skip

I’ve added a few checks and created the method applySkip()

Note:

we don’t need to check for negative $skip value, because this is handled by the OData library.

The applySkip() method is invoked from within the callback method processResponseData()

def Document applySkip(Document document, Message message){

       // check the URI for the $skip option

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

    Integer skipOption = uriInfo.getSkip();

       if(skipOption == null){

            // if skipOption is null, then there's no $skip in theURI, so do nothing

             return document;

    }

    // $skip is used

       int skipNumber = skipOption.intValue();

       // retrieve the list of entries

    Node entitySetNode = document.getFirstChild();

    NodeList entities = entitySetNode.getChildNodes();

       int totalCount = entities.getLength();

       // check if the given value for $skip is higher than the number of entries

       if(skipNumber > totalCount){

            // error handling

       message.setBody("The given value for skip is too high");

       message.setHeader("camelhttpresponsecode", 400);

  

             return null;

    }

       // now remove the entries according to $skip

       int i = 1;

       while (i <= skipNumber) {

       Node firstEntry = entitySetNode.getFirstChild();

       entitySetNode.removeChild(firstEntry);

       i++;

    }

       return document;

}

Supporting error handling

This sample code also showcases how to do error-handling:

The error message is set as body to the Message-instance.

As we know, the IGW – framework expects that the body is an xml-structure that matches the OData model.

Therefore, we have to tell the FWK that there isn't any valid response payload, but instead, an error message has to be displayed.

This is achieved by setting an error-status-code to the respective header.

In the sample code, we’re setting status 400 which is BAD REQUEST

Additionally, we have to modify the processResponseData() method:

If the applySkip method returns null, then we don’t set the body to the Message instance, as this is already done.

Supporting multiple system query options

Usually, you’ll want to support both $top and $skip in your OData service.

In such case, you have to consider the following:

From the full list of entries, you have to FIRST apply the $skip, and only THEN apply the $top.

This makes a significant difference.

The OData V2 specification describes it here (See section 4.4 and the second example)

The complete custom script

The following sample code contains the full content of the custom script.

The content is the same as the script file attached to this blog.

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.apache.olingo.odata2.api.uri.KeyPredicate

import org.apache.olingo.odata2.api.uri.NavigationSegment

import org.apache.olingo.odata2.api.uri.UriInfo

import org.w3c.dom.Document

import org.w3c.dom.Element;

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.ILogger

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

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

def Message processRequestData(message) {

       return message;

}

def Message processResponseData(message) {

       message = (Message)message;

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

       /* CONVERT PAYLOAD */

    InputSource inputSource = new InputSource(

                                   new ByteArrayInputStream(

                                       bodyString.getBytes(StandardCharsets.UTF_8)));

    inputSource.setEncoding("UTF-8"); // required due to BOM

    Document document = loadXMLDoc(inputSource);

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

    document = refactorDocument(document);

       // handle system query options

    document = applySkip(document, message);

       if(document == null){ // react to error

             return message;

    }

       // convert the modified DOM back to string

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

       /* FINALLY */

    message.setBody(structuredXmlString);

       return message;

}

def Document loadXMLDoc(InputSource source) {

    DocumentBuilder parser;

       try {

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

    } catch (ParserConfigurationException e) {

       ((ILogger)log).logErrors(LogMessage.TechnicalError, "Failed to create parser");

             return null;

    }

       // now parse

       try {

             return parser.parse(source);

    } catch (SAXException e) {

       ((ILogger)log).logErrors(LogMessage.TechnicalError, "Error while parsing source");

             return null;

    }

}

def Document refactorDocument(Document document){

       //find nodes

    Node rootElement = document.getFirstChild();

    Node asxValuesNode = rootElement.getFirstChild();

    Node proddataNode = asxValuesNode.getFirstChild();

    NodeList snwdNodeList = proddataNode.getChildNodes();

       //rename all nodes of the feed

    document.renameNode(proddataNode, proddataNode.getNamespaceURI(), "Companies");

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

        Node snwdNode = snwdNodeList.item(i);

        document.renameNode(snwdNode, snwdNode.getNamespaceURI(), "Company");

    }

       //replace node

    Node cloneNode = proddataNode.cloneNode(true);

    document.replaceChild(cloneNode, rootElement);

       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 toStringWithoutProlog(Document document, String encoding) {

       // Explicitly check this; otherwise this method returns just an XML Prolog

       if (document == null) {

       ((ILogger)log).logErrors(LogMessage.TechnicalError, "Error: document is null.");

             return null;

    }

    TransformerFactory transformerFactory = TransformerFactory.newInstance();

    Transformer t = null;

       try {

       t = transformerFactory.newTransformer();

    } catch (TransformerConfigurationException e) {

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

             return null;

    }

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

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

       t.setOutputProperty(OutputKeys.ENCODING, encoding);

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

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

       OutputStream os = new ByteArrayOutputStream();

       OutputStreamWriter osw = null;

       try {

       osw = new OutputStreamWriter(os, encoding);

    } catch (UnsupportedEncodingException e) {

       ((ILogger)log).logErrors(LogMessage.TechnicalError, "Error creating Writer");

             return null;

    }

    BufferedWriter bw = new BufferedWriter(osw);

       try {

       t.transform(new DOMSource(document), new StreamResult(bw));

    } catch (TransformerException e) {

       ((ILogger)log).logErrors(LogMessage.TechnicalError, "Error during transformation");

             return null;

    }

       return os.toString();

}

def Document applySkip(Document document, Message message){

       // check the URI for the $skip option

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

    Integer skipOption = uriInfo.getSkip();

       if(skipOption == null){

             // if skipOption is null, then there's no $skip in the URI, so do nothing

             return document;

    }

       // $skip is used

       int skipNumber = skipOption.intValue();

       // retrieve the list of entries

    Node entitySetNode = document.getFirstChild();

    NodeList entities = entitySetNode.getChildNodes();

       int totalCount = entities.getLength();

       // check if the given value for $skip is higher than the number of entries

       if(skipNumber > totalCount){

             // error handling

       message.setBody("The given value for skip is too high");

       message.setHeader("camelhttpresponsecode", 400);

             return null;

    }

       // now remove the entries according to $skip

       int i = 1;

       while (i <= skipNumber) {

       Node firstEntry = entitySetNode.getFirstChild();

       entitySetNode.removeChild(firstEntry);

             i++;

    }

       return document;

}