Skip to Content
Technical Articles
Author's profile photo Edrilan Berisha

Building an OData Service with a Spring-Boot Java Application using Olingo – Part II

So, in the last blog post, we created a health check endpoint in our Java application. In this one, we will introduce our entity relation model and build the OData service with help of the Olingo framework.

We have a simple entity relation model, which consists of three entities: Mother, Child, and Father. Obviously, a child can’t exist without a father and a mother. And both father and mother can have multiple children.

 

 

We define a couple of attributes in our mother entity, such as a technical ID, the name, and the age. The same attributes are maintained in the father entity. The child then receives the foreign key of the mother and father entities, and also a name.

So, we create an entity folder under the following path: src/main/java/com/github/olingo/example/entity

We create a java class for our mother entity, which will look like this:

package com.github.olingo.example.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;

@Entity
@Table(name="MOTHER")
public class Mother {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Mother mother = (Mother) o;
        return Objects.equals(id, mother.id) &&
                Objects.equals(name, mother.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

 

The annotations define two things: First, that this class will be an entity class, and, second, what will be the future name of the database table.

With the “@Id” annotation, we define which of our attributes will be our technical primary key on the database table. And with “@GeneratedValue”, we want to have the Id automatically generated for us every time we have a new instance of that entity.

The rest is pretty straightforward: We have getter and setter methods for all attributes and equals and hashCode method which will be generated for us by our IDE.

The entity class for our father entity looks pretty similar:

package com.github.olingo.example.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects;

@Entity
@Table(name="FATHER")
public class Father {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Father father = (Father) o;
        return Objects.equals(id, father.id) &&
                Objects.equals(name, father.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }

}

 

As for our child class, we have some parts which are different. So, let’s have a closer look.

As the child has two foreign keys that form a composite key together, we will also need to define a primary key class on top. Usually, these classes carry the “PK” abbreviation in the end.

So, we have a ChildPK.java class that looks like this:

package com.github.olingo.example.entity;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;

@Embeddable
public class ChildPK implements Serializable {

    @Column(name = "FATHER_ID", nullable = false)
    private Long fatherId;

    @Column(name = "MOTHER_ID", nullable = false)
    private Long motherId;

    public ChildPK() {
    }

    public  ChildPK(Father father, Mother mother) {
        this.fatherId = father.getId();
        this.motherId = mother.getId();
    }

    public Long getFatherId() {
        return fatherId;
    }

    public void setFatherId(Long fatherId) {
        this.fatherId = fatherId;
    }

    public Long getMotherId() {
        return motherId;
    }

    public void setMotherId(Long motherId) {
        this.motherId = motherId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ChildPK childPK = (ChildPK) o;
        return Objects.equals(fatherId, childPK.fatherId) &&
                Objects.equals(motherId, childPK.motherId);
    }

    @Override
    public int hashCode() {
        return Objects.hash(fatherId, motherId);
    }
}

 

So, it has both IDs from father and mother and that’s actually it. The rest are again just constructors, getter, setter, hashCode, and equals methods as in the other two entity classes before. What is important is to set this as our primary key class by using the “@Embeddable” annotation.

Now, let’s have a look at our child entity class:

package com.github.olingo.example.entity;

import javax.persistence.*;
import java.util.Objects;

@Entity
@Table(name="CHILD")
public class Child {
    @EmbeddedId
    private ChildPK childPK = new ChildPK();

    @ManyToOne(cascade = CascadeType.PERSIST)
    @MapsId("fatherId")
    private Father father;

    @ManyToOne(cascade = CascadeType.PERSIST)
    @MapsId("motherId")
    private Mother mother;

    private String name;

    public Child() {}

    public Child(Father father, Mother mother) {
        this.childPK = new ChildPK(father, mother);
    }

    public ChildPK getChildPK() {
        return childPK;
    }

    public void setChildPK(ChildPK childPK) {
        this.childPK = childPK;
    }

    public Father getFather() {
        return father;
    }

    public void setFather(Father father) {
        this.father = father;
    }

    public Mother getMother() {
        return mother;
    }

    public void setMother(Mother mother) {
        this.mother = mother;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Child child = (Child) o;
        return Objects.equals(childPK, child.childPK) &&
                Objects.equals(father, child.father) &&
                Objects.equals(mother, child.mother) &&
                Objects.equals(name, child.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(childPK, father, mother, name);
    }
}

 

Here, we have to define our “@EmbeddedId” on our primary key class that we have just created. As well as defining the relation of our entity to father and mother.

There are two ways of doing this: Unidirectional or bidirectional. In our case, we go for the unidirectional relation, which means we define the relation between child and mother or child and father just in our child entity class. We do so by using the following annotations:

@ManyToOne(cascade = CascadeType.PERSIST)

@MapsId(“motherId”)

So, with the “ManyToOne”, we define the relation and we can also define the cascading type, in case we want the child to be deleted as well if the father and mother instances were deleted.

With the “MapsId”, we map our private instance of these two classes to our composite primary key class. The rest of the coding should be already familiar to you by now.

So far, a solution of the implementation – can be found here.

The next step is to implement an OData service for our entities. For that, we create two other folders: a config folder which has our Jersey configuration, and a service folder where we can create later pre-processing or post-processing custom logic for different OData requests. In our service folder, we will create three new classes.

We start with:

src/main/java/com/github/olingo/example/service/OdataJpaServiceFactory.java

package com.github.olingo.example.service;

import org.springframework.stereotype.Component;

@Component
public class OdataJpaServiceFactory extends CustomODataServiceFactory{
    //need this wrapper class for the spring framework, otherwise we face issues when auto wiring directly the CustomODataServiceFactory
}

 

This class extends “CustomODataServiceFactory” and is also tagged with the “@Component” annotation for our spring framework.

Now, we have a look on the CustomODataServiceFactory:

src/main/java/com/github/olingo/example/service/CustomODataServiceFactory.java

 

package com.github.olingo.example.service;

import com.github.olingo.example.config.JerseyConfig;
import org.apache.olingo.odata2.api.ODataService;
import org.apache.olingo.odata2.api.ODataServiceFactory;
import org.apache.olingo.odata2.api.edm.provider.EdmProvider;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.processor.ODataContext;
import org.apache.olingo.odata2.api.processor.ODataSingleProcessor;
import org.apache.olingo.odata2.jpa.processor.api.ODataJPAContext;
import org.apache.olingo.odata2.jpa.processor.api.exception.ODataJPARuntimeException;
import org.apache.olingo.odata2.jpa.processor.api.factory.ODataJPAAccessFactory;
import org.apache.olingo.odata2.jpa.processor.api.factory.ODataJPAFactory;

import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;

public class CustomODataServiceFactory extends ODataServiceFactory {

    private ODataJPAContext oDataJPAContext;
    private ODataContext oDataContext;


    @Override
    public final ODataService createService(final ODataContext context) throws ODataException {
        oDataContext = context;
        oDataJPAContext = initializeODataJPAContext();

        validatePreConditions();

        ODataJPAFactory factory = ODataJPAFactory.createFactory();
        ODataJPAAccessFactory accessFactory = factory.getODataJPAAccessFactory();

        if (oDataJPAContext.getODataContext() == null) {
            oDataJPAContext.setODataContext(context);
        }

        ODataSingleProcessor oDataSingleProcessor = new CustomODataJpaProcessor(
                oDataJPAContext);


        EdmProvider edmProvider = accessFactory.createJPAEdmProvider(oDataJPAContext);
        return createODataSingleProcessorService(edmProvider, oDataSingleProcessor);

    }

    private void validatePreConditions() throws ODataJPARuntimeException {
        if (oDataJPAContext.getEntityManager() == null) {
            throw ODataJPARuntimeException.throwException(ODataJPARuntimeException.ENTITY_MANAGER_NOT_INITIALIZED, null);
        }
    }

    public final ODataJPAContext getODataJPAContext()
            throws ODataJPARuntimeException {
        if (oDataJPAContext == null) {
            oDataJPAContext = ODataJPAFactory.createFactory()
                    .getODataJPAAccessFactory().createODataJPAContext();
        }
        if (oDataContext != null)
            oDataJPAContext.setODataContext(oDataContext);
        return oDataJPAContext;

    }

    protected ODataJPAContext initializeODataJPAContext() throws ODataJPARuntimeException {
        ODataJPAContext oDataJPAContext = this.getODataJPAContext();
        ODataContext oDataContext = oDataJPAContext.getODataContext();


        HttpServletRequest request = (HttpServletRequest) oDataContext.getParameter(
                ODataContext.HTTP_SERVLET_REQUEST_OBJECT);
        EntityManager entityManager = (EntityManager) request
                .getAttribute(JerseyConfig.EntityManagerFilter.EM_REQUEST_ATTRIBUTE);
        oDataJPAContext.setEntityManager(entityManager);


        oDataJPAContext.setPersistenceUnitName("default");
        oDataJPAContext.setContainerManaged(true);
        return oDataJPAContext;
    }
}

 

An important component for the Olingo framework to serve data from the JPA framework is an implementation of a class which extends the “ODataServiceFactory”. Imagine this as our adapter between our OData service and the JPA.

The Olingo framework calls the “initializeJPAContext()” to get a new “ODataJPAContext” which is used in every OData request. To get a plain instance, we need to use the “getODataJPAContext()” method from the base class. With this, we can create some custom logic later on.

Now, we can prepare the class for our custom logic in the “CustomODataJpaProcessor“.

src/main/java/com/github/olingo/example/service/CustomODataJpaProcessor.java

package com.github.olingo.example.service;

import org.apache.olingo.odata2.api.edm.EdmException;
import org.apache.olingo.odata2.api.ep.EntityProviderException;
import org.apache.olingo.odata2.api.exception.ODataException;
import org.apache.olingo.odata2.api.exception.ODataNotFoundException;
import org.apache.olingo.odata2.api.processor.ODataResponse;
import org.apache.olingo.odata2.api.uri.info.*;
import org.apache.olingo.odata2.jpa.processor.api.ODataJPAContext;
import org.apache.olingo.odata2.jpa.processor.api.ODataJPADefaultProcessor;
import org.apache.olingo.odata2.jpa.processor.api.exception.ODataJPAModelException;
import org.apache.olingo.odata2.jpa.processor.api.exception.ODataJPARuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.InputStream;
import java.util.List;

public class CustomODataJpaProcessor extends ODataJPADefaultProcessor {

    private Logger logger = LoggerFactory.getLogger(getClass());

    public CustomODataJpaProcessor(ODataJPAContext oDataJPAContext) {
        super(oDataJPAContext);

    }

    @Override
    public ODataResponse readEntitySet(final GetEntitySetUriInfo uriParserResultView, final String contentType) throws ODataJPAModelException, ODataJPARuntimeException, EdmException {
        logger.info("READ: Entity Set {} called", uriParserResultView.getTargetEntitySet().getName());
        try {
            List<Object> jpaEntities = jpaProcessor.process(uriParserResultView);
            return responseBuilder.build(uriParserResultView, jpaEntities, contentType);
        } finally {
            this.close();
        }
    }

    @Override
    public ODataResponse readEntity(final GetEntityUriInfo uriParserResultView, final String contentType) throws ODataJPAModelException, ODataJPARuntimeException, ODataNotFoundException, EdmException {
        ODataResponse response = null;
        if (uriParserResultView.getKeyPredicates().size() > 1) {
            logger.info("READ: Entity {} called with key {} and key {}", uriParserResultView.getTargetEntitySet().getName(), uriParserResultView.getKeyPredicates().get(0).getLiteral(), uriParserResultView.getKeyPredicates().get(1).getLiteral());
        } else {
            logger.info("READ: Entity {} called with key {}", uriParserResultView.getTargetEntitySet().getName(), uriParserResultView.getKeyPredicates().get(0).getLiteral());
        }
        try {
            Object readEntity = jpaProcessor.process(uriParserResultView);
            response = responseBuilder.build(uriParserResultView, readEntity, contentType);
        } finally {
            this.close();
        }
        return response;
    }

    @Override
    public ODataResponse createEntity(final PostUriInfo uriParserResultView, final InputStream content, final String requestContentType, final String contentType) throws ODataJPAModelException, ODataJPARuntimeException, ODataNotFoundException, EdmException, EntityProviderException {
        logger.info("POST: Entity {} called", uriParserResultView.getTargetEntitySet().getName());
        ODataResponse response = null;
        try {
            Object createdEntity = jpaProcessor.process(uriParserResultView, content, requestContentType);
            response = responseBuilder.build(uriParserResultView, createdEntity, contentType);
        } finally {
            this.close();
        }
        return response;
    }



    @Override
    public ODataResponse updateEntity(final PutMergePatchUriInfo uriParserResultView, final InputStream content,
                                      final String requestContentType, final boolean merge, final String contentType) throws ODataException, ODataJPAModelException, ODataJPARuntimeException, ODataNotFoundException {
        logger.info("PUT: Entity {} called with key {}", uriParserResultView.getTargetEntitySet().getName(), uriParserResultView.getKeyPredicates().get(0).getLiteral());
        ODataResponse response = null;
        try {
            Object updatedEntity = jpaProcessor.process(uriParserResultView, content, requestContentType);
            response = responseBuilder.build(uriParserResultView, updatedEntity);
        } finally {
            this.close();
        }
        return response;
    }

    @Override
    public ODataResponse deleteEntity(DeleteUriInfo uriParserResultView, String contentType) throws ODataException {
        logger.info("DELETE: Entity {} called with key {}", uriParserResultView.getTargetEntitySet().getName(), uriParserResultView.getKeyPredicates().get(0).getLiteral());
        ODataResponse oDataResponse = null;
        try {
            this.oDataJPAContext.setODataContext(this.getContext());
            Object deletedEntity = this.jpaProcessor.process(uriParserResultView, contentType);
            oDataResponse = this.responseBuilder.build(uriParserResultView, deletedEntity);
        } finally {
            this.close();
        }
        return oDataResponse;
    }

}

 

Here, we override a couple of OData actions, which are relevant for us, such as the “readEntitySet” to fetch all entities. The single “readEntity”, the “createEntity”, “updateEntity”, and “deleteEntity”. We extend these methods with some logging functionality so that we can track all requests later in our application log. (Please make sure yourself that you use the latest version of the Log4j framework, this blog was written already quite some time ago)

Let’s turn our attention to our JerseyConfig class under following path:

src/main/java/com/github/olingo/example/config/JerseyConfig.java

The coding should consist of:

package com.github.olingo.example.config;

import com.github.olingo.example.service.OdataJpaServiceFactory;
import org.apache.olingo.odata2.api.ODataServiceFactory;
import org.apache.olingo.odata2.core.rest.ODataRootLocator;
import org.apache.olingo.odata2.core.rest.app.ODataApplication;
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.Path;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import java.io.IOException;

@Component
@ApplicationPath("/odata")
public class JerseyConfig extends ResourceConfig {

    public JerseyConfig(OdataJpaServiceFactory serviceFactory, EntityManagerFactory entityManagerFactory) {
        ODataApplication oDataApplication = new ODataApplication();
        oDataApplication
                .getClasses()
                .forEach( c -> {
                    if ( !ODataRootLocator.class.isAssignableFrom(c)) {
                        register(c);
                    }
                });
        register(new ODataServiceRootLocator(serviceFactory));
        register(new EntityManagerFilter(entityManagerFactory));
    }

    @Path("/")
    public static class ODataServiceRootLocator extends ODataRootLocator {

        private OdataJpaServiceFactory serviceFactory;

        @Inject
        public ODataServiceRootLocator (OdataJpaServiceFactory serviceFactory) {
            this.serviceFactory = serviceFactory;
        }

        @Override
        public ODataServiceFactory getServiceFactory() {
            return this.serviceFactory;
        }
    }

    @Provider
    public static class EntityManagerFilter implements ContainerRequestFilter,
            ContainerResponseFilter {
        public static final String EM_REQUEST_ATTRIBUTE =
                EntityManagerFilter.class.getName() + "_ENTITY_MANAGER";
        private final EntityManagerFactory entityManagerFactory;

        @Context
        private HttpServletRequest httpRequest;
        public EntityManagerFilter(EntityManagerFactory entityManagerFactory) {
            this.entityManagerFactory = entityManagerFactory;
        }

        @Override
        public void filter(ContainerRequestContext containerRequestContext) throws IOException {
            EntityManager entityManager = this.entityManagerFactory.createEntityManager();
            httpRequest.setAttribute(EM_REQUEST_ATTRIBUTE, entityManager);
            if (!"GET".equalsIgnoreCase(containerRequestContext.getMethod())) {
                entityManager.getTransaction().begin();
            }
        }
        @Override
        public void filter(ContainerRequestContext requestContext,
                           ContainerResponseContext responseContext) throws IOException {
            EntityManager entityManager = (EntityManager) httpRequest.getAttribute(EM_REQUEST_ATTRIBUTE);
            if (!"GET".equalsIgnoreCase(requestContext.getMethod())) {
                EntityTransaction entityTransaction = entityManager.getTransaction(); //we do not commit because it's just a READ
                if (entityTransaction.isActive() && !entityTransaction.getRollbackOnly()) {
                    entityTransaction.commit();
                }
            }
            entityManager.close();
        }
    }

}

 

It is important to know that we have to define this in our Spring application as a component. Therefore, we use the “@Component” annotation.

We need to register our ServiceFactory with Olingo’s runtime and to register Olingo’s entry point with the JAX-RS runtime. We do this by extending the „ResourceConfig“ class. With “@ApplicationPath(“/odata”)”, we define the endpoint of our OData service.

The “EntityManagerFilter” injects an “EntityManager” in the actual request. This way, it is available in the “ServiceFactory”. It implements both “ContainerRequestFilter” and “ContainerResponseFilter” interfaces.

The filter method with only one argument is called at the start of a resource request. It uses the provided „EntityManagerFactory“ to create a new instance of it, that is then stored under an attribute so it can be used by the „ServiceFactory“ later. We ignore the GET requests since they should not have any side effects in our case.

The other filter method is called after Olingo has finished processing the request. Here, we also check the request method, and we commit the transaction if required.

As last time, we can now build this application locally with the help of Maven by typing the following command into the terminal: ‘mvn spring-boot:run’.

Then, we open Postman and call the “localhost:8080/odata/$metadata” url which will return a 200 HTTP response and show you all entity sets our OData service provides:

 

In case you are facing issues with your project and you cannot figure out why, feel free to clone the solution from here. In the next blog post, I will show you how to implement some post-processing logic in different OData requests.

Want to have a look on the other blog post of this series?

 

Best,

Edrilan Berisha

SAP S/4HANA Cloud Development Financials

Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.