Persistence top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

This tutorial is a follow-up to our OData Protocol Guide, where we've explored the basics of the OData protocol.

Now, we'll see how to implement a simple OData service using the Apache Olingo library.

This library provides a framework to expose data using the OData protocol, thus allowing easy, standards-based access to information that would otherwise be locked away in internal databases.

2. What is Olingo?

Olingo is one of the “featured” OData implementations available for the Java environment – the other being the SDL OData Framework. It is maintained by the Apache Foundation and is comprised of three main modules:

  • Java V2 – client and server libraries supporting OData V2
  • Java V4 – server libraries supporting OData V4
  • Javascript V4 – Javascript, client-only library supporting OData V4

In this article, we'll cover only the server-side V2 Java libraries, which support direct integration with JPA. The resulting service supports CRUD operations and other OData protocol features, including ordering, paging and filtering.

Olingo V4, on the other hand, only handles the lower-level aspects of the protocol, such as content-type negotiation and URL parsing. This means that it'll be up to us, developers, to code all nitty-gritty details regarding things like metadata generation, generating back-end queries based on URL parameters, etc.

As for the JavaScript client library, we're leaving it out for now because, since OData is an HTTP-based protocol, we can use any REST library to access it.

3. An Olingo Java V2 Service

Let's create a simple OData service with the two EntitySets that we've used in our brief introduction to the protocol itself. At its core, Olingo V2 is simply a set of JAX-RS resources and, as such, we need to provide the required infrastructure in order to use it. Namely, we need a JAX-RS implementation and a compatible servlet container.

For this example, we've opted to use Spring Boot – as it provides a quick way to create a suitable environment to host our service. We'll also use Olingo's JPA adapter, which “talks” directly to a user-supplied EntityManager in order to gather all data needed to create the OData's EntityDataModel.

While not a strict requirement, including the JPA adapter greatly simplifies the task of creating our service.

Besides standard Spring Boot dependencies, we need to add a couple of Olingo's jars:

<dependency>
    <groupId>org.apache.olingo</groupId>
    <artifactId>olingo-odata2-core</artifactId>
    <version>2.0.11</version>
    <exclusions>
        <exclusion>
            <groupId>javax.ws.rs</groupId>
            <artifactId>javax.ws.rs-api</artifactId>
         </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.olingo</groupId>
    <artifactId>olingo-odata2-jpa-processor-core</artifactId>
    <version>2.0.11</version>
</dependency>
<dependency>
    <groupId>org.apache.olingo</groupId>
    <artifactId>olingo-odata2-jpa-processor-ref</artifactId>
    <version>2.0.11</version>
    <exclusions>
        <exclusion>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
        </exclusion>
    </exclusions>
</dependency>

The latest version of those libraries is available at Maven's Central repository:

We need those exclusions in this list because Olingo has dependencies on EclipseLink as its JPA provider and also uses a different JAX-RS version than Spring Boot.

3.1. Domain Classes

The first step to implement a JPA-based OData service with Olingo is to create our domain entities. In this simple example, we'll create just two classes – CarMaker and CarModel – with a single one-to-many relationship:

@Entity
@Table(name="car_maker")
public class CarMaker {    
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)    
    private Long id;
    @NotNull
    private String name;
    @OneToMany(mappedBy="maker",orphanRemoval = true,cascade=CascadeType.ALL)
    private List<CarModel> models;
    // ... getters, setters and hashcode omitted 
}

@Entity
@Table(name="car_model")
public class CarModel {
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private Long id;
    
    @NotNull
    private String name;
    
    @NotNull
    private Integer year;
    
    @NotNull
    private String sku;
    
    @ManyToOne(optional=false,fetch=FetchType.LAZY) @JoinColumn(name="maker_fk")
    private CarMaker maker;
    
    // ... getters, setters and hashcode omitted
}

3.2. ODataJPAServiceFactory Implementation

The key component we need to provide to Olingo in order to serve data from a JPA domain is a concrete implementation of an abstract class called ODataJPAServiceFactory. This class should extend ODataServiceFactory and works as an adapter between JPA and OData. We'll name this factory CarsODataJPAServiceFactory, after the main topic for our domain:

@Component
public class CarsODataJPAServiceFactory extends ODataJPAServiceFactory {
    // other methods omitted...

    @Override
    public ODataJPAContext initializeODataJPAContext() throws ODataJPARuntimeException {
        ODataJPAContext ctx = getODataJPAContext();
        ODataContext octx = ctx.getODataContext();
        HttpServletRequest request = (HttpServletRequest) octx.getParameter(
          ODataContext.HTTP_SERVLET_REQUEST_OBJECT);
        EntityManager em = (EntityManager) request
          .getAttribute(EntityManagerFilter.EM_REQUEST_ATTRIBUTE);
        
        ctx.setEntityManager(em);
        ctx.setPersistenceUnitName("default");
        ctx.setContainerManaged(true);                
        return ctx;
    }
}

Olingo calls the initializeJPAContext() method if this class to get a new ODataJPAContext  used to handle every OData request. Here, we use the getODataJPAContext() method from the base classe to get a “plain” instance which we then do some customization.

This process is somewhat convoluted, so let's draw a UML sequence to visualize how all this happens:

Note that we're intentionally using setEntityManager() instead of setEntityManagerFactory(). We could get one from Spring but, if we pass it to Olingo, it'll conflict with the way that Spring Boot handles its lifecycle – especially when dealing with transactions.

For this reason, we'll resort to pass an already existing EntityManager instance and inform it that its lifecycle its externally managed. The injected EntityManager instance comes from an attribute available at the current request. We'll later see how to set this attribute.

3.3. Jersey Resource Registration

The next step is to register our ServiceFactory with Olingo's runtime and register Olingo's entry point with the JAX-RS runtime. We'll do it inside a ResourceConfig derived class, where we also define the OData path for our service to be /odata:

@Component
@ApplicationPath("/odata")
public class JerseyConfig extends ResourceConfig {
    public JerseyConfig(CarsODataJPAServiceFactory serviceFactory, EntityManagerFactory emf) {        
        ODataApplication app = new ODataApplication();        
        app
          .getClasses()
          .forEach( c -> {
              if ( !ODataRootLocator.class.isAssignableFrom(c)) {
                  register(c);
              }
          });
        
        register(new CarsRootLocator(serviceFactory)); 
        register(new EntityManagerFilter(emf));
    }
    
    // ... other methods omitted
}

Olingo's provided ODataApplication is a regular JAX-RS Application class that registers a few providers using the standard callback getClasses()

We can use all but the ODataRootLocator class as-is. This particular one is responsible for instantiating our ODataJPAServiceFactory implementation using Java's newInstance() method. But, since we want Spring to manage it for us, we need to replace it by a custom locator.

This locator is a very simple JAX-RS resource that extends Olingo's stock ODataRootLocator and it returns our Spring-managed ServiceFactory when needed:

@Path("/")
public class CarsRootLocator extends ODataRootLocator {
    private CarsODataJPAServiceFactory serviceFactory;
    public CarsRootLocator(CarsODataJPAServiceFactory serviceFactory) {
        this.serviceFactory = serviceFactory;
    }

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

3.4. EntityManager Filter

The last remaining piece for our OData service the EntityManagerFilter. This filter injects an EntityManager in the current request, so it is available to the ServiceFactory. It's a simple JAX-RS @Provider class that implements both ContainerRequestFilter and ContainerResponseFilter interfaces, so it can properly handle transactions:

@Provider
public static class EntityManagerFilter implements ContainerRequestFilter, 
  ContainerResponseFilter {

    public static final String EM_REQUEST_ATTRIBUTE = 
      EntityManagerFilter.class.getName() + "_ENTITY_MANAGER";
    private final EntityManagerFactory emf;

    @Context
    private HttpServletRequest httpRequest;

    public EntityManagerFilter(EntityManagerFactory emf) {
        this.emf = emf;
    }

    @Override
    public void filter(ContainerRequestContext ctx) throws IOException {
        EntityManager em = this.emf.createEntityManager();
        httpRequest.setAttribute(EM_REQUEST_ATTRIBUTE, em);
        if (!"GET".equalsIgnoreCase(ctx.getMethod())) {
            em.getTransaction().begin();
        }
    }

    @Override
    public void filter(ContainerRequestContext requestContext, 
      ContainerResponseContext responseContext) throws IOException {
        EntityManager em = (EntityManager) httpRequest.getAttribute(EM_REQUEST_ATTRIBUTE);
        if (!"GET".equalsIgnoreCase(requestContext.getMethod())) {
            EntityTransaction t = em.getTransaction();
            if (t.isActive() && !t.getRollbackOnly()) {
                t.commit();
            }
        }
        
        em.close();
    }
}

The first filter() method, called at the start of a resource request, uses the provided EntityManagerFactory to create a new EntityManager instance, which is then put under an attribute so it can later be recovered by the ServiceFactory. We also skip GET requests since should not have any side effects, and so we won't need a transaction.

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

3.5. Testing

Let's test our implementation using simple curl commands. The first this we can do is get the services $metadata document:

curl http://localhost:8080/odata/$metadata

As expected, the document contains two types – CarMaker and CarModel – and an association. Now, let's play a bit more with our service, retrieving top-level collections and entities:

curl http://localhost:8080/odata/CarMakers
curl http://localhost:8080/odata/CarModels
curl http://localhost:8080/odata/CarMakers(1)
curl http://localhost:8080/odata/CarModels(1)
curl http://localhost:8080/odata/CarModels(1)/CarMakerDetails

Now, let's test a simple query returning all CarMakers where its name starts with ‘B':

curl http://localhost:8080/odata/CarMakers?$filter=startswith(Name,'B')

A more complete list of example URLs is available at our OData Protocol Guide article.

5. Conclusion

In this article, we've seen how to create a simple OData service backed by a JPA domain using Olingo V2.

As of this writing, there is an open issue on Olingo's JIRA tracking the works on a JPA module for V4, but the last comment dates back to 2016. There's also a third-party open-source JPA adapter hosted at SAP's GitHub repository which, although unreleased, seems to be more feature-complete at this point than Olingo's one.

As usual, all code for this article is available over on GitHub.

Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
newest oldest most voted
Notify of
Abel
Guest
Abel

Is this for glassfish only?

Eric Martin
Member
Eric Martin

No. There are instructions on the main site for running on other servers.
https://olingo.apache.org/

Comments are closed on this article!