RESTEasy is a Framework for building RESTful applications in Java. In this blog I will show how to easily build RESTful webservices that accept data from an HTML Form. We will also explore the possibilities to extend RESTEasy to handle more complex cases.
Note: All source code in this post is attached as a zipped Maven project.
Simple Example
So let’s look at a very simple example:
public class Person { private String firstName; private String lastName; } @Path("/person") public class PersonResource { private PersonRepository repository; @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) public Person savePerson(Person person) { return repository.save(person); } }
The intent of these two classes should be pretty clear: We want to expose the savePerson method as a WebService, accept data from a HTML form and produce JSON. The annotations used on the PersonResource are JAX-RS annotations.
Now notice that we both accept and return a Person object in the service method. We expect the RESTEasy framework to handle the unmarshalling from the Form fields and the marshalling to JSON. Luckily JAX-RS also comes with some handy annotations for this: The @FormParam annotation lets you define which Form field to map to which parameter or field. Unfortunately the JAX-RS specification does not talk about mapping forms to rich objects, so the signature for our service method would become
public Person savePerson ( @FormParam("firstName") String firstName, @FormParam("lastName") String lastName, ... ) {
which would become rather large and unwieldy rather soon, when the form has to include all the usual name and address fields.
This is where RESTEasy comes to the rescue with the @Form annotation. This annotation tells the RESTEasy framework to map the entire form onto a parameter object, relieving us from the task to specify all form fields in the parameter list of the service method. The code thus becomes:
public class Person { @FormParam("firstName") private String firstName; @FormParam("lastName") private String lastName; } @Path("/person") public class PersonResource { private PersonRepository repository; @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) public Person savePerson(@Form Person person) { return repository.save(person); } }
How to test this?
With these two simple classes, we’re done implementing our simple requirements. But how do we test this? RESTEasy comes with a JUnit extension that is very useful to test Resources.
@RunWith(MockitoJUnitRunner.class) public class PersonResourceTest extends BaseResourceTest{ private PersonResource resource; @Mock private PersonRepository repository; @Before public void setupResource() { MockitoAnnotations.initMocks(getClass()); resource = new PersonResource(); resource.setRepository(repository); (1) dispatcher.getRegistry().addSingletonResource(resource); } @After public void removeResource() { dispatcher.getRegistry().removeRegistrations(PersonResource.class); } @Test public void shouldSavePerson() throws Exception { (2) MockHttpRequest request = post("/person"); request.addFormHeader("firstName", "Maarten"); request.addFormHeader("lastName", "Winkels"); (3) ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass(Person.class); when(repository.save(personCaptor.capture())).thenAnswer(new ArgumentAnswer<Person>(0)); MockHttpResponse response = new MockHttpResponse(); (4) dispatcher.invoke(request, response); verify(repository).save(any(Person.class)); (5) Person person = capturePerson.getLastValue(); assertEquals("Maarten", person.getFirstName()); assertEquals("Winkels", person.getLastName()); (6) assertEquals(200, response.getStatus()); assertEquals("{\"firstName\":\"Maarten\",\"lastName\":\"Winkels\"}", response.getContentAsString()); } }
The test above is a true integration test as far as RESTEasy is concerned, in the sense that an HttpRequest is processed by the framework and triggers our PersonResource to produce a JSON response.
Mockito is used to be mock out the PersonRepository (which is not yet implemented), and to be able to check some behavior of the code in the test. Stepping through the code:
- The PersonResource with the mock for the repository is registered with the framework in the test. We need to register it as a singleton here, wince we have to prepare the resource with a mock. Notice that the class is removed in the @After method to ensure that the tests have no side-effects.
- A MockHttpRequest is prepared to be processed with certain values.
- The mock repository is setup to capture the argument coming into the request and to return the same argument as response. The ArgumentAnswer is a small extension of the Mockito/Hamcrest framework to enable the mock to return the parameter passed into the call on the mock as the return value.
- The request is executed on the dispatcher, capturing the response in a MockHttpResponse.
- The Person we captured is inspected to see that the values are copied from the request.
- The response is inspected for return value and content.
That was easy! We’ve integration tested our entire RESTful application without having to start a container!
A little more complex: nested object structures and forms
Let’s try something little more complex: Let’s assume the person has an address. We’ll code the test first:
@Test public void shouldSavePersonWithAddress() throws Exception { MockHttpRequest request = post("/person"); request.addFormHeader("firstName", "Maarten"); request.addFormHeader("lastName", "Winkels"); request.addFormHeader("address.street", "Dorpsstraat"); request.addFormHeader("address.houseNumber", "19a"); ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass(Person.class); when(repository.save(personCaptor.capture())).thenAnswer(new ArgumentAnswer<Person>(0)); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); verify(repository).save(any(Person.class)); Person person = capturePerson.getLastValue(); assertEquals("Maarten", person.getFirstName()); assertEquals("Winkels", person.getLastName()); assertEquals("Dorpsstraat", person.getAddress().getStreet()); assertEquals("19a", person.getAddress().getHouseNumber()); assertEquals(200, response.getStatus()); assertEquals("{\"firstName\":\"Maarten\",\"lastName\":\"Winkels\",\"address\":{\"street\":\"Dorpsstraat\",\"houseNumber\":\"19a\"}}", response.getContentAsString()); }
As you can see from the test, a person now has an address and we expect the form fields that start with "address." to be mapped to the fields in the Address class. In a sense, the form is now mapped to the nested object structure of person and address. Unfortunately neither JAX-RS nor RESTEasy has support for this. (RESTEasy does support a @Form annotation in a @Form mapped parameter class, but it does not support specifying a prefix, thus only supporting a single nested object of the same type.)
So we introduce a new annotation to indicate that a field is mapped to a part of the form fields:
public class Person { ... @NestedFormParams("address") private Address address; ... }
To the RESTEasy framework, this new annotation means a new way of injecting values into objects, which is rather similar to how values are injected into @Form annotated parameters. *Injectors are created by an InjectorFactory. The ExtendedInjectorFactory below extends the RESTEasy InjectorFactoryImpl to make RESTEasy aware of the new annotation.
public class ExtendedInjectorFactory extends InjectorFactoryImpl{ private final ResteasyProviderFactory factory; public ExtendedInjectorFactory(ResteasyProviderFactory factory) { super(factory); this.factory = factory; } @Override public ValueInjector createParameterExtractor(Class injectTargetClass, AccessibleObject injectTarget, Class type, Type genericType, Annotation[] annotations, boolean useDefault) { NestedFormParams param = FindAnnotation.findAnnotation(annotations, NestedFormParams.class); if (param != null) { return new NestedFormInjector(type, factory, param.value()); } return super.createParameterExtractor(injectTargetClass, injectTarget, type, genericType, annotations, useDefault); } }
In the test, the new InjectorFactory is registered with the ProviderFactory:
[java] @Before
public void setupResource() {
getProviderFactory().setInjectorFactory(new ExtendedInjectorFactory(getProviderFactory()));
…
}[/java]
Whenever a NestedFormParams annotation is found, a NestedFormInjector is created to handle the injection. Since the functionality is so similar to the FormInjector, it simply is an extension of that class.
public class NestedFormInjector extends FormInjector { private final String prefix; public NestedFormInjector(Class<?> type, ResteasyProviderFactory factory, String prefix) { super(type, factory); this.prefix = prefix; } @Override public Object inject(HttpRequest request, HttpResponse response) { if (!containsPrefixedName(request.getDecodedFormParameters().keySet())) { return null; } return super.inject(new PrefixedFormFieldsHttpRequest(prefix, request), response); } private boolean containsPrefixedName(Set<String> keys) { for (String key : keys) { if (key.startsWith(prefix)) { return true; } } return false; } }
To understand how the NestedFormInjector works, one has to understand how the FormInjector works. The form injector is created for a specific type. It uses a ConstructorInjector to create an instance of that type and a PropertyInjector to inject other values into this instance. The injection of these values is carried out by other injectors. If a field in the class is annotated with an @FormParam annotation, the FormInjector is used to inject the value from the form parameter into the field.
The NestedFormInjector works by restricting the access of any form injectors that are used for it’s properties to form fields that start with the given prefix (and a dot). This is achieved by wrapping the original HttpRequest in an PrefixedFormFieldHttpRequest. Objects of this class wrap the original form fields in a map that will add the prefix on each look up. The code can be found in the attached ZIP file.
Conclusion
By extending a few of the core classes of the RESTEasy framework, we have added the functionality to map a HTTP form to a complex association of two classes. This can be really useful when complex forms are posted.
A next step would be to map form fields to collections, lists or maps. I’ll write about that in a next post.