REST
This document is referring to a past Scout release. Please click here for the recent version. |
REST Resource Conventions
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 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.
@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 an implementor of IServletContributor
in your UiServletContributors
container class to expose your REST API using the /api
context path (set @Order
appropriately):
/**
* JAX-RS Jersey Servlet.
*/
@Order(20)
public static class ApiServletContributor implements IServletContributor {
@Override
public void contribute(ServletContextHandler handler) {
ServletHolder servlet = handler.addServlet(ServletContainer.class, "/api/*");
servlet.setInitParameter(ServerProperties.WADL_FEATURE_DISABLE, Boolean.TRUE.toString());
servlet.setInitParameter(ServletProperties.JAXRS_APPLICATION_CLASS, RestApplication.class.getName());
servlet.setInitOrder(1); // load-on-startup
}
}
Extend REST Application
The JAX-RS application API (jakarta.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
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 via IServletFilterContributor
and should be configured to be called after the authentication filter (set @Order
appropriately).
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.
@Order(40)
public static class HttpServerRunContextFilterContributor implements IServletFilterContributor {
@Override
public void contribute(ServletContextHandler handler) {
FilterHolder filter = handler.addFilter(HttpServerRunContextFilter.class, "/api/*", null);
filter.setInitParameter("session", "false");
}
}
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.
<!-- 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:
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, StandardCookieSpec.RELAXED);
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:
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):
@Override
protected void configureClientBuilder(ClientBuilder clientBuilder) {
super.configureClientBuilder(clientBuilder);
clientBuilder.property(RestClientProperties.COOKIE_SPEC, StandardCookieSpec.RELAXED);
clientBuilder.property(RestClientProperties.PROXY_URI, "http://my.proxy.com");
}
Some properties (see JavaDoc for details) may also be set on a request level:
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
(andPROXY_USER
/PROXY_PASSWORD
properties) -
Using the dynamic Scout
org.eclipse.scout.rt.shared.http.proxy.ConfigurableProxySelector
, see example configuration:
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:
@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 . |
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:
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.)
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, register servlet filter via IServletFilterContributor
(set @Order
appropriately):
@Order(45)
public static class CancellationFilterContributor implements IServletFilterContributor {
@Override
public void contribute(ServletContextHandler handler) {
handler.addFilter(RestRequestCancellationServletFilter.class, "/api/*", null);
}
}
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.
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();
}
@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();
}