We saw in previous post that Olingo can be used as a client to access existing OData services. The tool also provides the ability to implement custom OData services with Java. We will focus in this post on the way to do that. Moreover this post aims to provide first insights about the way to use Olingo to implement OData services.
Since the subject is a bit wide, we will only deal with the way to manage entities and query them.
Configuring Olingo in the project
The simplest way to configure the Olingo client to implement an OData v4 service is to use Maven and define the server 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-server-core</artifactId>
<version>4.0.0-beta-02</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 implement a simple service that implement entity CRUD (create, retrieve, upate and delete).
Creating the entry point servlet
Olingo provides out of the box a request handler that is based on the servlet technology. For that reason, an entry point servlet must be implemented to configure an Olingo application and delegate request processing to the OData handler.
We will describe first the global skeleton of an Olingo entry point servlet. We will focus then on each part of its processing.
public class OlingoEntryPointServlet extends HttpServlet {
private EdmProvider edmProvider;
private List<Processor> odataProcessors;
(...)
@Override
public void init(ServletConfig config) throws ServletException {
super.init();
this.edmProvider = createEdmProvider();
this.odataProcessors = getODataProcessors(edmProvider);
}
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
try {
doService(request, response);
} catch (RuntimeException e) {
throw new ServletException(e);
}
}
private void doService(HttpServletRequest request,
HttpServletResponse response) {
OData odata = createOData();
ServiceMetadata serviceMetadata = createServiceMetadata(
odata, edmProvider);
ODataHttpHandler handler = createHandler(
odata, serviceMetadata, odataProcessors);
handler.process(request, response);
}
}
The first thing the servlet needs is an instance of OData that corresponds to the root object for serving factory tasks and support loose coupling of implementation (core) from the API. This instance must be dedicated to serve a request and cant be shared by multiple threads. So we need to be careful when using it within a servlet that is a singleton by default. The class allows to get an instance using its method newInstance
, as described below:
private OData createOData() {
return OData.newInstance();
}
We need then to create the service metadata based on an EDM provider that will be used then to create the request handler, as described below:
private ServiceMetadata createServiceMetadata(
OData odata, EdmProvider edmProvider) {
EdmxReference reference = new EdmxReference(
URI.create("../v4.0/cs02/vocabularies/Org.OData.Core.V1.xml"));
reference.addInclude(new EdmxReferenceInclude(
"Org.OData.Core.V1", "Core"));
List<EdmxReference> references = Arrays.asList(reference);
return odata.createServiceMetadata(edmProvider, references);
}
The last thing we need to create to serve a request consists in the handler itself. Its used to handle the OData requests and is based on a set of processors. Its responsible to select the right registered processor and delegate then the processing. The create of an handler leverage the method createHandler
of the class OData
. We can notice that a specific handler must be create per request. The following content describes how to create one:
private ODataHttpHandler createHandler(
OData odata, ServiceMetadata serviceMetadata,
List<Processor> odataProcessors) {
ODataHttpHandler dataHandler = odata.createHandler(serviceMetadata);
if (odataProcessors!=null) {
for (Processor odataProcessor : odataProcessors) {
dataHandler.register(odataProcessor);
}
}
return dataHandler;
}
Now we have the entry point servlet implemented, we can focus on the definition of the data structure we will use for our OData service.
Defining data structure
Before bring able to handle requests, we need to define the data model (EDM or Entity Data Model) of our service and which endpoints allow to interact with it.
Methods of class EdmProvider
To do that, we need to create a class that extends the class EdmProvider
of Olingo and override some of its methods following your needs.
For the scope of this post, only few of them need to be overriden, as described below:
- Method
getSchemas
– Get the list of schemas and the elements (entity types, complex types, ) they contain. This method will be used to display data of the metadata URL (for example,/odata.svc/$metadata
). - Method
getEntityContainer
– Get the different entity sets. This method will be used to display data of the service URL (for example,/odata.svc
). - Method
getEntityType
– Get an entity type for a particularly full qualified name. - Method
getEntitySet
– Get the hints related to a specific entity set name. - Method
getEntityContainerInfo
Implementing a custom EdmProvider
The first step is to create programmatically the structure of our metadata (our EDM). We can use the classes provided by Olingo to do this, as described below:
// Schema
Schema schema = new Schema();
schema.setNamespace(namespace);
schema.setAlias(namespace);
schemas.add(schema);
// Entity types
List<EntityType> entityTypes = new ArrayList<EntityType>();
schema.setEntityTypes(entityTypes);
EntityType entityType = new EntityType().setName("MyEntityType");
complexTypes.add(complexType);
List<PropertyRef> pkRefs = new ArrayList<PropertyRef>();
PropertyRef ref = new PropertyRef().setPropertyName("pkName");
pkRefs.add(ref);
entityType.setKey(pkRefs);
Property property = new Property();
property.setName("myProperty");
property.setType(EdmPrimitiveTypeKind.String.getFullQualifiedName());
entityType.getProperties().add(property);
// Complex types (similar approach than for entity types)
List<ComplexType> complexTypes = new ArrayList<ComplexType>();
schema.setComplexTypes(complexTypes);
ComplexType complexType = new ComplexType().setName("MyComplexType");
complexTypes.add(complexType);
Property property = new Property();
property.setName(field.getName());
property.setType(field.getEdmType());
complexType.getProperties().add(property);
Now we have built the metadata for our EDM model, we can create our own implementation of the class EdmProvider
to leverage these metadata. A sample implementation is provided below. Setting the metadata can be done using its method setSchemas
.
public class EdmGenericProvider extends EdmProvider {
private String containerName = "default";
@Override
public List<Schema> getSchemas() throws ODataException {
return schemas;
}
@Override
public EntityContainer getEntityContainer() throws ODataException {
EntityContainer container = new EntityContainer();
container.setName(containerName);
// EntitySets
List<EntitySet> entitySets = new ArrayList<EntitySet>();
container.setEntitySets(entitySets);
// Load entity sets per index
for (Schema schema : schemas) {
for (EntitySet schemaEntitySet
: schema.getEntityContainer()
.getEntitySets()) {
EntitySet entitySet = new EntitySet().setName(
schemaEntitySet.getName()).setType(
new FullQualifiedName(
schemaEntitySet.getType().getNamespace(),
schemaEntitySet.getType().getName()));
entitySets.add(entitySet);
}
}
return container;
}
private Schema findSchema(String namespace) {
for (Schema schema : schemas) {
if (schema.getNamespace().equals(namespace)) {
return schema;
}
}
return null;
}
private EntityType findEntityType(Schema schema, String entityTypeName) {
for (EntityType entityType : schema.getEntityTypes()) {
if (entityType.getName().equals(entityTypeName)) {
return entityType;
}
}
return null;
}
@Override
public EntityType getEntityType(FullQualifiedName entityTypeName)
throws ODataException {
Schema schema = findSchema(entityTypeName.getNamespace());
return findEntityType(schema, entityTypeName.getName());
}
private ComplexType findComplexType(Schema schema, String complexTypeName) {
for (ComplexType complexType : schema.getComplexTypes()) {
if (complexType.getName().equals(complexTypeName)) {
return complexType;
}
}
return null;
}
@Override
public ComplexType getComplexType(FullQualifiedName complexTypeName)
throws ODataException {
Schema schema = findSchema(complexTypeName.getNamespace());
return findComplexType(schema, complexTypeName.getName());
}
private EntitySet findEntitySetInSchemas(String entitySetName)
throws ODataException {
List<Schema> schemas = getSchemas();
for (Schema schema : schemas) {
EntityContainer entityContainer = schema.getEntityContainer();
List<EntitySet> entitySets = entityContainer.getEntitySets();
for (EntitySet entitySet : entitySets) {
if (entitySet.getName().equals(entitySetName)) {
return entitySet;
}
}
}
return null;
}
@Override
public EntitySet getEntitySet(FullQualifiedName entityContainer,
String entitySetName) throws ODataException {
return findEntitySetInSchemas(entitySetName);
}
@Override
public EntityContainerInfo getEntityContainerInfo(
FullQualifiedName entityContainerName) throws ODataException {
EntityContainer container = getEntityContainer();
FullQualifiedName fqName = new FullQualifiedName(
container.getName(), container.getName());
EntityContainerInfo info = new EntityContainerInfo();
info.setContainerName(fqName);
return info;
}
public void setSchemas(List<Schema> schemas) {
this.schemas = schemas;
}
public void setContainerName(String containerName) {
this.containerName = containerName;
}
}
Handling requests
As we saw previously, OData requests are actually handled by processors within Olingo. A processor simply corresponds to a class that implements one or several processor interfaces of Olingo. When registering processors, Olingo will detect the kinds of requests it can handle based on such interfaces.
Here are the list of interfaces of Olingo that a processor can implement. They are all located under the package org.apache.olingo.server.api.processor
. Here is a list of all of them that are related to entities and properties. Additional ones focus actions, batch, delta, errors, service document and metadata.
- Interface
EntityCollectionProcessor
– Processor interface for handling a collection of entities, i.e. an entity set, with a particular entity type. - Interface
CountEntityCollectionProcessor
– Processor interface for handling counting a collection of entities. - Interface
EntityProcessor
– Processor interface for handling a single instance of an entity type. - Interface
PrimitiveProcessor
– Processor interface for handling an instance of a primitive type, i.e. primitive property of an entity. - Interface
PrimitiveValueProcessor
– Processor interface for getting value of an instance of a primitive type, e.g., a primitive property of an entity. - Interface
PrimitiveCollectionProcessor
– – Processor interface for handling a collection of primitive-type instances, i.e. a property of an entity defined as collection of primitive-type instances. - Interface
ComplexProcessor
– Processor interface for handling an instance of a complex type, i.e. a complex property of an entity. - Interface
ComplexCollectionProcessor
– Processor interface for handling a collection of complex-type instances, i.e. a property of an entity defined as collection of complex-type instances. - Interface
CountComplexCollectionProcessor
– Processor interface for handling counting a collection of complex properties, i.e. an EdmComplexType.
We will focus here on the way to handle entities. We will deal with other element kinds like primitive and navigation properties in an upcoming à venir post.
We already describe how to register a processor, so we can directly tackle the way to implement them.
Handling entities
Before being able to actually implement processing within processors, we need to know to create entities and how to get data from it programmatically using Olingo. We dont describe here how to handle navigation properties.
Building entities
When returning back data from the underlying store, we need to convert them to entities. The following code describes how to create an entity from data:
Entity entity = new EntityImpl();
// Add a primitive property
String primitiveFieldName = (...)
Object primitiveFieldValue = (...)
Property property = new PropertyImpl(null, primitiveFieldName,
ValueType.PRIMITIVE,
primitiveFieldValue);
entity.addProperty(property);
// Add a complex property
String complexFieldName = (...)
LinkedComplexValue complexValue = new LinkedComplexValueImpl();
List<Property> complexSubValues = complexValue.getValue();
String subPrimitiveFieldName = (...)
Object subPrimitiveFieldValue = (...)
Property complexSubProperty = new PropertyImpl(null, subPrimitiveFieldName,
ValueType.PRIMITIVE,
subPrimitiveFieldValue);
complexSubValues.add(complexSubProperty);
Property property = new PropertyImpl(null, complexFieldName,
ValueType.LINKED_COMPLEX, complexValue);
properties.add(property);
Extracting data from entities
In order to store data into a store, we need to extract received data from entity objects. The following code describes how fill maps from an entity and its properties.
public Map<String, Object> convertEntityToSource(Entity entity) {
Map<String, Object> source = new HashMap<String, Object>();
convertEntityPropertiesToSource(source, entity.getProperties());
return source;
}
private void convertEntityPropertiesToSource(
Map<String, Object> source,
List<Property> properties) {
for (Property property : properties) {
if (property.isComplex()) {
Map<String, Object> subSource = new HashMap<String, Object>();
convertEntityPropertiesToSource(subSource, property.asComplex());
source.put(property.getName(), subSource);
} else if (property.isPrimitive()) {
source.put(property.getName(), property.getValue());
}
}
}
Getting the EDM entity set for a request
Before being able to execute a request, we need to know on which EDM entity set it applies. This can be deduced from the URI resource paths. The implementation of the method getEdmEntitySet
below is very simple and return the hint from request:
private EdmEntitySet getEdmEntitySet(
UriInfoResource uriInfo)
throws ODataApplicationException {
List<UriResource> resourcePaths = uriInfo.getUriResourceParts();
UriResourceEntitySet uriResource
= (UriResourceEntitySet) resourcePaths.get(0);
return uriResource.getEntitySet();
}
Getting the primary key values
OData allows to specify the primary key of an entity directly within the URL like this /products('my id')
or /products(pk1='my composite id1',pk2='my composite id2')
. The following method getPrimaryKeyValues
retrieve the set of values of the primary from path:
private Map<String, Object> getPrimaryKeys(
UriResourceEntitySet resourceEntitySet) {
List<UriParameter> uriParameters = resourceEntitySet.getKeyPredicates();
Map<String, Object> primaryKeys = new HashMap<String, Object>();
for (UriParameter uriParameter : uriParameters) {
UriParameter key = keys.get(0);
String primaryKeyName = key.getName();
Object primaryKeyValue = getValueFromText(key.getText());
primaryKeys.put(primaryKeyName, primaryKeyValue);
}
return primaryKeys;
}
Structure of an handling method
Methods of processors to handle OData requests follows similar structures:
- First they get the
EdmEntitySet
corresponding to the request based on theUriInfoResource
. This element allows to get hints of the structure of data we will manipulate. See section for more details. - We can eventually get hints from request headers like content type.
- We need then to get instance(s) to serialize and eventually deserialize payloads. Such instances can be obtained from the OData instance with methods
createSerializer
andcreateDeserializer
based on the content type we want to use. - In the case of entity adding or updating, we need to deserialize the received entity. We cam then extract data from entity to handle them. See section for more details.
- Mainly in the case of getting entity sets and entity set count, we can get options (system query parameters like $filter and $select) to parameterize processing.
- If the OData response must contain an entity or an entity set, we need to build it and then serialize it. See section for the way create an entity or a list of entities.
- During the serialization and if the metadata must be contained in the response (different than
ODataFormat.JSON_NO_METADATA
), we need to create the context URL. - Finally we need to set the response headers.
We are now ready to handle different requests to get and manage entities.
Getting list of entities
Getting such list must be implemented within the method readEntityCollection
of our processor.
public void readEntityCollection(ODataRequest request,
ODataResponse response, UriInfo uriInfo,
ContentType requestedContentType)
throws ODataApplicationException,
SerializerException {
EdmEntitySet edmEntitySet = getEdmEntitySet(
uriInfo.asUriInfoResource());
// Load entity set (list of entities) from store
EntitySet entitySet = (...)
ODataFormat format = ODataFormat
.fromContentType(requestedContentType);
ODataSerializer serializer = odata.createSerializer(format);
ExpandOption expand = uriInfo.getExpandOption();
SelectOption select = uriInfo.getSelectOption();
InputStream serializedContent = serializer
.entityCollection(
edmEntitySet.getEntityType(),
entitySet,
EntityCollectionSerializerOptions.with()
.contextURL(
format == ODataFormat.JSON_NO_METADATA ? null
: getContextUrl(serializer, edmEntitySet,
false, expand, select, null))
.count(uriInfo.getCountOption()).expand(expand)
.select(select).build());
response.setContent(serializedContent);
response.setStatusCode(HttpStatusCode.OK.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE,
requestedContentType.toContentTypeString());
}
We dont describe here the way to handle queries provided with the query parameter $filter
. For more details, we can refer to a previous post that describes the way implement them: https://templth.wordpress.com/2015/04/03/handling-odata-queries-with-elasticsearch/.
Adding an entity
Adding an entity must be implemented within the method createEntity
of our processor, as described below.
public void createEntity(ODataRequest request,
ODataResponse response,
UriInfo uriInfo,
ContentType requestFormat,
ContentType responseFormat)
throws ODataApplicationException,
DeserializerException,
SerializerException {
String contentType = request.getHeader(HttpHeader.CONTENT_TYPE);
if (contentType == null) {
throw new ODataApplicationException(
"The Content-Type HTTP header is missing.",
HttpStatusCode.BAD_REQUEST.getStatusCode(),
Locale.ROOT);
}
EdmEntitySet edmEntitySet = getEdmEntitySet(
uriInfo.asUriInfoResource());
ODataFormat format = ODataFormat.fromContentType(requestFormat);
ODataDeserializer deserializer = odata.createDeserializer(format);
Entity entity = deserializer.entity(request.getBody(),
edmEntitySet.getEntityType());
// Actually insert entity in store and get the result
// This is useful if identifier is autogenerated
Entity createdEntity = (...)
ODataSerializer serializer = odata.createSerializer(format);
ExpandOption expand = uriInfo.getExpandOption();
SelectOption select = uriInfo.getSelectOption();
InputStream serializedContent = serializer
.entity(edmEntitySet.getEntityType(),
createdEntity,
EntitySerializerOptions
.with()
.contextURL(
format == ODataFormat.JSON_NO_METADATA ? null
: getContextUrl(serializer, edmEntitySet, true,
expand, select, null))
.expand(expand).select(select).build());
response.setContent(serializedContent);
response.setStatusCode(
HttpStatusCode.CREATED.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE,
responseFormat.toContentTypeString());
}
Loading an entity
Loading an entity must be implemented within the method loadEntity
of our processor, as described below.
public void readEntity(ODataRequest request,
ODataResponse response,
UriInfo uriInfo,
ContentType requestedContentType)
throws ODataApplicationException,
SerializerException {
EdmEntitySet edmEntitySet = getEdmEntitySet(
uriInfo.asUriInfoResource());
UriResourceEntitySet resourceEntitySet
= (UriResourceEntitySet) uriInfo.getUriResourceParts().get(0);
// Get primary key(s)
Map<String, Object> primaryKeys = getPrimaryKeys(resourceEntitySet);
// Load entity from store
Entity entity = (...)
if (entity == null) {
throw new ODataApplicationException(
"No entity found for this key",
HttpStatusCode.NOT_FOUND.getStatusCode(),
Locale.ENGLISH);
}
ODataFormat format = ODataFormat
.fromContentType(requestedContentType);
ODataSerializer serializer = odata.createSerializer(format);
ExpandOption expand = uriInfo.getExpandOption();
SelectOption select = uriInfo.getSelectOption();
InputStream serializedContent = serializer
.entity(edmEntitySet.getEntityType(),
entity,
EntitySerializerOptions
.with()
.contextURL(
format == ODataFormat.JSON_NO_METADATA ? null
: getContextUrl(serializer,edmEntitySet, true,
expand, select, null))
.expand(expand).select(select).build());
response.setContent(serializedContent);
response.setStatusCode(HttpStatusCode.OK.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE,
requestedContentType.toContentTypeString());
}
Updating an entity
Updating an entity must be implemented within the method updateEntity
of our processor, as described below. We can check if the update must be partial or complete based on the HTTP method used for the call.
public void updateEntity(ODataRequest request,
ODataResponse response,
UriInfo uriInfo,
ContentType requestFormat,
ContentType responseFormat)
throws ODataApplicationException,
DeserializerException,
SerializerException {
String contentType = request.getHeader(HttpHeader.CONTENT_TYPE);
if (contentType == null) {
throw new ODataApplicationException(
"The Content-Type HTTP header is missing.",
HttpStatusCode.BAD_REQUEST.getStatusCode(),
Locale.ROOT);
}
EdmEntitySet edmEntitySet = getEdmEntitySet(
uriInfo.asUriInfoResource());
ODataFormat format = ODataFormat.fromContentType(requestFormat);
ODataDeserializer deserializer = odata.createDeserializer(format);
Entity entity = deserializer.entity(request.getBody(),
edmEntitySet.getEntityType());
// Get primary key(s)
Map<String, Object> primaryKeys = getPrimaryKeys(resourceEntitySet);
// Partial update?
boolean partial = request.getMethod().equals(HttpMethod.PATCH);
// Actually update entity in store
Entity updatedEntity = (...)
ODataSerializer serializer = odata.createSerializer(format);
ExpandOption expand = uriInfo.getExpandOption();
SelectOption select = uriInfo.getSelectOption();
InputStream serializedContent = serializer
.entity(edmEntitySet.getEntityType(),
updatedEntity,
EntitySerializerOptions
.with()
.contextURL(
format == ODataFormat.JSON_NO_METADATA ? null
: getContextUrl(serializer, edmEntitySet, true,
expand, select, null))
.expand(expand).select(select).build());
response.setContent(serializedContent);
response.setStatusCode(HttpStatusCode.OK.getStatusCode());
response.setHeader(HttpHeader.CONTENT_TYPE,
responseFormat.toContentTypeString());
}
Deleting an entity
Deleting an entity must be implemented within the method deleteEntity
of our processor, as described below.
public void deleteEntity(final ODataRequest request,
ODataResponse response, final UriInfo uriInfo)
throws ODataApplicationException {
EdmEntitySet edmEntitySet = getEdmEntitySet(
uriInfo.asUriInfoResource());
UriResourceEntitySet resourceEntitySet
= (UriResourceEntitySet) uriInfo.getUriResourceParts().get(0);
// Get primary key(s)
Map<String, Object> primaryKeys = getPrimaryKeys(resourceEntitySet);
// Actually delete the entity based on these parameters
(...)
response.setStatusCode(
HttpStatusCode.NO_CONTENT.getStatusCode());
}
Pingback: This week in API land, Berlin edition | Restlet - We Know About APIs