Lab9 : REST Services
REST - Representational State Transfer
What is REST?
REST is an architectural style which is based on web-standards and the HTTP protocol. In a REST based architecture everything is a resource. A resource is accessed via a common interface based on the HTTP standard methods. In a REST based architecture you have a REST server which provides access to the resources. A REST client can access and modify the REST resources.
Every resource should support the HTTP common operations. Resources are identified by global IDs (which are typically URIs).
REST allows that resources have different representations, e.g., text, XML, JSON etc. The REST client can ask for a specific representation via the HTTP protocol (content negotiation).
HTTP methods
The PUT, GET, POST and DELETE methods are typically used in REST based architectures. The following gives an explanation of these operations.
- GET defines a reading access of the resource without side-effects. The resource is never changed via a GET request, e.g., the request has no side effects (idempotent).
- PUT creates a new resource.
- DELETE removes the resources. The operations are idempotent. They can get repeated without leading to different results.
- POST updates an existing resource or creates a new resource.
RESTFul web services
A RESTFul web services are based on HTTP methods and the concept of REST. A RESTFul web service defines the base URI for the services, the supported MIME-types (XML, text, JSON, user-defined, … ). It also defines the set of operations (POST, GET, PUT, DELETE) which are supported.
JAX-RS is a Java programming language API designed to make it easy to develop applications that use the REST architecture. Jersey is the reference implementation of JAX-RS.
Project
Create a new gradle based project named lab9. For this project we will be using the deprecated Jetty plugin instead of the recommended Gretty plugin we have been using so far. This is because Gretty does not shutdown after running tests as Jersey starts background threads. We are able to get around this with the older Jetty plugin but with some hacks.
Create the src/integration/java and src/integration/resources directories before modifying the gradle build file, so auto refresh works properly.
Create the src/integration/java and src/integration/resources directories before modifying the gradle build file, so auto refresh works properly.
build.gradle
plugins { id 'java' id 'war' id 'jetty' } group 'mis283' sourceCompatibility = 1.8 repositories { mavenCentral() } sourceSets { integration { java.srcDir 'src/integration/java' resources.srcDir 'src/integration/resources' } } dependencies { compile 'javax.ws.rs:javax.ws.rs-api:2.1' compile 'org.glassfish.jersey.core:jersey-server:2.26' compile 'org.hibernate:hibernate-core:5.2.11.Final' testCompile 'junit:junit:4.12' runtime 'org.glassfish.jersey.containers:jersey-container-servlet:2.26' runtime 'org.glassfish.jersey.core:jersey-common:2.26' runtime 'org.glassfish.jersey.inject:jersey-hk2:2.26' runtime 'org.glassfish.jersey.media:jersey-media-json-jackson:2.26' runtime 'com.h2database:h2:1.4.196' integrationCompile sourceSets.main.output integrationCompile configurations.compile integrationCompile configurations.testCompile integrationRuntime configurations.runtime integrationRuntime configurations.testRuntime } httpPort = 8080 stopPort = 9451 stopKey = 'foo' [jettyRun, jettyRunWar]*.daemon = true task integration(type: Test, dependsOn: jettyRun) { delete 'lab9.mv.db', 'lab9.trace.db' group = LifecycleBasePlugin.VERIFICATION_GROUP description = 'Runs the integration tests.' maxHeapSize = '256m' testClassesDir = sourceSets.integration.output.classesDir classpath = sourceSets.integration.runtimeClasspath binResultsDir = file("$buildDir/integration-test-results/binary/integration") reports { html.destination = "$buildDir/reports/integration-test" junitXml.destination = "$buildDir/integration-test-results" } mustRunAfter tasks.test jettyStop.execute() } // To rerun all tests and not just failures or updates test.dependsOn 'cleanTest' // To rerun all integration tests and not just failures or updates integration.dependsOn 'cleanIntegration' check.dependsOn integration
JPA Configuration
Copy over the persistence.xml from lab8 and just change lab8 to lab9 in the file.
Web Application Configuration
Create src/main/webapp/WEB-INF directory and create a web.xml file with the following:
Copy over the persistence.xml from lab8 and just change lab8 to lab9 in the file.
Web Application Configuration
Create src/main/webapp/WEB-INF directory and create a web.xml file with the following:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <display-name>REST services with JAX-RS and JAXB</display-name> <servlet> <servlet-name>Lab9Application</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>mis283.Application</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Lab9Application</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
Source files
Copy over the model and repository sources as well as the repository unit test sources from lab8 to the project. The only difference is that we will move the Datastore class over from the unit test source set to the main source set, as we will be using it to manage JPA entity manager factory.
Copy over the model and repository sources as well as the repository unit test sources from lab8 to the project. The only difference is that we will move the Datastore class over from the unit test source set to the main source set, as we will be using it to manage JPA entity manager factory.
Service
Create a new package named mis283.service under src/main/java source set and add REST services classes for managing Site and Article resources.
SiteService.java
package mis283.service; import mis283.model.Site; import javax.persistence.EntityManager; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Objects; import java.util.Optional; import java.util.logging.Logger; import static java.lang.String.format; import static java.util.logging.Level.SEVERE; import static mis283.repository.Datastore.getDatastore; import static mis283.repository.SiteRepository.getSiteRepository; @Path("/site") public class SiteService { private static final Logger logger = Logger.getLogger("service"); @PUT @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Site create(final Site site) { if (site.getId() != null && site.getId() > 0) throw new BadRequestException("Site id not allowed in create"); final EntityManager entityManager = getDatastore().getManager(); try { return getSiteRepository().save(site, entityManager); } catch (final Throwable t) { logger.log(SEVERE, "Error creating site", t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } @GET @Path("{id}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Site get(@PathParam("id") final Integer id) { if (id == null || id <= 0) throw new BadRequestException(format("Invalid site id: (%d) specified", id)); final EntityManager entityManager = getDatastore().getManager(); try { final Optional<Site> option = getSiteRepository().retrieve(id, entityManager); if (option.isPresent()) return option.get(); throw new NotFoundException(format("No site with id: (%d) found.", id)); } catch (final Throwable t) { logger.log(SEVERE, format("Error retrieving site with id: %d", id), t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } @POST @Path("{id}") @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Site update(@PathParam("id") final Integer id, final Site site) { if (site.getId() == null || site.getId() <= 0) throw new BadRequestException(format("Invalid site id: (%d) specified", site.getId())); if (id == null || id <= 0 || !Objects.equals(id, site.getId())) throw new BadRequestException(format("Resource path id: (%d) and resource id: (%d) do not match.", id, site.getId())); final EntityManager entityManager = getDatastore().getManager(); try { return getSiteRepository().save(site, entityManager); } catch (final Throwable t) { logger.log(SEVERE, "Error updating site", t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } @DELETE @Path("{id}") @Produces(MediaType.TEXT_PLAIN) public Response delete(@PathParam("id") final Integer id) { if (id == null || id <= 0) throw new BadRequestException(format("Invalid site id: (%d) specified", id)); final EntityManager entityManager = getDatastore().getManager(); try { getSiteRepository().delete(id, entityManager); return Response.ok(format("Deleted site with id: %d", id)).build(); } catch (final Throwable t) { logger.log(SEVERE, format("Error deleting site with id: %d", id), t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } }
ArticleService.java
package mis283.service; import mis283.model.Article; import javax.persistence.EntityManager; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Objects; import java.util.Optional; import java.util.logging.Logger; import static java.lang.String.format; import static java.util.logging.Level.SEVERE; import static mis283.repository.ArticleRepository.getArticleRepository; import static mis283.repository.Datastore.getDatastore; @Path("/article") public class ArticleService { private static final Logger logger = Logger.getLogger("service"); @PUT @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Article create(final Article article) { if (article.getId() != null && article.getId() > 0) throw new BadRequestException("Article id not allowed in create"); final EntityManager entityManager = getDatastore().getManager(); try { return getArticleRepository().save(article, entityManager); } catch (final Throwable t) { logger.log(SEVERE, "Error creating article", t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } @GET @Path("{id}") @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Article get(@PathParam("id") final Integer id) { if (id == null || id <= 0) throw new BadRequestException(format("Invalid article id: (%d) specified", id)); final EntityManager entityManager = getDatastore().getManager(); try { final Optional<Article> option = getArticleRepository().retrieve(id, entityManager); if (option.isPresent()) return option.get(); throw new NotFoundException(format("No article with id: (%d) found.", id)); } catch (final Throwable t) { logger.log(SEVERE, format("Error retrieving article with id: %d", id), t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } @POST @Path("{id}") @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Article update(@PathParam("id") final Integer id, final Article article) { if (article.getId() == null || article.getId() <= 0) throw new BadRequestException(format("Invalid article id: (%d) specified", article.getId())); if (id == null || id <= 0 || !Objects.equals(id, article.getId())) throw new BadRequestException(format("Resource path id: (%d) and resource id: (%d) do not match.", id, article.getId())); final EntityManager entityManager = getDatastore().getManager(); try { return getArticleRepository().save(article, entityManager); } catch (final Throwable t) { logger.log(SEVERE, "Error updating article", t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } } @DELETE @Path("{id}") @Produces(MediaType.TEXT_PLAIN) public Response delete(@PathParam("id") final Integer id) { if (id == null || id <= 0) throw new BadRequestException(format("Invalid article id: (%d) specified", id)); final EntityManager entityManager = getDatastore().getManager(); try { getArticleRepository().delete(id, entityManager); Response.ok(format("Deleted article with id: %d", id)).build(); } catch (final Throwable t) { logger.log(SEVERE, format("Error deleting article with id: %d", id), t); throw new InternalServerErrorException(t.getMessage()); } finally { if (entityManager != null) entityManager.close(); } throw new InternalServerErrorException("Should never get here"); } }
Unit Tests
Create a corresponding mis283.service package under the src/test/java source set and add a test suite for Site resources. Since the services we wrote are standard Java classes with no HTTP dependency, we can unit test them directly without needing any web container.
SiteServiceTest.java
package mis283.service; import mis283.model.Site; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import java.net.URL; import static java.lang.String.format; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class SiteServiceTest { private static Site site; static { site = new Site(); site.setName("Unit Test Site"); try { site.setUrl(new URL("http://test.com/")); } catch (final Throwable t) { throw new RuntimeException(t); } } private static final SiteService service = new SiteService(); @BeforeClass public static void create() { site = service.create(site); assertNotNull(site.getId()); assertTrue(site.getId() > 0); } @Test public void retrieve() { final Site s = service.get(site.getId()); assertNotNull(s.getId()); assertEquals(site.getId(), s.getId()); assertEquals(site.getName(), s.getName()); assertEquals(site.getUrl(), s.getUrl()); } @Test public void update() { final String modified = format("%s modified", site.getName()); Site s = service.get(site.getId()); assertNotNull(s.getId()); s.setName(modified); s = service.update(site.getId(), s); assertEquals(modified, s.getName()); s = service.get(s.getId()); assertEquals(modified, s.getName()); s.setName(site.getName()); s = service.update(site.getId(), s); assertEquals(site.getName(), s.getName()); s = service.get(s.getId()); assertEquals(site.getName(), s.getName()); } @AfterClass public static void delete() { final Integer id = site.getId(); service.delete(id); try { service.get(id); fail("Retrieving deleted site did not throw exception"); } catch (final Throwable ignored) {} } }
Application
Create an mis283.Application class under the main source set. This is the class that was configured in web.xml as the application that is configured with the Jersey servlet. All this class does is specify the packages under which REST services are to be searched for when the application is loaded. This may also be configured in web.xml, but using an application class is the more common current practice.
package mis283; import mis283.service.SiteService; import org.glassfish.jersey.server.ResourceConfig; public class Application extends ResourceConfig { public Application() { packages(SiteService.class.getPackage().getName()); } }
Integration Test
Create an mis283.service package under the integration source set and add an integration test suite for the Site resource. Note that we do not need the httpunit library for this project, since JAX-RS provides API’s for developing both REST servers as well as clients.
SiteServiceTest.java
package mis283.service; import mis283.model.Site; import org.glassfish.jersey.client.ClientConfig; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.net.URI; import java.net.URL; import static java.lang.String.format; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class SiteServiceTest { private static Site site; static { site = new Site(); site.setName("Unit Test Site"); try { site.setUrl(new URL("http://test.com/")); } catch (final Throwable t) { throw new RuntimeException(t); } } private static final ClientConfig config = new ClientConfig(); private static final Client client = ClientBuilder.newClient(config); private static final URI baseUri = UriBuilder.fromUri("http://localhost:8080/lab9").build(); @BeforeClass public static void create() { final WebTarget target = client.target(baseUri); site = target .path("site") .request(MediaType.APPLICATION_JSON) .put(Entity.entity(site, MediaType.APPLICATION_JSON),Site.class); assertNotNull(site.getId()); assertTrue(site.getId() > 0); } @Test public void retrieve() { final Site s = get(); assertNotNull(s.getId()); assertEquals(site.getId(), s.getId()); assertEquals(site.getName(), s.getName()); assertEquals(site.getUrl(), s.getUrl()); } @Test public void update() { final String modified = format("%s modified", site.getName()); Site s = get(); assertNotNull(s.getId()); s.setName(modified); s = update(s); assertEquals(modified, s.getName()); s = get(); assertEquals(modified, s.getName()); s.setName(site.getName()); s = update(s); assertEquals(site.getName(), s.getName()); s = get(); assertEquals(site.getName(), s.getName()); } @AfterClass public static void delete() { final WebTarget target = client.target(baseUri); final Response response = target .path("site") .path(site.getId().toString()) .request(MediaType.TEXT_PLAIN) .delete(Response.class); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); try { get(); fail("Retrieving deleted site did not throw exception"); } catch (final Throwable ignored) {} } private static Site get() { final WebTarget target = client.target(baseUri); return target .path("site") .path(site.getId().toString()) .request(MediaType.APPLICATION_JSON) .get(Site.class); } private Site update(final Site s) { final WebTarget target = client.target(baseUri); return target .path("site") .path(site.getId().toString()) .request(MediaType.APPLICATION_JSON) .post(Entity.entity(s, MediaType.APPLICATION_JSON), Site.class); } }