Skip to content
This repository was archived by the owner on Jan 10, 2019. It is now read-only.

REST API

Matthew Mason edited this page Sep 5, 2017 · 11 revisions
Table of Contents

Vert.x

Overview

The wayf-cloud is accessible via a RESTful API. The inbound requests are serviced with vert.x, a lightweight event-driven framework focused on throughput. Vert.x listens for inbound requests on an event loop. These events are immediately consumed and moved off of the event loop, freeing it up to accept more requests. After processing, the response is able to be written on any available thread.

The Reactive streams library is used to switch requests off of the inbound Vert.x event loop. It is important not to do any processing on the event loop so that it is free to service new requests.

Implementation

Vert.x

The root of the Vert.x implementation is com.atypon.wayf.verticle.WayfVerticle. It is in this class that the settings relevant for the web service are configured. What would traditionally be referred to as a controller in a MVC architecture is denoted in the WAYF cloud as an implementation of com.atypon.wayf.verticle.routing.RoutingProvider. Each implementation of the routing provider must tell the root verticle which endpoints it requires and how to route the. For example, all requests for the User type would be defined in com.atypon.wayf.verticle.routing.UserRouting.

Request Handling

In order to ensure the requirement that requests are immediately switched from the Vert.x event loop, com.atypon.wayf.verticle.WayfRequestHandlerFactory is provided. When defining how to handle an inbound request for Vert.x, handling of the request can be wrapped in a call to the factory. Handlers returned by the factory will automatically move the request off of the event loop, preventing the loop from being blocked.

In practice, this will look like:

public class UserRouting implements RoutingProvider {
    @Inject
    private WayfRequestHandlerFactory handlerFactory;

    @Override
    public void addRoutings(Router router) {
        router.post(USER_BASE_URL).handler(handlerFactory.single((rc) -> createUser(rc)));
        router.get(READ_USER).handler(handlerFactory.single((rc) -> readUser(rc)));
    }
}

In addition to moving the request to a processing thread, the WayfHandlerFactory will allow for uniform request processing across endpoint. That is, it can process the request and write the response to the client in a uniform manner. The developer does not need to handle serialization nor request context work.

Serialization Policy

Overview

In order for client requests to be sent over HTTP and read by the application, there must be a serialization policy that dictates how the Java objects and read from and written to JSON. The technology employed to handle this work is Jackson. Jackson is a reflection based tool that can serialize and deserialize JSON to POJOs (plain old java objects). Due to the close relationship of the object structure and the JSON it produces, it is important to have these objects be used strictly for data. There should be no business logic on them.

Implementation

There are two classes which reference the Jackson library, com.atypon.wayf.request.RequestReader and com.atypon.wayf.request.RequestWriter. The Jackson instance used by WAYF is actually managed by Vert.x. On server initialization and startup, the server will automatically detect which types require serialization. They classes will then be reflected upon to look for standard Jackson annotations. These annotations can provide customizations of the serialization policy such as name changes, field suppression, and object factories for polymorphic types.

Future Considerations

In a more mature solution, the Jackson ObjectMapper should be managed on its own and not as a part of the Vert.x library. This may then be accessed in a static manner. This pattern would ensure that each call to Jackson receives the same policy and removes the possibility of transformation errors.

In the current implementation, the API types are used in the business logic. This means that the Publisher type messaged in by an end-user will be the same Java type operated on by the core business logic. Considering how tightly the structure of the types are coupled with the serialized representations, this is not ideal. For example, renaming or restructuring a field for changes in business logic would cause a breaking API change. At some point there should be a translation layer that can map the versioned API types to version-less domain objects.

Request Context

Overview

Sometimes it is necessary to know information about how a particular request made it to the system. Understanding who made the request and what front-door was used can be extremely useful in debugging failure or profiling a system. To expose this information in an easily accessible way, the idea of the com.atypon.wayf.request.RequestContext object was created. All request level datapoints are stored in this object.

Implementation

As outlined in the "Request Handling" section above, all calls through the REST API are processed in a uniform manner. This is leveraged for the RequestContext. The inbound HTTP request is fed to the com.atypon.wayf.request.RequestContextFactory. This factory then scrapes all relevant information to a com.atypon.wayf.request.RequestContext object.

In order for the RequestContext to be accessible anywhere in the application stack, it is stored in ThreadLocal. This can be accessed via standard tool, com.atypon.wayf.request.RequestContextAccessor. This is a simple class but provides a uniform API for retrieving and setting the RequestContext.

Request/Response Processing

Overview

Most of the data sent to the WAYF REST API is transmitted via the HTTP request body or URL. The request body is serialized as a Java type as defined by the process in the "Serialization Policy" section. The URL values can be sent as path parameters or query parameters. However, in some requests, values may be read from HTTP request headers or the client's cookie.

In a similar fashion, most data response from the server are put in the HTTP response body. However, there are instances where data is set in the response headers and client's cookie.

Implementation

The input/output processing is standardized into two classes: com.atypon.wayf.request.RequestReader and com.atypon.wayf.request.RequestWriter. These encapsulate Vert.x specific functionality for reading and writing responses. Furthermore, these classes also wrap the serialization calls. So long as the core serialization policy has been successfully configured, the invoker does not need to know about the policy or its implementing technology.

Endpoint Design

Overview

While each type may not require a full API, the endpoints for each type should be implemented in a consistent and predictable manner. For version 1 of a User object with an ID of 123, the endpoints would look like:

CREATE - POST - /1/user
READ - GET - /1/user/123
UPDATE - PUT - /1/user/123
DELETE - DELETE - /1/user/123
FILTER - GET - /1/users?ids=123

With the exception of the "filter" endpoint, these are all standard REST implementations. The filter call is used to represent when the user intends to operate against the full set of User objects, hence the plural name of "users". For this, each filter criteria is passed in as a query param. For example, if the set of users that have a pre-determined set of IDs are required, this can be expressed with "ids=123,456,789". If, instead, the set of users with a first name of John are needed, this may be expressed with "firstName=John". Each query parameter constraint is "anded" with each other. So, if the query string looked like "ids=1,2&firstName=John" only Users with an id of 1 or 2 and a first name of John would be returned.

Implementation

The set of available query parameters is expressed on the backend as a query object. For a User, this is com.atypon.wayf.data.user.UserQuery. The query object is constructed in the controller (UserRouting) with the utility method com.atypon.wayf.request.RequestParamMapper.mapParams. This method will map all query parameters that match a field on the query definition.

The query object is then passed down the application stack to the database and used in the DB constraints. The query fields are represented in the constraint as two conditions. One is whether the field exists and the other constrains the data against the input. For user IDs, this looks like:

WHERE (:nullIds OR u.id IN (:ids))

Where UserQuery has a method:

    /**
     * Database use only
     */
    public boolean isNullIds() {
        return ids == null;
    }

This pattern prevents the database from constraining against fields that were omitted as a filter criteria.

Lastly, these calls have the potential to return large datasets. Every call to the server supports "limit" and "offset" query parameters for paging through the result set. If a limit is not specified, the DB utilities will default to 30. The "Link" response header will alert the client if there is more data in the resultset to paginate over. This is demonstrated in the com.atypon.wayf.request.ResponseWriterTest test case.