This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.
REST Resource¶
A REST resource represents a thing that can be managed via the REST API. Each module can define its own REST resources.
Hint
This documentation assumes that you know how REST works and how a REST resource is designed properly. This manual only serves as a guide for implementing resources in our application.
Warning
It’s very important that every resource is structured in such a way that it fits well into the API as a whole and that it follows the REST principles strictly.
Make sure that your resource is properly designed before you start implementing it, since it’s very hard to change its design once it’s in use (possibly there are also 3rd party applications using the resource).
- The steps to implement a REST resource in any module:
Add the required gradle dependencies / module-info entries
Implement your rest resource (by extending the class AbstractRestResource) and add JAX-RS annotations as required. Add the RestResource annotation to automatically register the resource.
The following paragraphs explain in detail how this is done.
Hint
Our REST API is based on the Apache Jersey framework which is an implementation of the JAX-RS specification. This documentation only covers the basics of this specification and primarily serves as a guide for implementing resources in our application. There is plenty of information publicly available online about Jersey and JAX-RS.
Add Gradle Dependency¶
Adding REST resources requires the following dependencies in the build.gradle
and module-info.java
of the
module. If another rest rest dependency is required (e.g. core.rest.entity
or core.rest.action
)
core.rest.core
should no longer be explicitly added as it is a transitive dependency of most other core.rest.xx
modules.
dependencies {
implementation project(":core:rest:rest-core")
implementation 'io.swagger.core.v3:swagger-annotations'
}
open module nice.optional.test {
requires nice.core.rest.core;
requires io.swagger.v3.oas.annotations;
}
Create Resource¶
Create the Java class for your resource by extending AbstractRestResource.
The following class defines a REST resource which will be available on ${BASE_PATH}/events/{city}
.
It defines a method called getEvents()
and a second method addEvent()
. The first method is mapped to
GET requests, the second one to POST requests. Both methods return JSON results.
Starting with version 3.0 we no longer support JAX-RS annotations in interfaces. When migrating rest resources
to 3.0, all JAX-RS annotations need to be moved to the “implementation” class and in most cases the interface
can be removed. To “register” a rest resource the @RestResource
must be added to the class.
Hint
${BASE_PATH} is /nice/rest
. So the full path of the following resource is /nice2/rest/events/{city}
.
@RestResource
@Path("/events/{city}")
public class EventsResource extends AbstractRestResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Load events",
description = "Load events which take place in a certain city",
tags = "events"
)
public CollectionBean getEvents(
@PathParam("city") @Parameter(description = "name of the city") String city,
@QueryParam("sort") @Parameter(description = "comma separated string of fields to sort by") String sort
) {
// load events here and return response
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Create event",
description = "Create a new event",
tags = "events"
)
public Response addEvent(EventBean event) {
// create event here and return response
}
}
There is an extensive set of JAX-RS annotations which can be used to define the behavior of a resource:
Annotation |
Description |
---|---|
Path |
Identifies the URI path. Can be specified on a class or a method. |
PathParam |
Represents the parameter of the URI path. |
GET |
Specifies the method that responds to GET requests. |
POST |
Specifies the method that responds to POST requests. |
PUT |
Specifies the method that responds to PUT requests. |
ch.tocco.nice2.rest.core.spi.PATCH |
Specifies the method that responds to PATCH requests (note that this annotation is not part of the
|
HEAD |
Specifies the method that responds to HEAD requests. |
DELETE |
Specifies the method that responds to DELETE requests. |
OPTIONS |
Specifies the method that responds to OPTIONS requests. |
FormParam |
Represents the parameter of the form. |
QueryParam |
Represents the parameter of the query string of an URL. |
HeaderParam |
Represents the parameter of the header. |
CookieParam |
Represents the parameter of the cookie. |
Produces |
Defines the media type for the response such as XML, PLAIN, JSON etc. |
Consumes |
Defines the media type that the method of a resource class can consume. |
Swagger documentation¶
There is a Swagger documentation available on /nice2/swagger
. Use the annotations @Operation
and @Parameter
to describe the resource in this documentation.
See the Swagger API documentation for more information about that.
How to test your resource¶
Test your resource by extending AbstractInjectingJerseyTestCase. Writing tests for your resource by extending this base class allows you to implement end-to-end tests which test the whole process including routing (via JAX-RS annotations on your interface) and error handling (via the exception mappers you contribute in the test).
Hint
Compared to simple unit tests, this is the preferred way to test your resource. However, lower level unit tests are important as well.
Set up your test like any conventional AbstractInjectingTestCase
and additionally implement the abstract method getRestResources():List<?>
and optionally
getExceptionMappers():List<ExceptionMapper>
to test error handling.
First add the required test dependency in your build.gradle
:
testImplementation(testFixtures(project(":core:rest:rest-core")))
Then add your test class(es):
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import import ch.tocco.nice2.rest.testlib.AbstractInjectingJerseyTestCase;
public class AddEventTest extends AbstractInjectingJerseyTestCase {
@Resource
private EventsResourceImpl eventsResource;
@Resource
private List<ExceptionMapper> exceptionMappers;
@Override
protected void setupTestModules() {
install(FixtureModules.embeddedDbModules(false));
install(FixtureModules.createSchema());
install(RestCoreModules.main());
bind(EventsResource.class, EventsResourceImpl.class);
bindDataModel(MyTestDataModel.class);
}
@Override
protected List<?> getRestResources() {
return ImmutableList.of(
eventsResource
);
}
@Override
protected List<ExceptionMapper> getExceptionMappers() {
return exceptionMappers;
}
@Test
public void testAddEvent() throws Exception {
Entity entity = Entity.entity(new EventBean(), MediaType.APPLICATION_JSON_TYPE);
Response response = target("/events/zurich").request().post(entity);
assertEquals(response.getStatus(), 201);
String location = response.getHeaderString("Location");
assertNotNull(location);
assertEventExists(URI.create(location));
}
}
Warning
When targeting an url with query parameters, the query params should not be added to the path but attached with .queryParams or the response will most likely be 404 - Not Found.
NO
Response response = target("/location/suggestions?city=Züri").get();
YES
Response response = target("/location/suggestions").queryParam("city", "Züri").request().get();
Warning
When mocking a service where an entity or primary key is passed as argument, you must use the sameKey
matcher.
If the matcher is not used the test will fail.
NO
expect(businessUnitManager.withBusinessUnitOfEntity(entity)).andReturn(Invoker.EMPTY);
YES
expect(businessUnitManager.withBusinessUnitOfEntity(sameKey(entity))).andReturn(Invoker.EMPTY);
Use it¶
Now start the application and send an HTTP request to ${HOST}/nice2/rest/events/zurich. If you send a GET request
(i.e. by simply entering the URL in your browser), getEvents()
should be called and you should receive a JSON
representation of events which take place in Zürich.
Enable cross-origin access (optional)¶
By default, the REST resources cannot be accessed from another domain outside the domain from which the REST API is served (forbidden by the same-origin security policy).
Follow the steps described in Cross-Origin Resource Sharing (CORS) if access from other domains should be enabled.
Privileged rebinding¶
Special attention to security is required when rebinding beans to entities in privileged mode. Our rebinding logic EntityBeanRebinder handles nested paths and creates missing entities where necessary. This means that even if you only intend to send beans of some specific entity model from the client, ANY entity model that is related through a relation path can be freely created or adjusted by a malicious user.
To limit this EntityBeanRebinder takes an optional RebindLimitConfiguration parameter that can be used to strictly define which entities may be created and which paths may be edited. Use the builder helper to setup this configuration, either as a black- or whitelist.