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?
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 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);
returnnull;
}
// 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
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");
returnnull;
}
// now parse
try {
return parser.parse(source);
} catch (SAXException e) {
((ILogger)log).logErrors(LogMessage.TechnicalError, "Error while parsing source");
returnnull;
}
}
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.");
returnnull;
}
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer t = null;
try {
t = transformerFactory.newTransformer();
} catch (TransformerConfigurationException e) {
((ILogger)log).logErrors(LogMessage.TechnicalError, "Error creating Transformer");
returnnull;
}
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");
returnnull;
}
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");
returnnull;
}
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);
returnnull;
}
// now remove the entries according to $skip
int i = 1;
while (i <= skipNumber) {
Node firstEntry = entitySetNode.getFirstChild();
entitySetNode.removeChild(firstEntry);
i++;
}
return document;
}