This blog is about Integration Gateway (IGW) in SAP Mobile Platform 3.0 (SMP).
It shows how to implement the $orderby 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 $orderby
Background
When requesting a list of entities from a service, it is up to the service implementation to decide in which order they are presented. This can depend on the backend data source - anyways, it is undefined.
But the user of an OData service might want to be able to specify the order, according to his needs.
For example, a usual case would be that the list of entities is sorted as per default by its ID number, but for a user, the ID is not relevant and he would prefer a sorting e.g. by the name
OData supports this requirement with the system query option $orderby
It is specified as follows:
$orderby=<propertyName>
The order can be ascending or descending:
$orderby=<propertyName> asc
$orderby=<propertyName> desc
If not specified, the default is ascending.
See here for more details (section 4.2.)
Note:
As of the OData specification, the $orderby system query option can be applied to multiple properties.
In that case, the value is specified as comma-separated list.
Example:
https://localhost:8083/gateway/odata/SAP/<yourService>/Companies?$orderby=COUNTRY desc, NAME asc
In this example, all companies should be displayed, and they should be sorted by their country in descending order. Additionally, within each country, they should be sorted by their name in ascending order.
In order to support such complex sorting, the OData service implementation has to make use of the ExpressionVisitor concept.
We aren't using it in the present blog, because the ExpressionVisitor is explained in the $filter blog
Implementation
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 the present blog.
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.
The step of applying the $orderby can be divided into the following sub-steps:
1. Analyze the URI, retrieve the value of the $orderby
2. Convert the list of DOM nodes into a java.util.ArrayList
3. Do the sorting on the ArrayList
4. Convert the ArrayList back to DOM nodes
Note:
This approach seems easier to me to showcase the $orderby. Of course, one could think also of other approaches, that would be e.g. more performant
Step 1: The URI
What we intend to achieve in this section is to get the name of the property (which should be used for sorting) from the URI.
Example:
We have a list of companies and we want to sort it by the country.
The URL to be invoked:
https://localhost:8083/gateway/odata/SAP/<yourService>/Companies?$orderby=COUNTRY
The name of the property that we need: COUNTRY
Possible values to be sorted: DE, IN, US, etc
The system query option $orderby is more powerful than the query options covered in the previous blogs.
So here we need a few more lines of code to get the desired information.
First, as usual, obtain the OrderByExpression instance from the UriInfo
Check if it is null.
Then ask it for its orders.
UriInfo uriInfo = (UriInfo) message.getHeaders().get("UriInfo"); OrderByExpression orderByExpression = uriInfo.getOrderBy();… List orders = orderByExpression.getOrders();
Here we get a java.util.List, because – as mentioned above, the $orderby expression can be formed by multiple properties.
We don’t need to check if the list is null or empty, because this is checked by the OData library.
So we directly access the first entry of the list.
As mentioned above, we’re keeping this tutorial simple, so we don’t support sorting by more than one property.
OrderExpression orderExpression = orders.get(0);
We need a few more lines to retrieve the property name |
CommonExpression commonExpression = orderExpression.getExpression(); String sortProperyName = ((PropertyExpression)commonExpression).getPropertyName();
Step 2: The list to be sorted
The decision to be taken here is: Do we want to sort manually, or do we want to use Collections.sort() ?
The second option is easier, but the disadvantage is that it can be applied only to an instance of java.util.List
But what we have is an xml DOM, an instance of org.w3c.dom.NodeList with child nodes
So we have to convert the NodeList into an ArrayList
We have to move all the nodes of our xml DOM into the ArrayList.
//For using standard sorter, we need an ArrayList instead of NodeList List<Element> companyList = new ArrayList<Element>(); // move all company nodes into the ArrayList Node entitySetNode = document.getFirstChild(); NodeList companiesNodeList = entitySetNode.getChildNodes(); for(int i = 0; i < companiesNodeList.getLength(); i++){ Node companyNode = companiesNodeList.item(i); if(companyNode.getNodeType() == Node.ELEMENT_NODE){ Element companyElement = (Element)companyNode; // populate the ArrayList companyList.add(companyElement); } }
Step 3: The sorting
Now we have an ArrayList containing the xml nodes which contain the data.
Based on that, we can implement a java.util.Comparator
This comparator will be then be used by the Collections.sort() method.
What does our comparator have to do?
It has to compare the values of a given property.
Example:
If the following URL is invoked:
https://localhost:8083/gateway/odata/SAP/<yourService>/Companies?$orderby=COUNTRY
then all the values of the property COUNTRY have to be compared.
For example, our comparator will have to compare DE with IN and with US, etc
Our ArrayList that has to be sorted, contains instances of (company-) Node, so we have to extract the desired property from it, and then read the value of the property.
Note: in our example, we can cast the org.w3c.dom.Node instance to an org.w3c.dom.Element, which makes it easier to handle.
Since we’re comparing strings, we can delegate the actual comparison to the String class
String value1 = element1.getElementsByTagName(sortProperyName).item(0).getFirstChild().getNodeValue(); String value2 = element2.getElementsByTagName(sortProperyName).item(0).getFirstChild().getNodeValue(); // we delegate the sorting to the native sorter of the java.lang.String class compareResult = value1.compareTo(value2);
There’s one more thing to consider here:
In our model, all properties are of type Edm.String
I don’t want to discuss here if it is meaningful to assign type Edm.String instead of Edm.int32 to the ID property;-)
In our comparator, we just treat the ID property differently.
If we would let the native String comparator do the compare job for numbers, we would get wrong results:
For a String-comparator the “5” is higher than e.g. “46”, which for numbers is wrong.
So in case of property ID, we convert the String to an Integer and let the Integer class do the comparison.
if(sortProperyName.equals("ID")){ Integer integerValue1 = new Integer(value1); Integer integerValue2 = new Integer(value2); // we delegate the sorting to the native sorter of the Integer class compareResult = integerValue1.compareTo(integerValue2); }
One last implementation to be done:
OData supports sorting in ascending or descending way.
We have to get this information from the orderExpression and evaluate it.
Depending on the value, the result has to be reverse.
Our comparator has to return a positive int if the first value is bigger than the second.
So if the sort order is descending, we just convert the positive value into a negative value.
// if 'desc' is specified in the URI, change the order of the list SortOrder sortOrder = orderExpression.getSortOrder() if (sortOrder.equals(SortOrder.desc)){ return - compareResult; // just convert the result to negative value to change the order }
Step 4: The final DOM
So far, we now have a sorted ArrayList.
But we need our instance of org.w3c.dom.Document to have sorted child nodes.
So our next step is to move the entries of the ArrayList back to the Document.
This can be done in several ways, I’ve decided to replace the existing parent node (which contains children in wrong order) with a newly created node that contains the children in the correct order.
So we have to
1. create a new node that represents the EntitySet
Element newEmptyEntitySetNode = document.createElement("Companies");
2. populate it with child nodes that are in the (sorted) ArrayList
Iterator iterator = companyList.iterator(); while (iterator.hasNext()) { Element companyElement = (Element)iterator.next(); newEmptyEntitySetNode.appendChild(companyElement); }
3. Finally replace the old EntitySet-node with the new one
document.replaceChild(newEmptyEntitySetNode, entitySetNode);
The complete implementation of $orderby
def Document applyOrderby(Document document, Message message){ // check the URI for the $orderby option UriInfo uriInfo = (UriInfo) message.getHeaders().get("UriInfo"); OrderByExpression orderByExpression = uriInfo.getOrderBy(); if(orderByExpression == null){ // if orderByExpression is null, then there's no $orderby in the URI, so do nothing return document; } List orders = orderByExpression.getOrders(); // for multiple properties for ordering OrderExpression orderExpression = orders.get(0); // simple implementation. Better: use ExpressionVisitor CommonExpression commonExpression = orderExpression.getExpression(); if(! (commonExpression instanceof PropertyExpression)){ return document; } // the name of the property, which should be used for sorting (e.g. sort by company name) String sortProperyName = ((PropertyExpression)commonExpression).getPropertyName(); //For using standard sorter, we need an ArrayList instead of NodeList List<Element> companyList = new ArrayList<Element>(); // move all company nodes into the ArrayList Node entitySetNode = document.getFirstChild(); NodeList companiesNodeList = entitySetNode.getChildNodes(); for(int i = 0; i < companiesNodeList.getLength(); i++){ Node companyNode = companiesNodeList.item(i); if(companyNode.getNodeType() == Node.ELEMENT_NODE){ Element companyElement = (Element)companyNode; // populate the ArrayList companyList.add(companyElement); } } // now do the sorting for the ArrayList Collections.sort(companyList, new Comparator<Element>(){ public int compare( Element element1, Element element2) { int compareResult = 0; String value1 = element1.getElementsByTagName(sortProperyName).item(0).getFirstChild().getNodeValue(); String value2 = element2.getElementsByTagName(sortProperyName).item(0).getFirstChild().getNodeValue(); // we need special treatment for the ID, which is of type string, but for correct order we need the number if(sortProperyName.equals("ID")){ Integer integerValue1 = new Integer(value1); Integer integerValue2 = new Integer(value2); // we delegate the sorting to the native sorter of the Integer class compareResult = integerValue1.compareTo(integerValue2); }else{ // we delegate the sorting to the native sorter of the java.lang.String class compareResult = value1.compareTo(value2); } // if 'desc' is specified in the URI, change the order of the list SortOrder sortOrder = orderExpression.getSortOrder(); if (sortOrder.equals(SortOrder.desc)){ return - compareResult; // just convert the result to negative value to change the order } return compareResult; } }); // now that the sorting is done, convert the (sorted) ArrayList back to NodeList // first create a new entitySet node Element newEmptyEntitySetNode = document.createElement("Companies"); // then add all child nodes Iterator iterator = companyList.iterator(); while (iterator.hasNext()) { Element companyElement = (Element)iterator.next(); newEmptyEntitySetNode.appendChild(companyElement); } // then replace the old entitySet node with the new node (containing the sorted children) document.replaceChild(newEmptyEntitySetNode, entitySetNode); //Finally, return the document with sorted child nodes return document; }
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.UriInfo import org.apache.olingo.odata2.api.uri.expression.CommonExpression import org.apache.olingo.odata2.api.uri.expression.OrderByExpression import org.apache.olingo.odata2.api.uri.expression.OrderExpression import org.apache.olingo.odata2.api.uri.expression.PropertyExpression import org.apache.olingo.odata2.api.uri.expression.SortOrder; import org.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"); // note: this is REQUIRED, because the input has a BOM, and has encoding UTF-16, which can't be parsed Document document = loadXMLDoc(inputSource); // now do the refactoring: throw away useless nodes from backend payload document = refactorDocument(document); // handle system query options document = applyOrderby(document, 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, "Error: failed to create parser: ParserConfigurationException"); return null; } // now parse try { return parser.parse(source); } catch (SAXException e) { ((ILogger)log).logErrors(LogMessage.TechnicalError, "Exception ocurred while parsing source: SAXException"); 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: the given 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: TransformerConfigurationException while 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: UnsupportedEncodingException while creating OutputStreamWriter..."); 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: TransformerException while transforming to String..."); return null; } return os.toString(); } def Document applyOrderby(Document document, Message message){ // check the URI for the $orderby option UriInfo uriInfo = (UriInfo) message.getHeaders().get("UriInfo"); OrderByExpression orderByExpression = uriInfo.getOrderBy(); if(orderByExpression == null){ // if orderByExpression is null, then there's no $orderby in the URI, so do nothing return document; } List orders = orderByExpression.getOrders(); // for multiple properties for ordering if(orders == null && orders.size() <= 0){ return document; } OrderExpression orderExpression = orders.get(0); // simple implementation. Better: use ExpressionVisitor CommonExpression commonExpression = orderExpression.getExpression(); if(! (commonExpression instanceof PropertyExpression)){ return document; } // the name of the property, which should be used for sorting (e.g. sort by company name) // example: for $orderby=NAME we'll have the string NAME in the variable sortPropertyName String sortProperyName = ((PropertyExpression)commonExpression).getPropertyName(); //For using standard sorter, we need an ArrayList instead of NodeList List<Element> companyList = new ArrayList<Element>(); // move all company nodes into the ArrayList Node entitySetNode = document.getFirstChild(); NodeList companiesNodeList = entitySetNode.getChildNodes(); for(int i = 0; i < companiesNodeList.getLength(); i++){ Node companyNode = companiesNodeList.item(i); if(companyNode.getNodeType() == Node.ELEMENT_NODE){ Element companyElement = (Element)companyNode; // populate the ArrayList companyList.add(companyElement); } } // now do the sorting for the ArrayList Collections.sort(companyList, new Comparator<Element>(){ public int compare( Element element1, Element element2) { int compareResult = 0; String value1 = element1.getElementsByTagName(sortProperyName).item(0).getFirstChild().getNodeValue(); String value2 = element2.getElementsByTagName(sortProperyName).item(0).getFirstChild().getNodeValue(); // we need special treatment for the ID, which is of type string, but for correct order we need the number // the string comparer rates 5 higher as 46 if(sortProperyName.equals("ID")){ Integer integerValue1 = new Integer(value1); Integer integerValue2 = new Integer(value2); // we delegate the sorting to the native sorter of the Integer class compareResult = integerValue1.compareTo(integerValue2); }else{ // we delegate the sorting to the native sorter of the java.lang.String class compareResult = value1.compareTo(value2); } // if 'desc' is specified in the URI, change the order of the list SortOrder sortOrder = orderExpression.getSortOrder(); if (sortOrder.equals(SortOrder.desc)){ return - compareResult; // just convert the result to negative value to change the order } return compareResult; } }); // now that the sorting is done, convert the (sorted) ArrayList back to NodeList // first create a new entitySet node Element newEmptyEntitySetNode = document.createElement("Companies"); // then add all child nodes Iterator iterator = companyList.iterator(); while (iterator.hasNext()) { Element companyElement = (Element)iterator.next(); newEmptyEntitySetNode.appendChild(companyElement); } // then replace the old entitySet node with the new node (containing the sorted children) document.replaceChild(newEmptyEntitySetNode, entitySetNode); //Finally, return the document with sorted child nodes return document; }