RESTify your JPA Entities

RESTify your JPA Entities As a Developer Evangelist, one of my responsibilities is to explore new technologies and get them working on OpenShift. OpenShift is one of those platforms which does not impose limitations and makes it very easy to try and deploy the latest frameworks and technologies. In this blog post, we will look at the Spring Data Rest project, create a simple application using it, then deploy that application to OpenShift.

Spring Data Rest project makes it very easy to provide REST interfaces to Spring Data JPA repositories using HATEOAS principles. It does all the boilerplate work required for implementing RESTFul CRUD services for JPA entities. HATEOAS stands for Hypermedia as the Engine of Application State. It states that resources should be discoverable through the publication of links that point to available resources. There should be a single endpoint for the resource, and all actions you’d need to undertake should be discoverable by inspecting that resource. For example, if we want to discover endpoints that are available at the application root, we can execute the curl command as shown below. As you can see below, the client can now extract a set of links from the returned JSON object that represents the next level of resources that are available to the client.

curl -v http://notebook-newideas.rhcloud.com/
 
{
  "links" : [ {
    "rel" : "notebooks",
    "href" : "http://notebook-newideas.rhcloud.com/notebooks"
  }, {
    "rel" : "notes",
    "href" : "http://notebook-newideas.rhcloud.com/notes"
  } ],
  "content" : [ ]

Spring Data JPA is another project which helps in implementing JPA based repositories using dynamic proxies. I talked about the Spring Data JPA project in part 3 of the Polyglot Persistence blog series so please refer to that for more information.

Prerequisite

Before we start building our application, we'll have to perform a few tasks:

  1. Sign up for an OpenShift Account. It is completely free and Red Hat gives every user three free Gears on which to run your applications. At the time of this writing, the combined resources allocated for each user is 1.5 GB of memory and 3 GB of disk space.

  2. Install the rhc client tool on your machine. The rhc is a ruby gem so you need to have ruby 1.8.7 or above on your machine. To install rhc, just type

sudo gem install rhc

If you already have one, make sure it is the latest one. To update your rhc tools, execute the command shown below.

sudo gem update rhc

For additional assistance setting up the rhc command-line tool, see the following page: https://openshift.redhat.com/community/developers/rhc-client-tools-install

  1. Setup an OpenShift account using rhc setup command. This command will help you create a namespace and upload the ssh keys to the OpenShift server.

Let's Build the Application

The source code of the application is available in github at https://github.com/shekhargulati/notebook-spring-data-rest.

Creating a Notebook OpenShift Application

After performing all the mandatory setup tasks, we can create an application named "notebook". This application allows users to store notes online. It has two entities; Notebook and Note. A notebook can contain many notes i.e. One to Many Relationship. To create a Java application, type the command shown below:

rhc app create notebook jbossas-7

This will create an application container for us, called a gear, and setup all of the required SELinux policies and cgroup configuration. OpenShift will also setup a private git repository for you and clone the repository to your local system. Finally, OpenShift will propagate the DNS to the outside world. The application will be accessible at http://notebook-domain-name.rhcloud.com/. Replace domain-name with your own unique domain name.

Adding PostgreSQL cartridge to the application

This application will use the PostgreSQL database to store notebooks and notes. To add the PostgreSQL cartridge execute the command shown below:

rhc cartridge add postgresql-8.4 --app notebook 

Pull Code and Push it to OpenShift

Next we will pull the code from my github repository and then push the code to our OpenShift gear. To do that follow the steps mentioned below:

$ cd notebook
 
$ git rm -rf src/ pom.xml
 
$ git commit -am "removed default files"
 
$ git remote add upstream -m master git://github.com/shekhargulati/notebook-spring-data-rest.git
 
$ git pull -s recursive -X theirs upstream master
 
$ git push

After the code has been built and deployed to our OpenShift gear, we can see list of REST resources using the curl command as shown below. This is the core of HATEOAS -- ability to discover resources.

curl -v http://notebook-newideas.rhcloud.com/
 
{
  "links" : [ {
    "rel" : "notebooks",
    "href" : "http://notebook-newideas.rhcloud.com/notebooks"
  }, {
    "rel" : "notes",
    "href" : "http://notebook-newideas.rhcloud.com/notes"
  } ],
  "content" : [ ]

The beauty is that we can walk through different links to see what all resources are exposed at that URL. For example, if we go to http://notebook-newideas.rhcloud.com/notebooks we will see that it exposes a REST resource for search as shown below.

curl -v http://notebook-newideas.rhcloud.com/notebooks
 
{
  "links" : [ {
    "rel" : "notebooks.search",
    "href" : "http://notebook-newideas.rhcloud.com/notebooks/search"
  } ],
  "content" : [ ]

Code Walkthrough

Now that we have seen the application running in the cloud, let's look at a few interesting things in the code.

Adding Spring Data Rest Maven Dependencies

The Spring Data Rest project is a Spring MVC project so we can easily add its capabilities to our project with very little effort. We need to add the following spring-data-rest-* dependencies in our pom.xml. You can look at the full pom.xml on github.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-rest-webmvc</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-rest-repository</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-rest-core</artifactId>
    <version>1.1.0.BUILD-SNAPSHOT</version>
</dependency>     

Configuring Spring JPA Repositories

The JPA repositories is at the heart of Spring Data REST project. We need to write repository interfaces for Notebook and Note entities before we configure Spring Data Rest. Spring Data JPA uses dynamic proxies to implement interfaces. The repository interface should extend either or CrudRepository or PagingAndSortingRepository of Spring Data JPA project. The CrudRepository provides methods for basic CRUD operations and PagingAndSortingRepository provides additional methods for paging and sorting on top of CRUD methods. In the code shown below I have used CrudRepository but we can also use PagingAndSortingRepository. Apart from basic CRUD methods, we can also specify finders in our interface and Spring Data JPA will generate finder implementations using method names. There are various ways to generate queries and you should read the documentation for more information.

public interface NotebookRepository extends CrudRepository<Notebook, Long> {
 
    public Notebook findByNameLike(String name);
 
}

Configuring Spring Data JPA Repositories

The next important piece of code is the ApplicationConfig class which contains various bean definitions for configuring JPA support in Spring. The class is marked with the @EnableJpaRepositories annotation to generate dynamic proxies for JPA repository interfaces . The @EnableTransactionManagement enables Spring annotation driven transaction management.

@Configuration
@ComponentScan(basePackages = "com.shekhar.notebook")
@EnableJpaRepositories(basePackageClasses = NotebookRepository.class)
@EnableTransactionManagement
public class ApplicationConfig {
 
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setDatabase(Database.POSTGRESQL);
        vendorAdapter.setGenerateDdl(true);
        vendorAdapter.setShowSql(true);
 
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan(Notebook.class.getPackage().getName());
        factory.setDataSource(dataSource());
        return factory;
    }
 
    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl("jdbc:postgresql://" + System.getenv("OPENSHIFT_POSTGRESQL_DB_HOST") + ":"
                + System.getenv("OPENSHIFT_POSTGRESQL_DB_PORT") + "/"+System.getenv("OPENSHIFT_APP_NAME"));
        dataSource.setUsername(System.getenv("OPENSHIFT_POSTGRESQL_DB_USERNAME"));
        dataSource.setPassword(System.getenv("OPENSHIFT_POSTGRESQL_DB_PASSWORD"));
        dataSource.setTestOnBorrow(true);
        dataSource.setTestOnReturn(true);
        dataSource.setTestWhileIdle(true);
        dataSource.setTimeBetweenEvictionRunsMillis(1800000L);
        dataSource.setNumTestsPerEvictionRun(3);
        dataSource.setMinEvictableIdleTimeMillis(1800000L);
        dataSource.setValidationQuery("SELECT 1");
        return dataSource;
    }
 
    @Bean
    public JpaDialect jpaDialect() {
        return new HibernateJpaDialect();
    }
 
    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return txManager;
    }
 
}

Defining Spring Data REST Resources

By default all the CrudRepository interfaces will be exported as REST services. So, our NotebookRepository will be exposed to the following URL:

http://localhost:8080/notebook.

You can configure how a repository is configured using @RestResource annotation. The @RestResource annotation is a Spring Data Rest annotation which can be used to configure the path, or whether a resource should be exported or not. The path variable in @RestResource annotation is used to override the default naming convention for resource URLs. The default naming convention is to remove the repository from the name of the interface. The default name would be "notebook" but we can change it by passing value in the path. The code shown below also shows how we can expose finder methods as REST resources. The finder will be exposed at http://notebook-newideas.rhcloud.com/notebooks/search/name

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.repository.annotation.RestResource;
 
import com.shekhar.notebook.domain.Notebook;
 
@RestResource(path = "notebooks")
public interface NotebookRepository extends CrudRepository<Notebook, Long> {
 
    @RestResource(path = "name", rel = "names")
    public Notebook findByNameLike(@Param("name") String name);
 
}

Configuring Spring Data REST

In Servlet 3.0, web.xml is not mandatory for web applications. As JBossAS7 is JavaEE6 compliant, it also does not require the use of web.xml. The Spring container requires you to create a class which implements the WebApplicationInitializer interface. This class is invoked by the Spring container at startup. The code shown below first registers the ApplicationConfig class with AnnotationConfigWebApplicationContext and then ContextLoaderListener is added to the ServletContext. A new instance of AnnotationConfigWebApplicationContext is created which registers RepositoryRestMvcConfiguration which contains the Spring MVC configuration for the REST exporter. Finally, the web context is used to instantiate DispatcherServlet which is added to ServletContext as shown below.

@Configuration
public class RestExporterWebInitializer implements WebApplicationInitializer {
 
    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException {
        AnnotationConfigWebApplicationContext rootCtx = new AnnotationConfigWebApplicationContext();
        rootCtx.register(ApplicationConfig.class);
 
        servletContext.addListener(new ContextLoaderListener(rootCtx));
 
        AnnotationConfigWebApplicationContext webCtx = new AnnotationConfigWebApplicationContext();
        webCtx.register(RepositoryRestMvcConfiguration.class);
 
        DispatcherServlet dispatcherServlet = new DispatcherServlet(webCtx);
        ServletRegistration.Dynamic reg = servletContext.addServlet(
                "rest-exporter", dispatcherServlet);
        reg.setLoadOnStartup(1);
        reg.addMapping("/*");
 
    }
 
}

Testing the application

In order to test the application, we use a few curl commands.

To create the Notebook, execute the curl command as shown below:

$ curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d  '{"author":"test_user","description":"this is a test notebook","name":"test notebook","tags":"test,hello-world"}' http://notebook-newideas.rhcloud.com/notebooks
 
 
HTTP/1.1 201 Created
Date: Mon, 27 May 2013 10:52:31 GMT
Server: Apache-Coyote/1.1
Location: http://notebook-newideas.rhcloud.com/notebooks/1
Content-Length: 0
Content-Type: text/plain

Now lets do a GET call for the notebook we just created.

$ curl http://notebook-newideas.rhcloud.com/notebooks/1
 
{
  "description" : "this is a test notebook",
  "name" : "test notebook",
  "created" : 1369651950487,
  "tags" : "test,hello-world",
  "author" : "test_user",
  "version" : 0,
  "links" : [ {
    "rel" : "self",
    "href" : "http://notebook-newideas.rhcloud.com/notebooks/1"
  }, {
    "rel" : "notebook.notebook.notes",
    "href" : "http://notebook-newideas.rhcloud.com/notebooks/1/notes"
  } ]
}

In the response, we get the content as well as the links.

To create a Note the curl command is shown below:

curl -i -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d  '{"tags":"test,hello","text":"test note 1","title":"test note title"}' http://notebook-newideas.rhcloud.com/notes

To add note to the notebook the curl command is shown below.

curl -v -d 'http://notebook-newideas.rhcloud.com/notes/2' -H "Content-Type: text/uri-list" http://notebook-newideas.rhcloud.com/notebooks/1/notes

What's Next?