Lightweight JAX-RS implementation.
The Java API for RESTful web services (JAX-RS) makes developing RESTful web services in Java simple and intuitive. It's also nice that the API defines a powerful, flexible and easy-to-use routing mechanism (mapping HTTP requests to Java object methods), that more than suitable for any web-application (not only for RESTful web services).
However, the specification contains requirements (such as e.g. XML support) that inevitably turn any full implementation into a multi-megabyte-sized framework with dozen(s) of dependencies on other libraries. And often, most of these features are never used in a particular application. (For example, think of a very typical use case: a microservice that should only handle a few JSON requests / responses).
The idea behind this project is to get a slightly limited, but lightweight JAX-RS implementation without big, not always necessary features, but with all the core features as per the spec.
- Intended to be used in conjunction with the embedded java servlet container (e.g. Jetty, Tomcat, Undertow)
- Don't use any dependencies other than rs-api & annotation-api
- Implement JAX-RS specification as much as possible
Be a complete implementation of the JAX-RS.
The library do not provide "complex" MessageBodyReaders/Writers out of the box. So, to get support for XML or JSON it need to provide an implementation of MessageBodyReaders/Writers.
However, such implementations are trivial issue:
- json-gson module as example of application/json MessageBodyReaders & MessageBodyWriter using GSON
- thymeleaf module as example of text/html MessageBodyWriter using Thymeleaf
The library requires an implementation of the ComponentProvider interface, which exposes JAX-RS resource instances. It seems that such an implementation is a tricky problem because you need "scopes" (Request, Session, Singletons etc.). But it's not. All of that is natural features of any good dependency injection framework (e.g. Dagger 2, Guice, HK2 ). It's anyway good idea to use dependency injection in the application, so all what is need for ComponentProvider: link it with dependency injection framework which you are using.
Example:
- demo-jetty module is Demo Application: it uses Dagger 2 for dependency injection and as the basis for the ComponentProvider implementation.
Usual JAX-RS resources, e.g:
@Path("/my")
public class MyEntryPoint {
@GET
@Path("something/{ id : \\d{6} }/{var1}-{var2}")
@Produces(MediaType.APPLICATION_JSON)
public MySomething getIt(
@PathParam("id") int id,
@PathParam("var1") String var1,
@PathParam("var2") String var2,
@QueryParam("qval") List<Integer> qval) {
return new MySomething(...);
}
}
Provide list of the annotated classes to the servlet and plug the servlet in the servlet-container. In combination with embedded Jetty it's looks like that:
import net.cactusthorn.routing.*;
import net.cactusthorn.routing.gson.*;
import net.cactusthorn.routing.thymeleaf.*;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.servlet.*;
import javax.servlet.MultipartConfigElement;
public class Application {
public static void main(String... args) {
ComponentProvider myComponentProvider = new MyComponentProvider(...);
Collection<Class<?>> resources = ...
RoutingConfig config =
RoutingConfig.builder(myComponentProvider)
.addResource(MyResource.class)
.addResource(resources)
.addMessageBodyWriter(SimpleGsonBodyWriter.class)
.addMessageBodyReader(SimpleGsonBodyReader.class)
.addMessageBodyWriter(new SimpleThymeleafBodyWriter("/thymeleaf/"))
.addParamConverterProvider(LocalDateParamConverterProvider.class)
.addExceptionMapper(UnsupportedOperationExceptionMapper.class)
.setParametersValidator(SimpleParametersValidator.class)
.build();
MultipartConfigElement mpConfig = new MultipartConfigElement("/tmp", 1024 * 1024, 1024 * 1024 * 5, 1024 * 1024 * 5 * 5);
ServletHolder servletHolder = new ServletHolder(new RoutingServlet(config));
servletHolder.setInitOrder(0);
servletHolder.getRegistration().setMultipartConfig(mpConfig);
ServletContextHandler servletContext = new ServletContextHandler(ServletContextHandler.SESSIONS);
servletContext.setContextPath("/");
servletContext.addServlet(servletHolder, "/*");
Server jetty = new Server(8080);
jetty.setHandler(servletContext);
jetty.start();
jetty.join();
}
}
- URI template matched to request URI according JAX-RS rules
- If no one resource found then HTTP response with status 404 send back to client.
- If some resources was found, but
@Consumes
-annotation of no one of them fit Content-Type request-header: HTTP response with status 415 send back to client. - If some resources was found, but
@Produces
-annotation of no one of them fit Accept request-header: HTTP response with status 406 send back to client.
The JAX-RS specification has defined strict sorting and precedence rules for matching URI expressions and is based on a most specific match wins algorithm. The JAX-RS provider gathers up the set of deployed URI expressions and sorts them based on the following logic:
- The primary key of the sort is the number of literal characters in the full URI matching pattern. The sort is in descending order.
- The secondary key of the sort is the number of template expressions embedded within the pattern (e.g. {id} or {id : .+}). This sort is in descending order.
- The tertiary key of the sort is the number of nondefault template expressions. A default template expression is one that does not define a regular expression (e.g. {id}).
The type of the annotated parameter must either:
- Be a primitive type
- Have a constructor that accepts a single String argument
- Have a static method named
valueOf
orfromString
that accepts a single String argument (see, for example,Integer.valueOf(String)
)- If both methods are present then
valueOf
used unless the type is an enum in which casefromString
used.
- If both methods are present then
- Have a registered implementation of
javax.ws.rs.ext.ParamConverterProvider
JAX-RS extension SPI that returns aParamConverter
instance capable of a "from string" conversion for the type. - Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.
@Path
for class and/or method- path-parameters (with regular expressions support)
@GET
@POST
@DELETE
@HEAD
@OPTIONS
@PATCH
@PUT
for method@PathParam
,@QueryParam
,@FormParam
,@CookieParam
,@HeaderParam
,@FormPart
for method parameters.@DefaultValue
@Consumes
for class and/or method- default(if not present) is "*/*"
@Produces
for class and/or method- default(if not present) is "text/plain"
@Context
for method parameters. The following types are currently supported:- javax.servlet.http.HttpServletRequest
- javax.servlet.http.HttpServletResponse
- javax.servlet.ServletContext
- javax.ws.rs.core.SecurityContext
- javax.ws.rs.core.HttpHeaders
- javax.ws.rs.core.UriInfo
- javax.ws.rs.ext.Providers
- javax.ws.rs.core.Application
javax.ws.rs.core.Response
javax.ws.rs.ext.ExceptionMapper
javax.ws.rs.ext.ParamConverterProvider
javax.ws.rs.core.UriBuilder
javax.ws.rs.core.Link.Builder
javax.ws.rs.core.Variant.VariantListBuilder
javax.ws.rs.core.Form
- ParametersValidator interface to integrate additional validations e.g. javax.validation
- Implemetation example is validation-javax module
javax.annotation.security.RolesAllowed
method annotation- to check entry point against request.isUserInRole(...)
- Implemetation example exists in demo-jetty module
@Template
annotation,Templated
-class andTemplatedMessageBodyWriter
to implement message body writers for html-template-engines (e.g. FreeMarker, Thymeleaf )- Implemetation example is thymeleaf module
- Default parameter name
- Only when project compiled with
javac -parameters
- Parameters annotation can be used with empty-string as name, e.g:
@QueryParam("") List<Integer> qval
. In this case paramener name will be used to match parameter in request.
- Only when project compiled with
Default(if the annotation is not present) priority is javax.ws.rs.Priorities.USER
Example:
@javax.annotation.Priority(3000)
public class LocalDateParamConverterProvider implements javax.ws.rs.ext.ParamConverterProvider {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("ddMMyyyy");
private static final javax.ws.rs.ext.ParamConverter<LocalDate> CONVERTER = new javax.ws.rs.ext.ParamConverter<LocalDate>() {
@Override
public LocalDate fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
return LocalDate.parse(value, FORMATTER);
}
@Override
public String toString(LocalDate value) {
if (value == null) {
return null;
}
return value.format(FORMATTER);
}
};
@Override @SuppressWarnings("unchecked")
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if (rawType == LocalDate.class) {
return (ParamConverter<T>) CONVERTER;
}
return null;
}
}
Default(if the annotation is not present) priority is javax.ws.rs.Priorities.USER
Default(if the annotation is not present) consumes is "*/*"
For the next types MessageBodyReaders are provided:
Type | Priority |
---|---|
java.io.InputStream | 50 |
java.lang.String | 50 |
Any convertable from String | 9999 |
@javax.annotation.Priority(3000)
@javax.ws.rs.Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_HTML})
public class MyClassMessageBodyReader implements javax.ws.rs.ext.MessageBodyReader<MyClass> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
...
}
@Override
public MyClass readFrom(Class<InputStream> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) {
...
}
}
@javax.annotation.Priority(3000)
@javax.ws.rs.Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_HTML})
public class MyClassMessageBodyReader implements net.cactusthorn.routing.body.reader.InitializableMessageBodyReader<MyClass> {
@Override
public void init(ServletContext servletContext, RoutingConfig routingConfig) {
...
}
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
...
}
@Override
public MyClass readFrom(Class<InputStream> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream) {
...
}
}
Default(if the annotation is not present) priority is javax.ws.rs.Priorities.USER
Default(if the annotation is not present) produces is "*/*"
For the next types MessageBodyReaders are provided:
Type | Priority |
---|---|
java.lang.String | 50 |
java.lang.Object | 9999 |
@javax.annotation.Priority(3000)
@javax.ws.rs.Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_HTML})
public class MyClassMessageBodyWriter implements javax.ws.rs.ext.MessageBodyWriter<MyClass> {
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
...
}
@Override
public void writeTo(MyClass entity, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
...
}
}
@javax.annotation.Priority(3000)
@javax.ws.rs.Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_HTML})
public class MyClassMessageBodyWriter implements net.cactusthorn.routing.body.writer.InitializableMessageBodyWriter<MyClass> {
@Override
public void init(ServletContext servletContext, RoutingConfig routingConfig) {
...
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
...
}
@Override
public void writeTo(MyClass entity, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
...
}
}
@Produces({MediaType.TEXT_HTML})
public class SimpleThymeleafBodyWriter implements net.cactusthorn.routing.body.writer.TemplatedMessageBodyWriter {
@Override
public void init(ServletContext servletContext, RoutingConfig routingConfig) {
...
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
...
}
@Override
public void writeTo(Templated templated, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
...
}
}
Default(if the annotation is not present) priority is javax.ws.rs.Priorities.USER
@javax.annotation.Priority(3000)
public static class UnsupportedOperationExceptionMapper implements ExceptionMapper<UnsupportedOperationException> {
@Override
public Response toResponse(UnsupportedOperationException exception) {
return Response.status(Response.Status.CONFLICT).build();
}
}
<dependency>
<groupId>net.cactusthorn.routing</groupId>
<artifactId>core</artifactId>
<version>0.30</version>
</dependency>
Public Releases can be also downloaded from GitHub Releases or GitHub Packages
net.cactusthorn.routing is released under the BSD 3-Clause license. See LICENSE file included for the details.