urouting api - Skullabs/kikaha GitHub Wiki
The uRouting (micro routing) API is a simple routing API in a JAXRS fashion optimized at compile time. It aims to provide a friendly and useful routing mechanism to developers, simplifying the way you used to create RESTFul endpoints with Undertow's API.
The router is the component that translates each incoming HTTP request to a method call. In this documentation we conventionally call these methods route
. The main Kikaha's micro Routing mechanism is defined by the kikaha-urouting
module, which you can include on your classpath by adding the following snippet on your pom.xml
file.
<dependency>
<groupId>io.skullabs.kikaha</groupId>
<artifactId>kikaha-urouting</artifactId>
</dependency>
The micro Routing is a compile time generated layer that binds your controller method to the Undertow's low-level API. It means that, under the hood, a class that implements io.undertow.server.HttpHandler
is generated to call your route method. The micro routing was developed to be simple and with low (or almost no) overhead. Its API was strongly inspired by JAX-RS, Play Framework and Scalatra, both awesome and powerful technologies that together cover most part of JVM's web developers community.
While writing a Kikaha route, you will probably notice that it have a basic structure (a convention) we should follow.
@HTTP_METHOD @Path( PATH )
RESPONSE nameOfThisRoute( PARAMETERS ){
...
}
Where the UPPERCASE words above means:
- HTTP_METHOD: an annotation which represents the HTTP Method you expect this route listen to requests.
- PATH: the path your route will listen to requests.
- PARAMETERS: A list of parameters you will be able to receive from a specific request
- RESPONSE: A object that will be serialized and sent as response to the HTTP Client
Every time you write a uRouting Route, Kikaha will:
- Generate a wrapper class which proxies your code with Undertow's API
- The generated code will check if you are running Blocking operations and take care of all tricky stuffs that would be related to this
- It will also check if you are using Non-Blocking codes, and will generate an optimized code for this kind of requests
- It will ensure you are running your code on a thread-pool for long-lived requests, which allows you to run SQL queries (or other blocking operations) without any worries.
When you are coding you should also be aware that:
- It is also possible to define a root path for every route method you create on your class annotating the class with a
@kikaha.urouting.api.Path
. - The
@kikaha.urouting.api.Produces
statically defines which is the Content-Type you intent to return - The
@kikaha.urouting.api.Consumes
statically defines which is the Content-Type you intent to receive an object from a request - You can have as many route methods as you want. There is no limitation out-of-box.
To return a response to the HTTP client you should basically return any object you want on your route method.
The following code will serialize the User
object and send it back to the HTTP Client.
@POST @Path("users")
User sendUser(){
return new User( "Jay", "Milagroso", 1l );
}
But, what if you want to send a more specific response, including Cookies or HTTP Headers?
In this case, you better return any object that implements a specific Response Type Interface kikaha.urouting.api.Response
. The kikaha.urouting.api.DefaultResponse
has a lot of methods which could helps you on the most common cases. Bellow there's an example that will send the same User
object, but including a few headers.
@POST @Path("users")
Response sendUser(){
return DefaultResponse.ok()
.entity( new User( "Jay", "Milagroso", 1l ) )
.header( "Access-Control-Allow-Origin", "*" );
}
- The
DefaultResponse
is the main implementation ofResponse
interface - It has some static constructor methods to make development easier
- It allows you to set the response status code, include headers and define new cookies.
- It also allows you to define the body and the content-type of your response.
Undertow have a very optimized API and one of the reasons it performs so well is because of fact it handles requests in a reactive way, allowing developers to take advantage from modern frameworks which uses Asynchronous APIs. Kikaha have abstracted this behavior on uRouting API through the AsyncResponse object.
Unlike synchronous routes, asynchronous routes should not have return objects. Also, it expects a special object (AsyncResponse) to be "injected" on the parameter list. Let's see an example:
@POST @Path("users")
void sendUser( @Context AsyncResponse asyncResp ){
// myAsyncDB is a hypothetical object that retrieves an user asynchronously.
myAsyncDB.retrieveUser( user -> {
Response resp = DefaultResponse.ok().entity( user ).header( "Access-Control-Allow-Origin", "*" );
asyncResp .write( resp )
});
}
As we can see on the above code, it uses AsyncResponse
object to send the response once it receives the response from our hypothetical myAsyncDB client.
The following HTTP methods Kikaha is able to handle:
- GET -
kikaha.urouting.api.GET
- DELETE -
kikaha.urouting.api.DELETE
- POST -
kikaha.urouting.api.POST
- PUT -
kikaha.urouting.api.PUT
- PATCH -
kikaha.urouting.api.PATCH
The sample bellow shows how is possible to handle a POST/PUT request with micro Routing API.
public class UserResource {
// the stored data
final Set<User> users = new HashSet<>();
// handling both PUT and POST to simplify the example
@POST @PUT
@Path( "users" )
@Consumes("application/json")
// once it does not return a response, the default response is 204
public void addUser( User user ){
users.add( user );
}
}
- The "User" object has no annotation. It will indicates to micro Routing API that it is the representation of the body request.
- You cannot send more than one object to the server as HTTP does not support this behavior.
Yes, you are able to handle requests in a similar as JAXRS does. Bellow is an example showing how you can retrieve a user by its id. In this case, the id is placed as a path parameter inside the URI path you defined at @kikaha.urouting.api.Path
annotation.
public class UserResource {
// the stored data
final Map<Long, User> users = new HasmMap<>();
@GET
@Path( "users/{id}" )
@Produces("application/json")
public User retrieveUserById( @PathParam("id") Long id ){
return users.get( id );
}
}
- The
@kikaha.urouting.api.PathParam
was used to extract the "id" parameter from the path. There is also more information you can retrieve from the request using pre-defined annotation from micro Routing API. Bellow are some examples: -
@kikaha.urouting.api.QueryParam( "id" )
: Retrieves the sent query parameters named "id" -
@kikaha.urouting.api.CookieParam( "SESSION_ID" )
: Retrieves the first sent cookie named "SESSION_ID" -
@kikaha.urouting.api.HeaderParam( "X-Token" )
: Retrieves the first send header named "X-Token". - You can also inject arguments from the Request Context using the
@kikaha.urouting.api.Context
.
All Kikaha's uRouting route is automatically deployed as an injectable service. In practice, it means that, if you are using the CDI module, you could inject any dependency through the @javax.inject.Inject
annotation, even if you does not annotate the class with the @javax.inject.Singleton
annotation.
In every request received Undertow stores the request data (headers, cookies, query parameters, etc) in an instance of HttpServerExchange
object. This object was designed to provide any useful data while the request is happening (request context only). You cannot access this informations outside of a request.
Sometimes you need to extract some meta information from your requests, like Autentication Tokens, the current Logged In user, the current Tenant, etc. It can become a repetitive task if you have many routing endpoints. Kikaha allows developers to inject data retrieved by the request context into your routing methods.
package sample;
import kikaha.urouting.api.*;
import io.undertow.security.idm.Account;
public class UserResource {
@POST
public void persistUser( @Context Account account, User user ){
// do something with the logged in account
}
}
The above example code shows how is possible to inject the current logged in account in the persistUser
method. Note that the @kikaha.urouting.api.Context
annotation was used to inject the io.undertow.security.idm.Account
object extracted from the Undertow's request context object (HttpServerExchange).
Out of box, Kikaha allows developers to inject the following data extracted from Request Context:
-
io.undertow.security.idm.Account
: the Undertow implementation of current logged in user -
io.undertow.server.HttpServerExchange
: the object that contains all data available at request context -
io.undertow.server.handlers.form.FormData
: Undertow implementation used to represent a Form Data received by the HTTP client -
io.undertow.security.api.SecurityContext
: Undertow implementation used to represent the security context from the current request. -
kikaha.core.security.SecurityContext
: A Kikaha specific implementation of Undertow's io.undertow.security.api.SecurityContext. It has an improved API and offers a more convenient approach to deal with the current session. -
kikaha.core.security.Session
: The current logged in session. It allows developers to store data into the current session.
Developers are encouraged to create their own kikaha.urouting.api.ContextProducer
every time is needed to inject meta-data received from current request context.
// TenantContextProducer.java
package sample;
import kikaha.urouting.api.*;
import javax.inject.*;
@Singleton
public class TenantContextProducer implements ContextProducer<Tenant> {
@Override
public Tenant produce( HttpServerExchange exchange ) throws RoutingException {
String hostAndPort = exchange.getHostAndPort()
return new Tenant( hostAndPort );
}
}
// Tenant.java
public class Tenant {
final private String hostAndPort;
public Tenant( String hostAndPort ){
this.hostAndPort = hostAndPort;
}
public String toString(){
return hostAndPort;
}
}
// UserResource.java
@Path("api/users")
public class UserResource {
@GET
@Path("{userId}")
public User retrieveUserById(
@Context Tenant tenant, @PathParam("userId") Long id ){
// do something
}
}
The above sample implementation shows how is possible to inject a Tenant object (representing which is the tenant of current request) in the routing method. At this example, we are assuming that the Tenant Id is the current sub-domain, like mycustomer.mydomain.com
, but it is possible to retrieve this information from dynamic sources like databases or some cache mechanisms like **Hazelcast **.
The sample code bellow shows how to handle an upload request with 'multipart/form-data' content type.
@Path("api/pdf/upload")
public class PDFUploadResource {
@MultiPartFormData
public void handleUpload( File file ){
System.out.println( file.getName() );
}
}
The File file parameter, that represents the uploaded file, can be altogether if other special parameters like @PathParam
or @Context
annotated attributes.
@Singleton
public class UserService {
@Inject
EntityManager em;
@GET @Path("users")
public Iterable<User> retrieveUsers(){
Query query = em.createQuery("SELECT e FROM User e");
return (Iterable<User>) query.getResultList();
}
}
Consider the above sample code. The retrieveUsers
is a very straightforward method to retrieve all persisted User
s found in the database. But, what would happen if the method throws javax.persistence.QueryTimeoutException
. You probably will try to handle this exception with a try/catch block. It wouldn't be a big deal until this behavior starts to repeat in every method that uses Query.getResultList()
method.
To avoid repetitive coding style handling exceptions, you can define a global Exception Handler. It allows you to define custom HTTP responses when specific exceptions are thrown. The following sample codes illustrates how to handle the QueryTimeoutException
.
import kikaha.urouting.api.*;
import javax.inject.*;
@Singleton
public class QueryTimeoutExceptionHandler
implements ExceptionHandler<QueryTimeoutException> {
public Response handle( QueryTimeoutException cause ){
return DefaultResponse.serverError( cause.getMessage() );
}
}
Note that we should make
QueryTimeoutExceptionHandler
injectable, in order be automatically discovered by Kikaha's start up routine. That's the reason we put the@Singleton
annotation into that class.
What if you are designing an application that will receive data in an uncommon content-type like CVS or even XLS? Kikaha allows developers to include new mechanisms to handle and write back data in any formats (Content-Type) he needs. These mechanisms are called Serializers and Unserializers.
Unserializers are mechanisms designed to convert incomming data into another (more useful) format/object. Every time you make a POST request and send an entity as body, Kikaha uses the Content-Type header to detect which Unserializer
will handle this request and convert it into an object that represents your sent entity. The bellow code illustrates how you can handle this POST request.
package sample;
import kikaha.urouting.api.*;
import javax.inject.*;
@Path("api/users")
public class User {
@POST
@Consumes( "text/csv" )
public void persistUser( User user ){
// call user persistence here
}
}
If the HTTP client which is sending the request informs a Content-Type, it will be used to identify which kikaha.urouting.api.Unserializer
implementation will be used to convert the sent data into User
. If the Content-Type header is missing, the fallback Content-Type defined by the @Consumes
annotation will be used.
Out-of-box, Kikaha only supports PLAIN TEXT Content-Types. There is a module called kikaha-urouting-jackson
and kikaha-urouting-jaxb
that add JSON and XML support respectively. Bellow you can see how you can create your own CSV unserializer.
import kikaha.core.modules.http.ContentType;
import kikaha.urouting.api.*;
import javax.inject.*;
@Singleton
@ContentType("text/csv")
public class CSVUnserializer implements Unserializer {
public <T> T unserialize( final HttpServerExchange input, final Class<T> targetClass, String encoding ) throws IOException {
// You can use the 'input' object to unserialize the entity
// Or import the Simplified Undertow API
T instance = // apply your unserialization rule here
return instance;
}
}
Serializers, in other hand, are responsible to convert an Java object into data that will be send back to the HTTP client. Every time you need retrieve a list of users from your database and send it back to the browser, Kikaha will use the Content-Type returned by Response.contentType()
to determine which kikaha.urouting.api.Serializer
implementation will be used to translate the Java list of objects into a data that the browser understand. The method retrieveUsers
bellow illustrates this.
package sample;
import kikaha.urouting.api.*;
import javax.inject.*;
@Path("api/users")
public class UserResource {
// Inject here the services you need to retrieve data...
@GET
public Response retrieveUsers(){
List<User> users = // retrieve the list of users
return DefaultResponses.ok( users )
.contentType( "text/csv" );
}
@GET
@Path("alternative1")
@Produces("text/csv")
public Response retrieveUsersAlternative1(){
List<User> users = // retrieve the list of users
return DefaultResponses.ok( users );
}
@GET
@Path("alternative2")
@Produces("text/csv")
public List<User> retrieveUsersAlternative2(){
List<User> users = // retrieve the list of users
return users;
}
}
The above sample code also illustrate two alternative ways to send back the user list to the HTTP cliente. The retrieveUsersAlternative1
shows that is possible to define the produced response Content-Type through the @Produces
annotation. The retrieveUsersAlternative2
shows that you can just return the user list object, also indicating the produced response Content-Type through the @Produces
annotation.
In both cases (serialization and unserialization) when no Content-Type is defined "text/plain" is assumed. Thus, developers are encouraged always inform the desired Content-Type for each exposed endpoint, or at the class definition