Implementing an OData service with Olingo

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 the UriInfoResource. 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 and createDeserializer 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());
}

Advertisements
This entry was posted in Olingo and tagged , , , . Bookmark the permalink.

One Response to Implementing an OData service with Olingo

  1. Pingback: This week in API land, Berlin edition | Restlet - We Know About APIs

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s