REST

This document is referring to a past Scout release. Please click here for the recent version.

REST Resource Conventions

Table 1. HTTP Methods for RESTful Services
HTTP Method CRUD Description

POST

Create

Is most-often used to create new resources.

POST is not idempotent. Making two identical POST requests will most-likely result in two resources containing the same information or the action executed twice.

GET

Read

Only used to read or retrieve a representation of a resource.

According to the HTTP specification GET (and HEAD) requests are used to read data and must not change anything! If a REST API wants to violate the specification, such requests must be protected against CSRF which is not enabled for GET and HEAD requests by default. See the Scout Bean org.eclipse.scout.rt.rest.csrf.AntiCsrfHelper for more details.

GET requests are idempotent, which means that making multiple identical requests ends up having the same result as a single request (assuming the data has not been changed in the meantime).

PUT

Update/Replace

Is most-often used to update resources.

PUT expects to send the complete resource (not like PATCH) and is idempotent. In other words, if you create or update a resource using PUT and then make that same call again, the resource is still there and still has the same state as it did with the first call.

If, for instance, calling PUT on a resource increments a counter within the resource, the call is no longer idempotent. In such a scenario it is strongly recommended to use POST for non-idempotent requests.

PATCH

Update

PATCH is used to update resources. The PATCH request typically only contains the changes to the resource, not the complete resource.

PATCH is not required to be idempotent. But it is possible to implement it in a way to be idempotent, which also helps prevent bad outcomes from collisions between multiple requests on the same resource.

DELETE

Delete

Used to delete a resource.

DELETE operations are idempotent concerning the result but may return another status code after the first deletion (e.g. 404 NOT FOUND).

REST Resource Provider

A REST resource using the JAX-RS API is implemented by a POJO class annotated with a set of annotations.

The Scout module org.eclipse.scout.rt.rest contains the basic IRestResource marker interface which integrates REST resources within the Scout framework. The interface is annotated by @Bean allowing the Scout platform to load and register all REST resources automatically at startup using the Jandex class inventory.

Listing 1. Example: REST resource
@Path("example")
public class ExampleResource implements IRestResource {

  @GET
  @Path("{id}")
  @Produces(MediaType.APPLICATION_JSON)
  public ExampleEntityDo getExampleEntity(@PathParam("id") String id) {
    return BEANS.get(ExampleEntityDo.class)
        .withName("example-" + id)
        .withValues(1);
  }
}

REST Resource Registration

All available REST resources are automatically registered by the RestApplication class while the Scout platform startup.

Add the following snippet to your web.xml file to expose your REST API using the /api context path:

Listing 2. web.xml
<!-- JAX-RS Jersey Servlet -->
<servlet>
  <servlet-name>api</servlet-name>
  <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
  <init-param>
    <param-name>javax.ws.rs.Application</param-name>
    <param-value>org.eclipse.scout.rt.rest.RestApplication</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

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

Extend REST Application

The JAX-RS application API (javax.ws.rs.core.Application) allows a REST application implementation to specify a set of classes, a set of singleton instances and a map of custom properties to be registered. The Scout implementation of the REST application class org.eclipse.scout.rt.rest.RestApplication allows contributing classes, singletons and properties without needing to extend the RestApplication class.

Three different contributor interfaces are available for contributions:

  • IRestApplicationClassesContributor to contribute any classes

  • IRestApplicationSingletonsContributor to contribute any object instances (singletons)

  • IRestApplicationPropertiesContributor to contribute key/value properties

Listing 3. Example class contributor
public static class ExampleClassContributor implements IRestApplicationClassesContributor {
  @Override
  public Set<Class<?>> contribute() {
    return Collections.singleton(MyCustomExample.class);
  }
}

Data Objects

Scout data objects may be used as request and response objects for REST APIs. See Data Objects for details and examples.

Marshaller

A REST API may be used by non-Java consumers. In order to communicate using a platform-independent format, usually REST services use JSON as transport format. The marshaller between Java data objects and JSON is abstracted in the JAX-RS specification. Using the @Produces(MediaType.APPLICATION_JSON) annotation, each REST service method specifies the produced data format.

The Scout REST integration uses the popular Jackson library as default marshaller.

RunContext

Like a usual service call using the Scout service tunnel a REST request must ensure that processing of the request takes place within a RunContext. The HttpServerRunContextFilter or HttpRunContextFilter can be used to intercept incoming REST requests and wrap them within a Scout RunContext. HttpServerRunContextFilter can be used if a Scout server dependency is available. Optionally this filter also supports the creation of a Scout server session if this should be required (stateful). Refer to the javadoc for more details. The HttpRunContextFilter on the other hand does not provide session support and is always stateless.

Therefore, a REST resource implementation is not required to deal with setting up a RunContext to wrap the request within each method. The filter must be added in the web.xml configuration file and should be configured to be called after the authentication filter. The filter expects that the authentication has been performed and that a subject is available (JAAS context). All following filters and servlets and thus also the REST resources run automatically in the correct context.

Listing 4. web.xml registration example for HttpServerRunContextFilter.
<filter>
   <filter-name>HttpServerRunContextFilter</filter-name>
   <filter-class>org.eclipse.scout.rt.server.context.HttpServerRunContextFilter</filter-class>
   <init-param>
     <param-name>session</param-name>
     <param-value>false</param-value>
   </init-param>
 </filter>

<filter-mapping>
  <filter-name>HttpServerRunContextFilter</filter-name>
  <url-pattern>/api/*</url-pattern>
</filter-mapping>

Beside the subject and other attributes the HttpServerRunContextFilter and HttpRunContextFilter setup the Correlation ID, as well as the locale. Both values are read from the incoming request header, the caller must ensure that the headers Accept-Language and X-Scout-Correlation-Id are set accordingly.

Dependency Management

Scout REST services based on JAX-RS using the Jersey library and the Jackson JSON marshaller need a maven dependency to jersey-media-json-jackson in the application pom.xml. This enables the use of Jackson as JAX-RS marshaller with the Jersey JAX-RS implementation. Additionally, a dependency to the Scout module org.eclipse.scout.rt.rest.jackson is necessary. This module adds a set of Jackson additions in order to use the Jackson library together with Scout data objects.

Listing 5. Dependency section of pom.xml to use Scout REST services with Jackson & Jersey
<!-- JAX-RS Jersey -->
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.inject</groupId>
    <artifactId>jersey-hk2</artifactId>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
</dependency>

<!-- Jackson/Scout integration -->
<dependency>
    <groupId>org.eclipse.scout.rt</groupId>
    <artifactId>org.eclipse.scout.rt.rest.jackson</artifactId>
</dependency>

REST Client

The Scout module org.eclipse.scout.rt.rest offers a set of helper classes in order to call REST services.

Each REST service endpoint is represented by a specific REST resource client helper class. The (usually application scoped bean) class is used to specify the resource URL and additional properties used to build up the connection (authentication, additional headers,…​). Further it provides a call-back method for transforming unsuccessful responses into appropriate exception.

At least the REST resource’s base URI must be specified:

Listing 6. Example: REST resource client helper
public class ExampleRestClientHelper extends AbstractRestClientHelper {

  @Override
  protected String getBaseUri() {
    return "https://api.example.org/"; (1)
  }

  @Override
  protected void configureClientBuilder(ClientBuilder clientBuilder) {
    super.configureClientBuilder(clientBuilder);
    clientBuilder.property(RestClientProperties.COOKIE_SPEC, CookieSpecs.STANDARD);
    clientBuilder.property(RestClientProperties.PROXY_URI, "http://my.proxy.com");
  }

  @Override
  protected RuntimeException transformException(RuntimeException e, Response response) { (2)
    if (response != null && response.hasEntity()) {
      ErrorDo error = response.readEntity(ErrorResponse.class).getError();
      throw new VetoException(error.getMessage())
          .withTitle(error.getTitle());
    }
    return e;
  }
}
1 Declare base uri.
2 Custom exception transformer that is used as default strategy for all invocations prepared by this helper. (This is just for demonstration. Better extend org.eclipse.scout.rt.rest.client.proxy.AbstractEntityRestClientExceptionTransformer).

Based on the helper class, an example REST resource client may be implemented:

Listing 7. Example: REST resource client
public class ExampleResourceClient implements IRestResourceClient {

  protected static final String RESOURCE_PATH = "example";

  protected ExampleRestClientHelper helper() {
    return BEANS.get(ExampleRestClientHelper.class);
  }

  public ExampleEntityDo getExampleEntity(String id) {
    WebTarget target = helper().target(RESOURCE_PATH)
        .property(RestClientProperties.FOLLOW_REDIRECTS, false)
        .path("/{id}")
        .resolveTemplate("id", id);

    return target.request()
        .accept(MediaType.APPLICATION_JSON)
        .get(ExampleEntityDo.class); (1)
  }

  public ExampleEntityDo updateExampleEntity(String id, ExampleEntityDo entity) {
    WebTarget target = helper().target(RESOURCE_PATH)
        .path("/{id}")
        .resolveTemplate("id", id);

    return target.request()
        .accept(MediaType.APPLICATION_JSON)
        .post(Entity.json(entity), ExampleEntityDo.class); (2)
  }

  public void deleteExampleEntity(String id) {
    WebTarget target = helper().target(RESOURCE_PATH)
        .path("/{id}")
        .resolveTemplate("id", id);

    Response response = target.request().delete(); (3)
    response.close();
  }

  public ExampleEntityDo getExampleEntityCustomExceptionHandling(String id) {
    WebTarget target = helper().target(RESOURCE_PATH, this::transformCustomException) (4)
        .path("/{id}")
        .resolveTemplate("id", id);

    return target.request()
        .accept(MediaType.APPLICATION_JSON)
        .get(ExampleEntityDo.class);
  }

  protected RuntimeException transformCustomException(RuntimeException e, Response r) {
    if (r != null && r.hasEntity() && MediaType.TEXT_PLAIN_TYPE.equals(r.getMediaType())) {
      String message = r.readEntity(String.class);
      throw new VetoException(message);
    }
    return e;
  }
}
1 HTTP GET example: Directly read response into an object. Exceptions are transformed transparently and the underlying resources are released (e.g. HTTP client).
2 HTTP POST example: Again, directly read the response into an object.
3 HTTP DELETE example: This delete operation does not send a response if it was successful. Hence close the returned Response explicitly to release underlying resources (see next line). Note: Unsuccessful responses are already handled by the REST client proxy.
4 Use custom exception transformer.

REST Client Properties

The Scout REST Client implementation offers a set of properties to customize the underlying REST- and HTTP client, see org.eclipse.scout.rt.rest.client.RestClientProperties for a list of supported properties.

Properties can be set on the REST client during initialization (valid for all requests):

Listing 8. Setting properties for REST Client for all requests
  @Override
  protected void configureClientBuilder(ClientBuilder clientBuilder) {
    super.configureClientBuilder(clientBuilder);
    clientBuilder.property(RestClientProperties.COOKIE_SPEC, CookieSpecs.STANDARD);
    clientBuilder.property(RestClientProperties.PROXY_URI, "http://my.proxy.com");
  }

Some properties (see JavaDoc for details) may also be set on a request level:

Listing 9. Setting properties for REST Client for a single requests, e.g. setting FOLLOW_REDIRECTS to false
  public ExampleEntityDo getExampleEntity(String id) {
    WebTarget target = helper().target(RESOURCE_PATH)
        .property(RestClientProperties.FOLLOW_REDIRECTS, false)
        .path("/{id}")
        .resolveTemplate("id", id);

    return target.request()
        .accept(MediaType.APPLICATION_JSON)
        .get(ExampleEntityDo.class); (1)
  }

REST Client HTTP Proxy

There are multiple possibilities to configure a REST client to use a HTTP proxy:

  • Directly on REST client instance: see org.eclipse.scout.rt.rest.client.RestClientProperties.PROXY_URI (and PROXY_USER / PROXY_PASSWORD properties)

  • Using the dynamic Scout org.eclipse.scout.rt.shared.http.proxy.ConfigurableProxySelector, see example configuration:

Listing 10. Setting HTTP proxy for outgoing requests to *.example.com
scout.http.proxyPatterns[0]=.*\.example.com(:\d+)?=127.0.0.1:8888

REST Cancellation Support

REST and the underlying HTTP protocol do not provide an explicit way to cancel running requests. Typically, a client terminates its connection to the HTTP server if it is no longer interested in the response. REST resources would have to monitor TCP connections and interpret a close as cancellation. Depending on the abstraction of the REST framework, connection events are not passed through and the cancellation is only recognized when the response is written to the closed connection. Until this happens, however, backend resources are used unnecessarily.

Scout’s standard REST integration implements the described approach by closing the connection without any further action. It is not possible to react to this on the resource side.

In order to enable a real cancellation, Scout also provides all necessary elements to assign an ID to a request, to manage these IDs in the backend during execution and to cancel transactions in the event of a cancellation. The following steps must be taken for their use:

Cancellation Resource and Resource Client

Scout does not impose nor provide a cancellation resource. It must be implemented by the project:

Listing 11. Example: REST cancellation Resource
@Path("cancellation")
public class CancellationResource implements IRestResource {

  @PUT
  @Path("{requestId}")
  public void cancel(@PathParam("requestId") String requestId) {
    String userId = BEANS.get(IAccessControlService.class).getUserIdOfCurrentSubject(); (1)
    BEANS.get(RestRequestCancellationRegistry.class).cancel(requestId, userId); (2)
  }
}
1 Resolve the userId of the current user. This is optional and may depend on the current project.
2 Invoke the cancellation registry for the given requestId and userId.
Listing 12. Example: REST cancellation Resource Client
public class CancellationResourceClient implements IRestResourceClient {

  protected static final String RESOURCE_PATH = "cancellation";

  protected CancellationRestClientHelper helper() {
    return BEANS.get(CancellationRestClientHelper.class);
  }

  public void cancel(String requestId) {
    WebTarget target = helper().target(RESOURCE_PATH)
        .path("{requestId}")
        .resolveTemplate("requestId", requestId);

    Response response = target.request()
        .put(Entity.json(""));
    response.close();
  }
}

Install Cancellation Request Filter

To assign an ID to each request, an appropriate client request filter must be registered:

Listing 13. Example: Register Client Request Cancellation Filter in REST Client Helper
public class CancellationRestClientHelper extends AbstractRestClientHelper {

  @Override
  protected String getBaseUri() {
    return "https://api.example.org/";
  }

  @Override
  protected void registerRequestFilters(ClientBuilder clientBuilder) {
    super.registerRequestFilters(clientBuilder);
    clientBuilder.register(new RestRequestCancellationClientRequestFilter(this::cancelRequest)); (1)
  }

  protected void cancelRequest(String requestId) {
    BEANS.get(CancellationResourceClient.class).cancel(requestId); (2)
  }
}
1 Register the RestRequestCancellationClientRequestFilter that assigns a UUID to every request, which is sent as an HTTP header named X-ScoutRequestId.
2 Binds the actual cancel-operation to the cancel Method (in this case the cancellation rest resource client from above).

Implement Cancellation Servlet Filter

Requests arriving at the backend need to be registered in the cancellation registry. This is done by a servlet filter (Note: REST container filters would have two issues: 1. there is no real interceptor around the resource call, but only a ContainerRequestFilter that is invoked before and a ContainerResponseFilter which is invoked after the the request is passed to the resource. 2. Cancellation in Scout is tied to an ITransaction that are managed by a RunContext and observed and controlled by a RunMonitor. Depending on sub-RunContexts and their transaction isolation it might happen, that the transaction visible in a container filter is not controlled by the currently active RunMonitor. Therefore, a cancel request would not cancel the transaction.)

Listing 14. Example: Register client request cancellation filter in Rest Client Helper
public class RestRequestCancellationServletFilter extends AbstractRestRequestCancellationServletFilter {

  @Override
  protected Object resolveUserId(HttpServletRequest request) {
    return BEANS.get(IAccessControlService.class).getUserIdOfCurrentSubject(); (1)
  }
}
1 Implement the same userId Lookup as in the CancellationResource.

Finally, declare the servlet filter in your web.xml:

Listing 15. web.xml registration example for RestRequestCancellationFilter.
<filter>
   <filter-name>RestRequestCancellationFilter</filter-name>
   <filter-class>org.eclipse.scout.docs.snippets.rest.RestRequestCancellationServletFilter</filter-class>
 </filter>

<filter-mapping>
  <filter-name>RestRequestCancellationFilter</filter-name>
  <url-pattern>/api/*</url-pattern>
</filter-mapping>
Make sure the cancellation filter is registered after the HttpServerRunContextFilter.

REST Multipart Support

Due to missing support for multipart handling in official JAX-RS API, Scout provides an own implementation allowing to use multipart requests without requiring other third party libraries.

Listing 16. Example: Client
  public void pushContent(BinaryResource resource, String displayText) {
    MultipartMessage multipartMessage = BEANS.get(MultipartMessage.class)
        .addPart(MultipartPart.ofFile("resource", resource.getFilename(), new ByteArrayInputStream(resource.getContent())))
        .addPart(MultipartPart.ofField("displayText", displayText));

    WebTarget target = helper()
        .target(RESOURCE_PATH)
        .path("/content");

    Response response = target.request()
        .accept(MediaType.APPLICATION_JSON)
        .post(multipartMessage.toEntity());
    response.close();
  }
Listing 17. Example: Server
  @POST
  @Path("content")
  @Consumes(MediaType.MULTIPART_FORM_DATA)
  public Response pushContent(@HeaderParam("Content-Type") MediaType mediaType, InputStream inputStream) {
    File file = null;
    String displayText = null;

    IMultipartMessage multipartMessage = IMultipartMessage.of(mediaType, inputStream);
    while (multipartMessage.hasNext()) {
      try (IMultipartPart part = multipartMessage.next()) {
        switch (part.getPartName()) {
          case "resource": // file field part
            String[] parts = FileUtility.getFilenameParts(part.getFilename());
            file = IOUtility.createTempFile(part.getInputStream(), parts[0], parts[1]);
            break;
          case "displayText": // text field part
            displayText = IOUtility.readStringUTF8(part.getInputStream());
            break;
          default:
            throw new VetoException("Unexpected part {}", part.getPartName());
        }
      }
      catch (Exception e) {
        throw new PlatformException("Failed to handle multipart", e);
      }
    }

    // do something
    System.out.println(displayText + ": " + file.getAbsolutePath());

    return Response.ok().build();
  }