Olingo (http://olingo.apache.org) is a framework that implements the OData specification. Its provides support for its latest version, the version 4, for both client and server sides. We will describe here how to use the client side to retrieve data.
Configure Olingo in the project
The simplest way to configure the Olingo client to interact with OData v4 services is to use Maven and define the client as a dependency in the file pom.xml
, as described below:
<?xml version="1.0" encoding="UTF-8"?>
<project (...)>
<modelVersion>4.0.0</modelVersion>
(...)
<dependencies>
<dependency>
<groupId>org.apache.olingo</groupId>
<artifactId>odata-client-core</artifactId>
<version>4.0.0-beta-01</version>
</dependency>
</dependencies>
(...)
</project>
Maven can be used then to generate the configuration for your IDE. For example, for Eclipse, simply execute the following command:
mvn eclipse:eclipse
Now we have a configured project, lets start to implement the processing. We will focus here on the way to retrieve data. We will base on the online service Northwind. We first describe the general frame to build a request. We can then go further and build more complex requests and describe how to navigate into data.
Getting an instance of the client
Getting the Olingo client instance is pretty simple using the factory class ODataClientFactory
, as described below:
ODataClient client = ODataClientFactory.getV4();
On our case, we choose a OData v4 compliant client. This instance will be used to execute requests and interact with the OData service.
Getting service metadata
OData provides service metadata to determine which data can be used for the service. Olingo provides the class ODataServiceDocumentRequest
, ODataRetrieveResponse
to execute a request against the service document. The following code describes how to use them:
String serviceRoot = "http://services.odata.org/V4/Northwind/Northwind.svc";
ODataServiceDocumentRequest req =
client.getRetrieveRequestFactory().getServiceDocumentRequest(serviceRoot);
ODataRetrieveResponse<ODataServiceDocument> res = req.execute();
From this response, we can get an instance of class ODataServiceDocument
to actually get the metadata, as described below:
ODataServiceDocument serviceDocument = res.getBody();
Collection<String> entitySetNames = serviceDocument.getEntitySetNames();
Map<String,URI> entitySets = serviceDocument.getEntitySets();
Map<String,URI> singletons = serviceDocument.getSingletons();
Map<String,URI> functionImports = serviceDocument.getFunctionImports();
URI productsUri = serviceDocument.getEntitySetURI("Products");
At this level, we can only have access to elements provided and their URIs but we can have details of their structures. If we want to do a deeper introspection, we need to use a metadata request, as described below:
String serviceRoot = "http://services.odata.org/V4/Northwind/Northwind.svc";
EdmMetadataRequest request
= client.getRetrieveRequestFactory().getMetadataRequest(serviceRoot);
ODataRetrieveResponse<Edm> response = request.execute();
Fomr the response, we get an instance of the class Edm
that contains all the metadata. We can first begin to get the schemas, get all contained elements. Lets see below how to implement this:
Edm edm = response.getBody();
List<EdmSchema> schemas = edm.getSchemas();
for (EdmSchema schema : schemas) {
String namespace = schema.getNamespace();
for (EdmComplexType complexType : schema.getComplexTypes()) {
FullQualifiedName name = complexType.getFullQualifiedName();
}
for (EdmEntityType entityType : schema.getEntityTypes()) {
FullQualifiedName name = entityType.getFullQualifiedName();
}
}
We can get now details about the element itself. The following code gets details of the entity Customers
:
EdmEntityType customerType = edm.getEntityType(
new FullQualifiedName("NorthwindModel", "Customer"));
List<String> propertyNames = customerType.getPropertyNames();
for (String propertyName : propertyNames) {
EdmProperty property = customerType.getStructuralProperty(propertyName);
FullQualifiedName typeName = property.getType().getFullQualifiedName();
}
Now we have inspected the metadata of the service, we can actually get the corresponding data.
Getting data
Getting data with Olingo consists in several steps:
- Build the request URI
- Execute the request
- Browse the received data
For the next step, Olingo provides the class URIBuilder
that allows to build a OData v4 compliant URI for data we want to get. This class supports all features of OData v4. We leverage it all along the post to build our queries. Following code describes how to use it to create an URI to get all customers:
String serviceRoot = "http://services.odata.org/V4/Northwind/Northwind.svc";
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers").build();
Now we have the URI, we can execute the request using the client instance. Olingo gives access then to an iterator to browse the received data. Following code describes how to get data from this instance:
ODataRetrieveResponse<ODataEntitySetIterator<ODataEntitySet, ODataEntity>> response
= client.getRetrieveRequestFactory().getEntitySetIteratorRequest(customersUri).execute();
ODataEntitySetIterator<ODataEntitySet, ODataEntity> iterator = response.getBody();
Now we received the data, we can browse them based on the iterator. The latter contains elements of type ODataEntity
. Each element has the following
- Properties: the properties of entities
- Navigation links: the links that allow to navigate through the data graph from these entities. This includes inline entities and entity sets
- Association links: the navigation links
- Operations: the operations that can be executed on entities
Following code describes how to iterate over customers and get their properties:
while (iterator.hasNext()) {
ODataEntity customer = iterator.next();
List<ODataProperty> properties = customer.getProperties();
for (ODataProperty property : properties) {
String name = property.getName();
ODataValue value = property.getValue();
String valueType = value.getTypeName();
(...)
}
}
We will describe navigation links below in the section .
Controlling retrieved data
The class URIBuilder allows to specify the subset of properties to get for entities with its method select
. In the following code, we only want to get the properties PersonID
, CompanyName
and ContactName
:
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.select("PersonID,CompanyName,ContactName").build();
Counting elements and handling pagination
The method count
of the class URIBuilder
specifies that the request corresponds to a count of elements. Building an URI for a count request is described below:
URI customersCountUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.count(true).build();
Getting the count result can be simply done on the body of the response since the class ODataEntitySet
provides a method getCount
, as described below:
ODataEntitySet entitySet = req.execute().getBody();
int count = entitySet.getCount();
In the context of pagination, the number of elements is useful to determine the number of pages for the result. We can then play with arguments top
for the maximum number of elements to retrieve per request and skip
to specify the number of elements to be skipped at the beginning of result. For example, to retrieve the third page of data, we can simply
int page = 3;
int numberOfElementsPerPage = 10;
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.top((page-1)*numberOfElementsPerPage).skip(10).build();
We can also notice the method skipToken
to get elements with identifier greater than the value specified. An example of use is described below:
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.skipToken("5").build();
Ordering data
Ordering data is simply based on the method orderBy
of the class URIBuilder
, as described below. We can notice that its possible to possible to specify if we want to order ascending or descending.
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.orderBy("PersonID desc").build();
Browsing the data graph with links
OData supports data graph and handle relationships between elements. By default, only the first level of the graph are retrieved. If relationships are present, the elements contain references to other levels.
Following code describes how to get to levels of the data graph with two requests.
- The first one for the customers
- The second one for the orders linked to customers
Following code describes how to follow navigation links in order to get corresponding data:
ODataEntity customer = iterator.next();
List<ODataLink> links = customer.getNavigationLinks();
for (ODataLink link : links) {
URI linkUri = client.newURIBuilder(serviceRoot)
.appendNavigationSegment(link.getLink().toString()).build();
ODataRetrieveResponse<ODataEntitySetIterator<ODataEntitySet, ODataEntity>> responseOrders
= client.getRetrieveRequestFactory().getEntitySetIteratorRequest(linkUri).execute();
ODataEntitySetIterator<ODataEntitySet, ODataEntity> iteratorOrders = responseOrders.getBody();
while (iteratorOrders.hasNext()) {
ODataEntity order = iteratorOrders.next();
List<ODataProperty> propertiesOrder = order.getProperties();
for (ODataProperty propertyOrder : propertiesOrder) {
ODataValue value = propertyOrder.getValue();
(...)
}
(...)
}
}
For information, the value contained in the link is Customers('ANATR')/Orders
.
Its possible to do the same thing in only one request. This can be configured using the method expand
of the class URIBuilder
. Its parameter defines the name of the field to load eagerly.
In this context, Olingo provides methods expand
and expandWithSelect
on the class URIBuilder
. They allow to specify the properties to load in the same request, as described below:
URI customersWithOrdersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.select("PersonID,Orders").expand("Orders").build();
Now we saw how to configure the request, lets have Ć look at the way to extract corresponding data from rachat element in the response body. In such case, the link becomes . This means that the data are there and can be directly gotten. To do this, simply cast the instance of OdataLink
to ODataInlineEntitySet
. This gives us access to the corresponding entry set and the list of entities itself.
ODataEntity customer = iterator.next();
List<ODataLink> links = customer.getNavigationLinks();
for (ODataLink link : links) {
ODataInlineEntitySet inlineLink = (ODataInlineEntitySet) link;
CommonODataEntitySet entitySet = inlineLink.getEntitySet();
List<ODataEntity> entities = (List<ODataEntity>) entitySet.getEntities();
for (ODataEntity entity : entities) {
List<ODataProperty> entityProperties = entity.getProperties();
for (ODataProperty property : entityProperties) {
ODataValue value = property.getValue();
(...)
}
(...)
}
}
The previous sample describes the use of a link with multiple cardinality. Such mechanism is also usable with cardinality of 1. The main difference is the way to get corresponding data. As a matter of fact, we dont retrieve an entity set in this case, but directly an entity. We need to cast to the class ODataInlineEntity
, as described below:
List<ODataLink> links = customer.getNavigationLinks();
for (ODataLink link : links) {
ODataInlineEntity inlineLink = (ODataInlineEntity) navigationLink;
CommonODataEntity entity = inlineLink.getEntity();
List<ODataProperty> entityProperties
= (List<ODataProperty>) entity.getProperties();
for (ODataProperty property : entityProperties) {
ODataValue value = property.getValue();
(...)
}
(...)
}
Loading data by identifier
We can also get an element by its identifier rather than finding it from a query. In the case, we need to use the method appendKeySegment
of the class URIBuilder
, as described below:
URI customerUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.appendKeySegment(''ERNSH").build();
In this context, we get only one element and not a list of elements. We need to adapt the request execution to specify that we expect only one element. We use in this case the method getEntityRequest
as described below. The body of the response is the element itself.
ODataRetrieveResponse<ODataEntity> response =
client.getRetrieveRequestFactory().getEntityRequest(customersUri).execute();
ODataEntity customer = response.getBody();
Searching OData v4 data
OData provides the parameter $filter
allows to execute query on data. The method filter
of the class URIBuilder
accepts as parameter the expression to evaluate, as described below:
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.filter("CustomerID eq 'ERNSH' or CustomerID eq 'ALFKI'")
.build();
We dont describe here all the expression capabilities supported for the filter expression but such expressions support operators (logical and comparison, arithmetic, lambda), canonical functions, path exdpressions and grouping. This allows to build complex search expressions.
For more details, see the section > in the part 2 of the OData v4 specification.
Another way to do search with OData consists in using the parameter $search
allows to implement free-text search. The method search
of the class URIBuilder
used jointly with the class SearchFactory
allows to use this feature, as described below:
URI customersUri = client
.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers")
.search(client.getSearchFactory().or(
client.getSearchFactory().literal("blue"),
client.getSearchFactory().literal("green"))
).build();
It is up to the service to decide what makes a product blue
or green
. For this reason, this parameter isnt always supported by services.
Controlling data format
OData v4 supports two format to exchange data: JSON (the default one) and Atom. Olingo allows to control which format is used under the hood. There are two ways to do this:
- The first one leverages the parameter
$format
of OData - The second one is based on the HTTP header
Accept
.
The difference between these two approaches is that the second one goes further and also allows to control the metadata exhanged.
The first approach uses the method format
of the class URIBuilder
, as described below:
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers").format("atom").build();
The second approach is based on the class ODataFormat
and the method setFormat
of the request instance. The class defines a set of constant to specify what we actually expect regarding the response content (format and level of metadata in the case of JSON). Following code describes how to implement such approach:
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers").build();
ODataRetrieveRequest<ODataEntitySetIterator<ODataEntitySet, ODataEntity>> request
= client.getRetrieveRequestFactory().getEntitySetIteratorRequest(customersUri);
//request.setFormat(ODataFormat.JSON_NO_METADATA);
request.setFormat(ODataFormat.ATOM);
ODataRetrieveResponse<ODataEntitySetIterator<ODataEntitySet, ODataEntity>> response
= request.execute();
To finish, lets have a look at the way to handle errors that can occur requests.
Handle errors
When something wrong happens during a call, the Olingo client will throw an exception of type ODataClientErrorException
. We simply need to catch it and handle the corresponding error. In the following sample, the entity Customers1
doesnt exist:
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment("Customers1").build();
try {
ODataRetrieveResponse<ODataEntitySetIterator<ODataEntitySet, ODataEntity>> response
= client.getRetrieveRequestFactory().getEntitySetIteratorRequest(customersUri).execute();
(...)
} catch(ODataClientErrorException ex) {
ex.printStackTrace();
}
The exception contains the following message:
org.apache.olingo.client.api.communication.ODataClientErrorException: Resource not found for
the segment 'Customers1'. [HTTP/1.1 404 Not Found]
We also have the ability to check the status code returned and use it to handle errors:
ODataRetrieveResponse<ODataEntitySetIterator<ODataEntitySet, ODataEntity>> response
= client.getRetrieveRequestFactory().getEntitySetIteratorRequest(customersUri).execute();
if (response.getStatusCode() >= 400) {
// Handle error
}
Pingback: Manipulating data of OData v4 services with Olingo | Sandbox for the Web stack
Very good explanation! Works fine for me!!!!
Great! I’m pleased to hear that š
Great tutorial ! It’s very helpfull :-)! However I have one question. How can I use “expandWithQuery” method to build a request and make “imbricated expand” ? I’m facing some issue during execution that I don’t understand …
Thanks vey much for your feedback!
I think that you look for this:
Map options = new HashMap();
options.put(QueryOption.FILTER, “ShipName eq ‘Alfreds Futterkiste'”);
URI customersUri = client.newURIBuilder(serviceRoot)
.appendEntitySetSegment(“Customers”)
.select(“CustomerID,Orders”)
.expandWithOptions(“Orders”, options).build();
This allows to build something as described in section “Nested Filter in Expand” of the link http://www.odata.org/getting-started/basic-tutorial/.
Thanks very much! That’s exactly what I’m looking for but I need to do this:
options.put(QueryOption.EXPAND, ā….ā);
And then my application crashes … š¦
Any help ?