Testing JAX-RS/Jersey using Maven & Jetty

December 7, 2008 - zenoconsultingzenoconsulting


Recently I needed to build a web based service that allowed a client to do an HTTP POST and pass a raw XML file. I did a little research and decided to try using JAX-RS (Java API for RESTful Web Services). JAX-RS is just a specification. There are a handful of implementations, but I decided to try Sun's implementation called Jersey.

I needed a lightweight web server to test with, so I decided to use Jetty for testing. I also like Maven a lot, so that's what I used to build and manage the project.

Here's a quick primer on how to set up a similar project quickly.

Assumptions

  • You installed JDK 6
  • You installed the latest Maven
  • You have a proper IDE (e.g. Eclipse)

Create a new maven webapp project:

$ mvn archetype:create -DgroupId=com.mycompany.app -DartifactId=my-webapp -DarchetypeArtifactId=maven-archetype-webapp

I use Eclipse, so the next step is just to build the necessary project files for your IDE:

$ mvn eclipse:eclipse

Now just import the project into eclipse.

Update the pom.xml to pull in the dependencies you'll need (below). You'll need to add the URL for the java.net repo. The versions of these jars may be dated when you read this, so it makes sense to browse the repositories, and find the versions that make sense. I am using snapshot jars for some of these because they are still under active development at the time of writing. For testing I decided to try out HttpUnit.

<repositories>
  <repository>
   <id>javamaven2</id>
   <name>Repository for Maven2</name>
   <url>http://download.java.net/maven/2</url>
  </repository>
 </repositories>
 <dependencies>
  <dependency>
   <!-- the implementation of JAX-RS -->
   <groupId>com.sun.jersey</groupId>
   <artifactId>jersey-server</artifactId>
   <version>1.0.1</version>
  </dependency>
  <dependency>
   <groupId>javax.mail</groupId>
   <artifactId>mail</artifactId>
   <version>1.4.2-SNAPSHOT</version>
  </dependency>
  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.13</version>
   <type>jar</type>
  </dependency>
  <!-- PROVIDED BY CONTAINER; HERE FOR COMPILE ONLY -->
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
   <scope>provided</scope>
  </dependency>
  <!-- TEST SCOPE DEPENDENCIES -->
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>[4.0,5.0)</version>
   <scope>test</scope>
  </dependency>
  <dependency>
   <!-- used in integration tests -->
   <groupId>javanettasks</groupId>
   <artifactId>httpunit</artifactId>
   <version>[1.0,2.0)</version>
   <scope>test</scope>
  </dependency>
  <dependency>
   <!-- this is needed by HTTP-UNIT and is missing from their pom.xml -->
   <groupId>rhino</groupId>
   <artifactId>js</artifactId>
   <version>1.6R5</version>
   <scope>test</scope>
  </dependency>
 </dependencies>

Now just refresh the eclipse project files via maven:

$ mvn eclipse:clean eclipse:eclipse

Refresh the eclipse project with F5. You should have all the jars you need.

JAX-RS makes it very easy to build classes that serve the HTTP protocol. You can do most of the heavy lifting with annotations. The class is deceptively simple:

package biz.zenoconsulting.jersey;
 
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
 
import org.apache.log4j.Logger;
 
/**
 * Jersey Demo
 */
@Path("/hello")
public class MyServlet {
 
    private static final Logger LOGGER = Logger.getLogger(MyServlet.class);
 
    /**
     * 
     * @return
     */
    @GET
    @Produces("text/plain")
    public String doGet() {
        return "hello";
    }
 
    /**
     * 
     * @param xml
     * @return
     */
    @POST
    @Produces("application/xml")
    @Consumes({"application/x-www-form-urlencoded", "multipart/form-data"})
    public String doPost(@FormParam("xml") String xml) {
        if(xml == null) { 
            LOGGER.error("Expected 'xml' parameter was not found in the POST");
            return null;
        }
        return xml;
    }

There are a couple things to note here. The GET method will produce the mime type text/plain and it just returns a "hello" string. The POST method consumes the mime types application/x-www-form-urlencoded and multipart/form-data and it expects a form parameter named "xml" that contains the XML content. It will just echo that back to the browser. Finally, the path to this service will be the root path + /hello/. In this case, the root path will be the host where we deploy the WAR file + the context (see below).

Now, setup the web.xml file (found under src/main/webapp/WEB-INF):

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <display-name>Jersey Demo</display-name>
    <servlet>
        <servlet-name>ServletAdaptor</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>biz.zenoconsulting.jersey</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServletAdaptor</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>30</session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

Make sure the init-param value matches the same package namespace you chose to use (mine is biz.zenoconsulting.jersey) — Jersey will scan the class files for JAX-RS annotations in that package namespace.

Now, go back and edit pom.xml to setup Jetty as follows:

<build>
  <plugins>
    <plugin>
    <!-- JETTY 6 PLUGIN -->
    <groupId>org.mortbay.jetty</groupId>
    <artifactId>maven-jetty-plugin</artifactId>
    <version>6.1.14</version>
    <configuration>
        <scanIntervalSeconds>10</scanIntervalSeconds>
        <contextPath>/jersey-demo</contextPath>
        <connectors>
            <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
                <port>8088</port>
                <maxIdleTime>60000</maxIdleTime>
            </connector>
        </connectors>
        <webApp>${basedir}/target/jersey-demo</webApp>
        <requestLog implementation="org.mortbay.jetty.NCSARequestLog">
            <filename>target/yyyy_mm_dd.request.log</filename>
            <retainDays>90</retainDays>
            <append>true</append>
            <extended>true</extended>
            <logTimeZone>GMT</logTimeZone>
        </requestLog>
    </configuration>
       </plugin>
   </plugins>
</build>

Here we set up Jetty to launch an HTTP server on port 8088. It also generates an request log underneath the target directory that can assist you in debugging issues. Note also the context path here is set to /jersey-demo. Now, we can start Jetty:

davis@kafka:~/svn/jersey-demo$ mvn jetty:run
[INFO] Scanning for projects...
[INFO] Searching repository for plugin with prefix: 'jetty'.
[INFO] ------------------------------------------------------------------------
[INFO] Building Jersey Demo
[INFO]    task-segment: [jetty:run]
[INFO] ------------------------------------------------------------------------
[INFO] Preparing jetty:run
[WARNING] Removing: run from forked lifecycle, to prevent recursive invocation.
[INFO] [resources:resources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:compile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources]
[INFO] Using default encoding to copy filtered resources.
[INFO] [compiler:testCompile]
[INFO] Nothing to compile - all classes are up to date
[INFO] [jetty:run]
[INFO] Configuring Jetty for project: Jersey Demo
[INFO] Webapp source directory = /home/davis/svn/jersey-demo/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] web.xml file = /home/davis/svn/jersey-demo/src/main/webapp/WEB-INF/web.xml
[INFO] Classes = /home/davis/svn/jersey-demo/target/classes
2008-11-29 23:58:11.479::INFO:  Logging to STDERR via org.mortbay.log.StdErrLog
[INFO] Context path = /jersey-demo
[INFO] Tmp directory =  determined at runtime
[INFO] Web defaults = org/mortbay/jetty/webapp/webdefault.xml
[INFO] Web overrides =  none
[INFO] Webapp directory = /home/davis/svn/jersey-demo/src/main/webapp
[INFO] Starting jetty 6.1.14 ...
2008-11-29 23:58:11.595::INFO:  jetty-6.1.14
2008-11-29 23:58:11.908::INFO:  No Transaction manager found - if your webapp requires one, please configure one.
Nov 29, 2008 11:58:12 PM com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Scanning for root resource and provider classes in the packages:
  biz.zenoconsulting.jersey
Nov 29, 2008 11:58:12 PM com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Root resource classes found:
  class biz.zenoconsulting.jersey.MyServlet
Nov 29, 2008 11:58:12 PM com.sun.jersey.api.core.PackagesResourceConfig init
INFO: Provider classes found:
2008-11-29 23:58:13.154::INFO:  Opened /home/davis/svn/jersey-demo/target/2008_11_30.request.log
2008-11-29 23:58:13.214::INFO:  Started SelectChannelConnector@0.0.0.0:8088
[INFO] Started Jetty Server
[INFO] Starting scanner at interval of 10 seconds.

Cool, Jetty is running on port 8088, and Jersey found the MyServlet class. I can now go hit the URL http://localhost:8088/jersey-demo/hello/ and I'll see my "hello" string returned. Let's try out the POST method. I do this any number of ways:

  1. Using curl
  2. Making a simple HTML form
  3. Writing an http-unit test

Here's a simple HTML form you can try:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
 <html>
     <head>
          <title>Post an XML File</title>
     </head>
     <body>
         <form action="http://localhost:8088/jersey-demo/hello" enctype="multipart/form-data" method="post">
          <p>Send the XML File: <input type="file" name="xml">
          <p><input type="submit" value="Submit">
         </form>
     </body>
</html>

If I open the form in a browser, and browse to upload the pom.xml file, submit, and I see the XML is echoed back to the browser window.

Another very cool feature/integration with Maven and Jetty is setting up Jetty to auto-start and auto-stop before and after Maven's integration-test lifecycle phase. This enables you to write a simple integration test that hits the web server, and not have to worry about starting / stopping the server. This is very nice for Continuous Integration builds. Add this to the pom.xml to set this up:

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <excludes>
            <exclude>**/*IntegrationTest*.java</exclude>
        </excludes>
    </configuration>
    <executions>
        <execution>
            <id>integration-test</id>
            <goals>
                <goal>test</goal>
            </goals>
            <phase>integration-test</phase>
            <configuration>
                <excludes>
                    <exclude>none</exclude>
                </excludes>
                <includes>
                    <include>**/*IntegrationTest*.java</include>
                </includes>
            </configuration>
        </execution>
    </executions>
</plugin>

Here we've specified a naming pattern for test classes named **/*IntegrationTest*.java will only be run under maven's integration-test phase. If you run

 $ mvn test

these tests will be skipped, but if you run

 $ mvn integration-test

these tests will be run. This is nice for continuous integration systems.

Now you can write an integration test. Call it something like MyServletIntegrationTest.java. And you can run it via maven. When maven starts, it will automatically start Jetty on port 8088, and after the test, Jetty will be shut down. Here's an integration test skeleton you can play with:

package biz.zenoconsulting.jersey;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
 
import java.io.File;
 
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
 
import biz.zenoconsulting.jersey.MyServlet;
import biz.zenoconsulting.jersey.util.StringUtil;
 
import com.meterware.httpunit.PostMethodWebRequest;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebResponse;
 
/**
 * Test class for {@link MyServlet}
 * <p>
 * This is an integration test.
 * <p>
 * In order to pass these tests within an IDE, you have to first run Jetty. 
 * This is configured for you via maven, if you run the command:
 * 
 * <pre>
 * mvn jetty:run
 * </pre>
 * 
 * Alternatively, you can just run the maven command:
 * <pre>
 * mvn integration-test
 * </pre>
 * 
 * and maven will do everything for you (start jetty, run test, stop jetty).
 * <p>
 * Make sure the literal URL below matches that defined in pom.xml
 */
public class MyServletIntegrationTest {
 
    /** good XML input */
    private static final String XML = "./src/test/resources/xml/file.xml";
 
    /** URL */
    private static final String URL = "http://localhost:8088/jersey-demo/hello";
 
    private static final String ERR_MSG = 
        "Unexpected exception in test. Is Jetty Running at "+URL+" ? ->";
 
    private PostMethodWebRequest req;
    private WebConversation wc;
 
    /**
     * @throws java.lang.Exception
     */
    @Before
    public void setUp() {
        req = new PostMethodWebRequest(URL);
        wc = new WebConversation();
    }
 
    /**
     * @throws java.lang.Exception
     */
    @After
    public void tearDown() throws Exception {
    }
 
    /**
     * Test method for {@link biz.zenoconsulting.jersey.MyServlet#doGet()}.
     */
    @Test
    public void testDoGet() {
        try {
            WebResponse wr = wc.getResponse(URL);
            assertTrue(wr.getText().contains("hello"));
        } catch (Exception e) {
            fail(ERR_MSG + e);
        }
    }
 
    /**
     * Test method for
     * {@link biz.zenoconsulting.jersey.MyServlet#doPost()}
     */
    @Test
    public void testDoPost() {
        try {
            req.setParameter("xml", StringUtil.readFileAsString(XML));
            WebResponse resp = wc.getResponse(req);
            assertNotNull("response was null", resp);
            assertEquals(200, resp.getResponseCode());
        } catch (Exception e) {
            fail(ERR_MSG + e);
        }
    }
}

The test uses a simple static method I wrote that reads an XML file into a string:

public final class StringUtil {
 
    /**
     * Given the path to a file (e.g. XML file), it will return the contents
     * as a string.
     * 
     * @param path
     * @return
     * @throws IOException
     */
    public static String readFileAsString(String path) throws IOException {
        StringBuffer sb = new StringBuffer(1000);
        BufferedReader reader = new BufferedReader(new FileReader(path));
        char[] buf = new char[1024];
        int numRead = 0;
        while((numRead = reader.read(buf)) != -1) {
            sb.append(buf, 0, numRead);
        }
        reader.close();
        return sb.toString();
    }
}

Now create a directory /src/test/resources/xml/ and put an xml file in there (note I hard-coded the path to be ./src/test/resources/xml/file.xml. Re-run:

$ mvn integration-test

..and your integration test should be running and passing. The next step is to setup security with HTTPS and password authentication. I will extend this post to explain how to set that up very shortly.


Backlinks


Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License