Monday, June 17, 2013

Jargon and RestEasy - some notes on what I've run into

I'm starting to work on a formal REST API for iRODS.  This is coming from multiple projects, but this first one gives me a chance to build the skeleton and set down some practices for later.  The project itself is here on the RENCI GForge.

For several reasons, I decided to roll with JBoss RestEasy.  Not least of which is their compliance with JAX-RS, which goes some way towards future-proofing any work I do.  There is also a need to do some S/MIME encryption of messages, and it looks like RestEasy handles this well enough.

RestEasy is not without its headaches and frustrations.  A good deal of this frustration has to do with integrating Spring beans into the mix, which I use a lot in Jargon.  The docs don't seem to reflect actual usage in this area, both for service development, and for testing.  In the Spring Integration section of the RestEasy docs, you get this example:


<web-app>
   <display-name>Archetype Created Web Application</display-name>

   <servlet>
      <servlet-name>Spring</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet;</servlet-class>
   </servlet>

   <servlet-mapping>
      <servlet-name>Spring</servlet-name>
      <url-pattern>/*</url-pattern>
   </servlet-mapping>


</web-app>

For your web.xml, and:


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
">

    <!-- Import basic SpringMVC Resteasy integration -->
    <import resource="classpath:springmvc-resteasy.xml"/>

For the Spring configuration file.

It doesn't work, it doesn't load your beans...

I dug around a lot (and sorry, I cannot retrace my steps and refer you to some of the info I found!), and by combining several proposed solutions I found that this worked...

First, for the web.xml document:


 irods-rest

 
  contextConfigLocation
  /WEB-INF/rest-servlet.xml
 

   
        resteasy.media.type.param.mapping
        contentType
    
    
    
    
            resteasy.document.expand.entity.references
            false
         
 
 

 
 
  org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap
 

 
 
  org.jboss.resteasy.plugins.spring.SpringContextLoaderListener
 

 
 
  resteasy.servlet.mapping.prefix
  /rest
 
 
  rest
  org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher
 

 
 
  rest
  /rest/*
 

 
The things to highlight here include the fact that I had to comment out the RestEasy component scan context parameter, add the contextConfigLocation parameter, and wire in the Spring RestEasy integration components by hand.  In this configuration, it does load my custom beans, and then it loads my RestEasy services by the fact that I added direct Spring configuration for that component scan:



 
 
 
 
 
 

 
OK, so that seems to be OK now. I have a service running, it's doing content negotiation, it's wired in my Jargon components...now, how do I test it? Jargon has a lot of tests, I don't need to test Jargon or iRODS, so I decided to test at the http request level. Given this, I was not too excited at testing with mocks. Mocks seem like a lot of trouble, and might mask some of the subtleties involved, given that it's pretty easy (I thought) to test all of this with an embedded servlet container. This seems ideal...test end-to-end as the user sees things, create sample code at the same time. What could be better!

The JBoss docs don't go into great detail about best practices for testing RestEasy apps, but clearly the TJWS embedded container seemed obvious.  They provide a bit of pseudo-code in the docs, and, unfortunately, it does not work:



   public static void main(String[] args) throws Exception 
   {
      final TJWSEmbeddedJaxrsServer tjws = new TJWSEmbeddedJaxrsServer();
      tjws.setPort(8081);

      tjws.start();
      org.jboss.resteasy.plugins.server.servlet.SpringBeanProcessor processor = new SpringBeanProcessor(tjws.getDeployment().getRegistry(), tjws.getDeployment().getFactory();
      ConfigurableBeanFactory factory = new XmlBeanFactory(...);
      factory.addBeanPostProcessor(processor);
   }


At least we see a bit that looks like we can adapt to the setup() method of a JUnit test case.  Given that clue, I found some very helpful posts, such as this one from 'eugene' (thanks eugene!).  But even this did not work, as ApplicationContext was not @Autowired.  I kept getting NPEs.

This got me very close, and I've used SpringJUnit4ClassRunner extensively for Hibernate/JPA based applications in the iDrop suite, so I felt like I just needed to hack on that a bit and I could get there.  The missing piece came from 'Daff' (thanks Daff!) who pointed out the ability to have your JUnit test case extend ApplicationContextAware in his post.

I tried to wire this into the @BeforeClass annotated startup() method with a static ApplicationContext variable.  Needless to say, that did not work, and was always 'null'.  It ended up that I had to place that server startup code in the @Before annotated method, which runs on instance variables, and the context was then available.  That's a little bit hinky, but given that I have a very short window for this project, I rolled with a solution there that saves the ApplicationContext in a static variable, and checks to see (singleton-like) if an instance has been created yet for the JUnit class.  This is working fine so far, and only smells a tiny bit.  I may revisit it, but I'm happy enough to get a base testing strategy defined.

So, here's a JUnit test that works with Spring configured beans:


package org.irods.jargon.rest.commands;

import junit.framework.Assert;

import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.plugins.server.tjws.TJWSEmbeddedJaxrsServer;
import org.jboss.resteasy.plugins.spring.SpringBeanProcessor;
import org.jboss.resteasy.plugins.spring.SpringResourceFactory;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:jargon-beans.xml",
  "classpath:rest-servlet.xml" })
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
  DirtiesContextTestExecutionListener.class })
public class UserServiceTest implements ApplicationContextAware {

 private static TJWSEmbeddedJaxrsServer server;

 private static ApplicationContext applicationContext;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {

 }

 @AfterClass
 public static void tearDownAfterClass() throws Exception {
  if (server != null) {
   server.stop();
  }
 }

 @Before
 public void setUp() throws Exception {
  if (server != null) {
   return;
  }

  server = new TJWSEmbeddedJaxrsServer();
  server.setPort(8888);
  ResteasyDeployment deployment = server.getDeployment();
  server.start();
  Dispatcher dispatcher = deployment.getDispatcher();
  SpringBeanProcessor processor = new SpringBeanProcessor(dispatcher,
    deployment.getRegistry(), deployment.getProviderFactory());
  ((ConfigurableApplicationContext) applicationContext)
    .addBeanFactoryPostProcessor(processor);

  SpringResourceFactory noDefaults = new SpringResourceFactory(
    "userService", applicationContext, UserService.class);
  dispatcher.getRegistry().addResourceFactory(noDefaults);

 }

 @After
 public void tearDown() throws Exception {
 }

 @Test
 public void testGetUserJSON() throws Exception {

  final ClientRequest clientCreateRequest = new ClientRequest(
    "http://localhost:8888/user/mconway?contentType=application/json");

  final ClientResponse clientCreateResponse = clientCreateRequest
    .get(String.class);
  Assert.assertEquals(200, clientCreateResponse.getStatus());
  String entity = clientCreateResponse.getEntity();
  Assert.assertNotNull(entity);
 }

 

 @Override
 public void setApplicationContext(final ApplicationContext context)
   throws BeansException {
  applicationContext = context;
 }

}

























So it might not be 'ideal', but I can move on and get this thing done.  I'd appreciate any pointers or refinements, and hopefully this will at least get you running and save you similar headaches.