November 16, 2017

Java EE 7 Designing JAX-RS with System Test

Whenever building a Java EE 6 or 7 application always use the javax:javaee-api maven dependency and only that. So lets start with a Java EE 7 maven pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>se.magnuskkarlsson.examples</groupId>
    <artifactId>example-javaee7</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>
    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>example-javaee7</finalName>
    </build>
</project>

To make JAX-RS work we need to add an class that extends Application and annotated with @ApplicationPath.

package se.magnuskkarlsson.example.javaee7;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class JavaEE7Application extends Application {
}

Then we can write our Boundary JAX-RS class.

package se.magnuskkarlsson.example.javaee7.boundary;

import java.net.URI;
import java.util.List;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;

import javax.ws.rs.GET;
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.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import se.magnuskkarlsson.example.javaee7.control.EmployeeBean;

import se.magnuskkarlsson.example.javaee7.entity.Employee;

@Path("/employee")
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class EmployeeRest {

    @Inject
    EmployeeBean bean;

    // returns 200 OK if found, othereise 204 No Content
    @GET
    @Path("/{id}")
    public Employee findById(@PathParam(value = "id") long id) {
        return bean.findById(id);
    }

    // returns 200 OK, otherwise empty JsonArray
    @GET
    public List findAll() {
        return bean.findAll();
    }

    // returns 201 Created and Location header, otherwise 400 Bad Request and reason in body
    @POST
    public Response create(@Context UriInfo uriInfo, @Valid Employee employee) {
        long id = bean.create(employee).getId();
        URI location = uriInfo.getAbsolutePathBuilder().path("/" + id).build();
        return Response.created(location).build();
    }

    // returns 200 OK, otherwise 400 Bad Request and reason in body
    @PUT
    public Employee update(@Valid Employee employee) {
        return bean.update(employee);
    }

    // returns always 204 No Content
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam(value = "id") long id) {
        bean.delete(id);
    }

}

And our business logic we encapsulate in a Controller class.

package se.magnuskkarlsson.example.javaee7.control;

import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import se.magnuskkarlsson.example.javaee7.entity.Employee;

@Stateless
public class EmployeeBean {

    @PersistenceContext
    private EntityManager em;

    public Employee findById(long id) {
        return em.find(Employee.class, id);
    }

    public List findAll() {
        return em.createQuery("FROM Employee e").getResultList();
    }

    public Employee create(Employee employee) {
        em.persist(employee);
        return employee;
    }

    public Employee update(Employee employee) {
        em.merge(employee);
        return employee;
    }

    public void delete(long id) {
        try {
            Employee employee = em.getReference(Employee.class, id);
            em.remove(employee);
        } catch (EntityNotFoundException IGNORE) {
            // we want to delete anyway ...
        }
    }

}

And finally our Entity class.

package se.magnuskkarlsson.example.javaee7.entity;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.validation.constraints.Size;

import javax.xml.bind.annotation.XmlRootElement;

@Entity
@XmlRootElement
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue
    private Long id;
    @Column(length = 255)
    @Size(min = 3, max = 255)
    private String name;

    public Employee() {
    }

    public Employee(Long id, String name) {
        this.id = id;
        this.name = 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;
    }

}

We also need to a META-INF/persistence.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="javaee7" transaction-type="JTA">
        <properties>
            <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
        </properties>
    </persistence-unit>
</persistence>

And a WEB-INF/beans.xml file to make our application CDI aware.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
    bean-discovery-mode="all">
</beans>

To test this build and deploy your application to, e.g. JBoss EAP 7.

Then create a new simple java project that we suffix with -st, for system test. In that pom.xml we need to add a jax-rs and json-p implementation dependency.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>se.magnuskkarlsson.examples</groupId>
    <artifactId>example-javaee7-st</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- jax-rs reference implementation -->
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-client</artifactId>
            <version>2.17</version>
            <scope>test</scope>
        </dependency>
        <!-- json-p reference implementation -->
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.4</version>
            <scope>test</scope>
        </dependency>  
        <!-- the glue between jax-rs and json-p reference implementation -->
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-processing</artifactId>
            <version>2.17</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

Now lets write our system test.

package se.magnuskkarlsson.example.javaee7.boundary;

import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
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 static javax.ws.rs.core.MediaType.*;
import javax.ws.rs.core.Response;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;

public class EmployeeRestIT {

    private Client client;
    private WebTarget target;
    
    @Before
    public void setUp() throws Exception {
        client = ClientBuilder.newClient();
        target = client.target("http://localhost:8080/example-javaee7/rest/employee");
    }
    
    @Test
    public void crud() throws Exception {
        JsonObjectBuilder builderCreate = Json.createObjectBuilder();
        JsonObject input = builderCreate.add("name", "Foo Bar").build();
        Response respCreate = target.request(APPLICATION_JSON).post(Entity.json(input));
        assertThat(respCreate.getStatus(), is(201));
        String location = respCreate.getHeaderString("Location");
        assertNotNull(location);

        Response respGet = client.target(location).request(APPLICATION_JSON).get();
        assertThat(respGet.getStatus(), is(200));
        JsonObject payload = respGet.readEntity(JsonObject.class);
        System.out.println("payload='" + payload + "'");
        
        int id = payload.getInt("id");
        JsonObjectBuilder builderUpdate = Json.createObjectBuilder();
        JsonObject update = builderUpdate.add("id", id).add("name", "UPDATED!").build();
        Response respUpdate = target.request(APPLICATION_JSON).put(Entity.json(update));
        assertThat(respCreate.getStatus(), is(201));
        System.out.println("respUpdate=" + respUpdate.readEntity(JsonObject.class));
        
        Response respDelete = target.path("/" + id).request(APPLICATION_JSON).delete();
        assertThat(respDelete.getStatus(), is(204));
    }
    
    @Test
    public void findNonExisting() throws Exception {
        Response response = target.path("/-999").request(APPLICATION_JSON).get();
        assertThat(response.getStatus(), is(204));
        String payload = response.readEntity(String.class);
        assertThat(payload, is(""));
    }
    
    @Test
    public void findAll() throws Exception {
        Response resp = target.request(APPLICATION_JSON).get();
        assertThat(resp.getStatus(), is(200));
        JsonArray array = resp.readEntity(JsonArray.class);
        assertThat(array.size(), is(0));
    }

    @Test
    public void updateInvalid() throws Exception {
        JsonObjectBuilder builder = Json.createObjectBuilder();
        JsonObject input = builder.add("name", "E").build();
        Response response = target.request(APPLICATION_JSON).put(Entity.json(input));
        
        assertThat(response.getStatus(), is(400));
        System.out.println("status : " + response.getStatus());
        System.out.println("entity : " + response.readEntity(String.class));
        response.getHeaders().forEach((key, value) -> System.out.println(key + ": " + value));
    }
    
    @Test
    public void delete() throws Exception {
        JsonObjectBuilder builder = Json.createObjectBuilder();
        JsonObject input = builder.add("name", "Foo Bar").build();
        Response respCreate = target.request(APPLICATION_JSON).post(Entity.json(input));
        assertThat(respCreate.getStatus(), is(201));
        String location = respCreate.getHeaderString("Location");
        assertNotNull(location);
        
        Response respDelete = client.target(location).request(APPLICATION_JSON).delete();
        assertThat(respDelete.getStatus(), is(204));
    }
    
    @Test
    public void deleteNonExisting() throws Exception {
        Response respDelete = target.path("/-999").request(APPLICATION_JSON).delete();
        assertThat(respDelete.getStatus(), is(204));
    }
    
}

No comments: