Overview

Since version 5.8.0, Tapestry provides out-of-the-box support for writing REST endpoints as regular event handler methods in page classes. They work in the same way the activate event (i.e. onActivate() methods) work, including how event handler method parameters work. The @RequestBody annotation was created so event handler methods can access the request body. The @StaticActivationContextValue annotation was created so you can write event handler methods that are only called when one or more parts of the URL path match given values. Both annotations are not REST-specific and can be used in any event handler method. A new subproject/JAR, tapestry-rest-jackson, automates the use of Jackson Databind to make JSON conversions. tapestry-Swagger/OpenAPI 3.0 descriptions are generated automatically and can be easily customized. A new subproject/JAR, tapestry-openapi-viewer, provides an out-of-the-box viewer for the generated OpenAPI description using Swagger UI. For a Tapestry REST support example project, check out https://github.com/thiagohp/tapestry-rest-example.

Some important warnings:

  1. Tapestry's REST support isn't an implementation of JAX-RS, so they expect any of its concepts to work here. It's REST implemented in a Tapestry way.
  2. REST endpoint event handler methods in components are ignored just like onActivate() is.

The following HTTP methods are supported:

HTTP methodTapestry event name

EventConstants

constant name

Event handler

method name

GEThttpGetHTTP_GETonHttpGet
POSThttpPostHTTP_POSTonHttpPost
DELETEhttpDeleteHTTP_DELETEonHttpDelete
PUThttpPutHTTP_PUTonHttpPut
HEADhttpHeadHTTP_HEADonHttpHead
PATCHhttpPatchHTTP_PATCHonHttpPatch

Writing REST endpoints

Writing a REST endpoint in Tapestry is exactly the same as writing onActivate() method in a page class. Everything is the same: parameter handling, returned value processing, precedence rules, class inheritance, URLs, etc. If you know how to write onActivate() methods, you already know almost everything you need how to write a REST endpoint event handler.   There are only 2 small differences between onActivate() and REST endpoint event handler methods:

  1. REST event handler methods are invoked after the onActivate()
  2. The event name is different, according to the HTTP method to be handled.

So, for example, if you want a REST endpoint with URL /userendpoint/[id], supposing the id is a Long, handling the GET HTTP method, you can just write the following page class and event handler:

public class UserEndpoint {

    Object onHttpGet(Long id) { // It could also be @OnEvent(EventConstants.HTTP_GET) Object anyMethodName(Long id)
        (...)
    }
	(...)
}

The example above could also be written using the @OnEvent annotation and would work the same:

public class UserEndpoint {

    @OnEvent(EventConstants.HTTP_GET) 
	Object getById(Long id) { // or any other method name
        (...)
    }
	(...)
}

Reading the request body with @RequestBody

Many times, specially with POST, PUT and PATCH requests, the data is sent through the request body. To get this data, the event handler method needs to add a parameter with the @RequestBody annotation. It has a single property, allowEmpty, with false as its default value, which defines whether an empty request body is empty. If not, an exception will be thrown.

@OnEvent(EventConstants.HTTP_PUT) 
Object save(@RequestBody User user) {
	(...)
}

@OnEvent(EventConstants.HTTP_PUT) 
Object save(Long id, @RequestBody User user) {
	(...)
}

The following types are supported out-of-the-box:

  • String
  • Reader
  • InputStream
  • JSONObject
  • JSONArray
  • Primitive types and their wrapper types

The actual conversion logic is implemented in the HttpRequestBodyConverter service, which is defined is an ordered configuration of HttpRequestBodyConverter instances. The service calls all contributed instances until one of them returns a non-null value. If none of them returns a non-null value, it falls back to trying to find a coercion, direct or indirect, from HttpServletRequest to the desired type. 

Here's one example of implementing an new HttpRequestBodyConverter then the code added to AppModule or any other Tapestry-IoC module class to have it used by @RequestBody:

/**
 * Converts the body of HTTP requests to {@link User} instances. It delegates this task
 * to {@link UserService#toObject(String)}.
 */
public class UserHttpRequestBodyConverter implements HttpRequestBodyConverter {
    
    private final UserService userService;

    public UserHttpRequestBodyConverter(UserService userService) {
        super();
        this.userService = userService;
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> T convert(HttpServletRequest request, Class<T> type) {
        T value = null;
        // Check whether this converter handles the given type
        if (User.class.equals(type)) {
			// Actual conversion logic
            try {
                value = (T) userService.toObject(IOUtils.toString(request.getReader()));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return value;
    }

}
UserHttpRequestBodyConverter contribution
    public static void contributeHttpRequestBodyConverter(
            OrderedConfiguration<HttpRequestBodyConverter> configuration) {
        
        configuration.addInstance("User", UserHttpRequestBodyConverter.class); // automatic instantiation and dependency injection
		// or configuration.add("User", new UserHttpRequestBodyConverter(...));
    }

Answering REST requests

Just like any other Tapestry event handler method, the returned value defines what gets to be sent to the user agent making the request. This logic is written in ComponentEventResultProcessor implementations, usually but not necessarily one per return type/class, which are contributed to the ComponentEventResultProcessor service. These implementations can also set additional HTTP headers and set the HTTP status code.

REST requests responses usually fall into 2 types: ones just returning HTTP status and and headers (for example, HEAD and DELETE requests) and ones returning that and also content (for example, GET, sometimes other methods too).

Content responses

For content responses, Tapestry has out-of-the-box support for StreamResponse (mostly binary content),  TextStreamResponse (simple text content), JSONArray (since Tapestry 5.8.0) and JSONObject (since 5.8.0).  Here's one example for adding support for a class, User, converting it to the JSON format:

/**
 * Handles {@link User} instances when they're returned by event handler methods.
 * Heavily inspired by {@link JSONCollectionEventResultProcessor} from Tapestry itself.
 */
final public class UserComponentEventResultProcessor 
        implements ComponentEventResultProcessor<User> {

    private final Response response;

    private final ContentType contentType;
    
    private final UserService userService;

    public UserComponentEventResultProcessor(Response response,
            @Symbol(TapestryHttpSymbolConstants.CHARSET) String outputEncoding,
            UserService userService)    {
        this.response = response;
        this.userService = userService;
        contentType = new ContentType(InternalConstants.JSON_MIME_TYPE).withCharset(outputEncoding);
    }

    public void processResultValue(User user) throws IOException
    {
        PrintWriter pw = response.getPrintWriter(contentType.toString());
        pw.write(userService.toJsonString(user));
        pw.close();
		// You could also set extra HTTP headers or the status code here
    }        
}
UserComponentEventResultProcessor contribution
    public void contributeComponentEventResultProcessor(
            MappedConfiguration<Class, ComponentEventResultProcessor> configuration) {
        configuration.addInstance(User.class, UserComponentEventResultProcessor.class);
    }

Non-content responses

For responses without content, just HTTP status and headers, and also for simple String responses, Tapestry 5.8.0 introduced the HttpStatus class. You can create instances of it by either using its utility static methods that match HTTP status names like ok(), created(), accepted()notFound() and forbidden() or using one of its constructors. In both cases, you can customize the response further by using a fluent interface with methods for header-specific methods like withLocation(url) and withContentLocation(url) or the generic withHttpHeader(String name, String value). Check the HttpStatus JavaDoc for the full list of methods.

    @OnEvent(EventConstants.HTTP_PUT) 
    Object save(@RequestBody User user) {
        userService.save(user);
        return HttpStatus.created()
                .withContentLocation("Some URL")
                .withHttpHeader("X-Something", "X-Value");
    }