A JAX-RS like annotation processor for vert.x verticals and more
Branch: master
Clone or download
Latest commit 6d33c7b Nov 12, 2018
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
src #35 additional route binding improvements ... Nov 11, 2018
.gitignore Introducing basic JAX-RS annotation processing for Path, HttpMethod, … Apr 11, 2017
LICENSE Initial commit Apr 7, 2017
README.md Raising version to 0.8.8 Nov 11, 2018
_config.yml Set theme jekyll-theme-minimal Feb 10, 2018
pom.xml Bumping version to 0.8.8 Nov 11, 2018

README.md

Rest.Vertx

Lightweight JAX-RS (RestEasy) like annotation processor for vert.x verticals

Rest.Vertx is still in beta, so please report any issues discovered.
You are highly encouraged to participate and improve upon the existing code.

If this project help you reduce time to develop? Keep it running and donate for cookies and coffee. paypal

Setup

<dependency>      
     <groupId>com.zandero</groupId>      
     <artifactId>rest.vertx</artifactId>      
     <version>0.8.8</version>      
</dependency>

See also: older versions

Acknowledgments

This project uses:

Example

Step 1 - annotate a class with JAX-RS annotations

@Path("/test")
public class TestRest {

	@GET
	@Path("/echo")
	@Produces(MediaType.TEXT_HTML)
	public String echo() {

		return "Hello world!";
	}
}

Step 2 - register annotated class as REST API

TestRest rest = new TestRest();
Router router = RestRouter.register(vertx, rest);

vertx.createHttpServer()
	.requestHandler(router::accept)
	.listen(PORT);

or alternatively

Router router = Router.router(vertx);

TestRest rest = new TestRest();
RestRouter.register(router, rest);

vertx.createHttpServer()
	.requestHandler(router::accept)
	.listen(PORT);

or alternatively use RestBuilder helper to build up endpoints.

Registering by class type

version 0.5 or later

Alternatively RESTs can be registered by class type only.

Router router = RestRouter.register(vertx, TestRest.class);

vertx.createHttpServer()
	.requestHandler(router::accept)
	.listen(PORT);

RestBuilder

version 0.7 or later

Rest endpoints, error handlers, writers and readers can be bound in one go using the RestBuilder.

Router router = new RestBuilder(vertx)
    .register(RestApi.class, OtherRestApi.class)
    .reader(MyClass.class, MyBodyReader.class)
    .writer(MediaType.APPLICATION_JSON, CustomWriter.class)
    .errorHandler(IllegalArgumentExceptionHandler.class)
    .errorHandler(MyExceptionHandler.class)
    .build();

or

router = new RestBuilder(router)
    .register(AdditionalApi.class)		                
    .build();

Paths

Each class can be annotated with a root (or base) path @Path("/rest").
In order to be registered as a REST API endpoint the class public method must have a @Path annotation.

@Path("/api")
public class SomeApi {
   
  @GET
  @Path("/execute")
  public String execute() {
	  return "OK";
  }
}

OR - if class is not annotated the method @Path is taken as the full REST API path.

public class SomeApi {
	
   @GET
   @Path("/api/execute")
   public String execute() {
 	    return "OK";
   }
}
> GET /api/execute/ 

NOTE: multiple identical paths can be registered - if response is not terminated (ended) the next method is executed. However this should be avoided whenever possible.

Path variables

Both class and methods support @Path variables.

// RestEasy path param style
@GET
@Path("/execute/{param}")
public String execute(@PathParam("param") String parameter) {
	return parameter;
}
> GET /execute/that -> that
// vert.x path param style
@GET
@Path("/execute/:param")
public String execute(@PathParam("param") String parameter) {
	return parameter;
}
> GET /execute/this -> this

Path regular expressions

// RestEasy path param style with regular expression {parameter:>regEx<}
@GET
@Path("/{one:\\w+}/{two:\\d+}/{three:\\w+}")
public String oneTwoThree(@PathParam("one") String one, @PathParam("two") int two, @PathParam("three") String three) {
	return one + two + three;
}
> GET /test/4/you -> test4you

since version 0.8.7 or later

Also possible are Vert.x style paths with regular expressions.

// VertX style path :parameter:regEx   
@GET
@Path("/:one:\\d+/minus/:two:\\d+")
public Response test(int one, int two) {
    return Response.ok(one - two).build();
}
> GET /12/minus/3 -> 9

Query variables

Query variables are defined using the @QueryParam annotation.
In case method arguments are not nullable they must be provided or a 400 bad request response follows.

@Path("calculate")
public class CalculateRest {

	@GET
	@Path("add")
	public int add(@QueryParam("one") int one, @QueryParam("two") int two) {

		return one + two;
	}
}
> GET /calculate/add?two=2&one=1 -> 3

In case needed a request reader can be assigned to provide the correct variable:

	@GET
	public int getDummyValue(@QueryParam("dummy") @RequestReader(DummyReader.class) Dummy dummy) {

		return dummy.value;

Decoding of query variables

since version 0.8.7 or later

Query variables are decoded by default
If the original (non decoded) value is desired, we can use the @Raw annotation.

@GET
@Path("/decode")
public String echoGetQuery(@QueryParam("decoded") String decodedQuery,
	                       @QueryParam("raw") @Raw String rawQuery) {
> GET /decode?decoded=hello+world -> decoded = "hello world"
> GET /decode?raw=hello+world     -> raw = "hello+world"

Matrix parameters

Matrix parameters are defined using the @MatrixParam annotation.

@GET
@Path("{operation}")
public int calculate(@PathParam("operation") String operation, @MatrixParam("one") int one, @MatrixParam("two") int two) {
    
  switch (operation) {
    case "add":
      return one + two;
      
	case "multiply" :
	  return one * two;
	
	  default:
	    return 0;
    }
}
> GET /add;one=1;two=2 -> 3

Conversion of path, query, ... variables to Java objects

Rest.Vertx tries to convert path, query, cookie, header and other variables to their corresponding Java types.

Basic (primitive) types are converted from string to given type - if conversion is not possible a 400 bad request response follows.

Complex java objects are converted according to @Consumes annotation or @RequestReader request body reader associated.

Option 1 - The @Consumes annotation mime/type defines the reader to be used when converting request body.
In this case a build in JSON converter is applied.

@Path("consume")
public class ConsumeJSON {

	@POST
	@Path("read")
	@Consumes("application/json")
	public String add(SomeClass item) {

		return "OK";
	}
}

Option 2 - The @RequestReader annotation defines a ValueReader to convert a String to a specific class, converting:

  • request body
  • path
  • query
  • cookie
  • header
@Path("consume")
public class ConsumeJSON {

	@POST
	@Path("read")
	@Consumes("application/json")
	@RequestReader(SomeClassReader.class)
	public String add(SomeClass item) {

		return "OK";
	}
}

Option 3 - An RequestReader is globally assigned to a specific class type.

RestRouter.getReaders().register(SomeClass.class, SomeClassReader.class);
@Path("consume")
public class ConsumeJSON {

	@POST
	@Path("read")
	public String add(SomeClass item) {

		return "OK";
	}
}

Option 4 - An RequestReader is globally assigned to a specific mime type.

RestRouter.getReaders().register("application/json", SomeClassReader.class);
@Path("consume")
public class ConsumeJSON {

	@POST
	@Path("read")
	@Consumes("application/json")
	public String add(SomeClass item) {

		return "OK";
	}
}

First appropriate reader is assigned searching in following order:

  1. use parameter ValueReader
  2. use method ValueReader
  3. use class type specific ValueReader
  4. use mime type assigned ValueReader
  5. use general purpose ValueReader

Missing ValueReader?

If no specific ValueReader is assigned to a given class type, rest.vertx tries to instantiate the class:

  • converting String to primitive type if class is a String or primitive type
  • using a single String constructor
  • using a single primitive type constructor if given String can be converted to the specific type
  • using static methods fromString(String value) or valueOf(String value) (in that order)

@SuppressCheck annotation

Rest.vertx tries to be smart and checks all readers and writers type compatibility.
Meaning if a REST method returns a String then a String compatible writer is expected.

In case the check is to strong (preventing some fancy Java generics or inheritance) the @SuppressCheck annotation can be applied to skip the check.

NOTE: This will not prevent a writer/reader runtime exception in case of type incompatibility!

@SuppressCheck
public class TestSuppressedWriter implements HttpResponseWriter<Dummy> {

	@Override
	public void write(Dummy result, HttpServerRequest request, HttpServerResponse response) {
		response.end(result.name);
	}
}

Cookies, forms and headers ...

Cookies, HTTP form and headers can also be read via @CookieParam, @HeaderParam and @FormParam annotations.

@Path("read")
public class TestRest {

	@GET
	@Path("cookie")
	public String readCookie(@CookieParam("SomeCookie") String cookie) {

		return cookie;
	}
}
@Path("read")
public class TestRest {

	@GET
	@Path("header")
	public String readHeader(@HeaderParam("X-SomeHeader") String header) {

		return header;
	}
}
@Path("read")
public class TestRest {

	@POST
	@Path("form")
	public String readForm(@FormParam("username") String user, @FormParam("password") String password) {

		return "User: " + user + ", is logged in!";
	}
}

@DefaultValue annotation

We can provide default values in case parameter values are not present with @DefaultValue annotation.

@DefaultValue annotation can be used on:

  • @PathParam
  • @QueryParam
  • @FormParam
  • @CookieParam
  • @HeaderParam
  • @Context
public class TestRest {

	@GET
	@Path("user")
	public String read(@QueryParam("username") @DefaultValue("unknown") String user) {

		return "User is: " + user;
	}
}
> GET /user -> "User is: unknown
   
> GET /user?username=Foo -> "User is: Foo

Request context

Additional request bound variables can be provided as method arguments using the @Context annotation.

Following types are by default supported:

  • @Context HttpServerRequest - vert.x current request
  • @Context HttpServerResponse - vert.x response (of current request)
  • @Context Vertx - vert.x instance
  • @Context EventBus - vert.x EventBus instance
  • @Context RoutingContext - vert.x routing context (of current request)
  • @Context User - vert.x user entity (if set)
  • @Context RouteDefinition - vertx.rest route definition (reflection of Rest.Vertx route annotation data)
@GET
@Path("/context")
public String createdResponse(@Context HttpServerResponse response, @Context HttpServerRequest request) {

	response.setStatusCode(201);
	return request.uri();
}

Registering a context provider

If desired a custom context provider can be implemented to extract information from request into a object.
The context provider is only invoked in when the context object type is needed. Use addProvider() method on RestRouter or RestBuilder to register a context provider.

public class TokenProvider  implements ContextProvider<Token> {
                           
    @Override
    public Token provide(HttpServerRequest request) throws Throwable {
        String token = request.getHeader("X-Token");
        if (token != null) {
            return new Token(token);
        }
        
        return null;
    }
}

RestRouter.addProvider(Token.class, TokenProvider.class);

or

RestRouter.addProvider(Token.class, request -> {
		String token = request.getHeader("X-Token");
		if (token != null) {
			return new Token(token);
		}
			
		return null;
	});

or

public class Token {

	public String token; 
	
	public Token (HttpServerRequest request) {
		token = request.getHeader("X-Token");
    }
}

RestRouter.addProvider(Token.class, Token::new)
@GET
@Path("/token")
public String readToken(@Context Token token) {

	return token.getToken();
}

If @Context for given class can not be provided than a 400 @Context can not be provided exception is thrown

Pushing a custom context

While processing a request a custom context can be pushed into the vert.x routing context data storage.
This context data can than be utilized as a method argument. The pushed context is thread safe for the current request.

The main difference between a context push and a context provider is that the context push is executed on every request, while the registered provider is only invoked when needed!

In order to achieve this we need to create a custom handler that pushes the context before the REST endpoint is called:

Router router = Router.router(vertx);
router.route().handler(pushContextHandler());

router = RestRouter.register(router, new CustomContextRest());
vertx.createHttpServer()
		.requestHandler(router::accept)
		.listen(PORT);

private Handler<RoutingContext> pushContextHandler() {

	return context -> {
		RestRouter.pushContext(context, new MyCustomContext("push this into storage"));
		context.next();
	};
}

or

RestRouter.provide(TokenProvider.class); // push of context provider 

A pushed context is handy in case we wan't to make sure some context related object is always present (on every request), ie. session / user ...

Then the context object can than be used as a method argument

@Path("custom")
public class CustomContextRest {
	

    @GET
    @Path("/context")
    public String createdResponse(@Context MyCustomContext context) {
    
    }

Context reader

version 0.8.6 or later

A custom context reader can be applied to a @Context annotated variable to override the global context providers.

    @GET
    @Path("/token")
    @ContextReader(TokenProvider.class) 
    public String createdResponse(@Context Token token) {
        return token.token;
    }
	
    // or
        
    @GET
    @Path("/token")
    public String createdResponse(@ContextReader(TokenProvider.class) @Context Token token) {
        return token.token;
    }

Response building

Response writers

Metod results are converted using response writers.
Response writers take the method result and produce a vert.x response.

Example of a simple response writer:

@Produces("application/xml")        // content-type header
@Header("X-Status: I'm a dummy")    // additional static headers
public class DummyWriter implements HttpResponseWriter<Dummy> {

	@Override
	public void write(Dummy data, HttpServerRequest request, HttpServerResponse response) {

		response.status(200); // for illustration ... needed only when overriding 200
		
		String out = data.name + "=" + data.value;
		response.end("<custom>" + out + "</custom>");
	}
}

Option 1 - The @Produces annotation mime/type defines the writer to be used when converting response.
In this case a build in JSON writer is applied.

@Path("produces")
public class ConsumeJSON {

	@GET
	@Path("write")
	@Produces("application/json")
	public SomeClass write() {

		return new SomeClass();
	}
}

Option 2 - The @ResponseWriter annotation defines a specific writer to be used.

@Path("produces")
public class ConsumeJSON {

	@GET
	@Path("write")
	@Produces("application/json")
	@ResponseWriter(SomeClassWriter.class) // writer will be used for this REST call only
	public SomeClass write() {

		return new SomeClass();
	}
}

Global writers are used in case no other writer is specified for given type or content-type!

Option 3 - An ResponseWriter is globally assigned to a specific class type.

RestRouter.getWriters().register(SomeClass.class, SomeClassWriter.class);
RestRouter.getWriters().register("application/json", SomeClassWriter.class);
RestRouter.getWriters().register(SomeClassWriter.class); // where SomeClassWriter is annotated with @Produces("application/json")

Option 4 - An ResponseWriter is globally assigned to a specific mime type.

RestRouter.getWriters().register(MyClass.class, MyJsonWriter.class);
RestRouter.getWriters().register("application/json", MyJsonWriter.class);
RestRouter.getWriters().register(MyJsonWriter.class); // where MyJsonWriter is annotated with @Produces("application/json") 
@Path("produces")
public class ConsumeJSON {

	@GET
	@Path("write")
	@Produces("application/json") // appropriate content-type writer will be looked up
	public SomeClass write() { 

		return new SomeClass();
	}
}

First appropriate writer is assigned searching in following order:

  1. use assigned method ResponseWriter
  2. use class type specific writer
  3. use mime type assigned writer
  4. use general purpose writer (call to .toString() method of returned object)

vert.x response builder

In order to manipulate returned response, we can utilize the @Context HttpServerResponse.

@GET
@Path("/login")
public HttpServerResponse vertx(@Context HttpServerResponse response) {

    response.setStatusCode(201);
    response.putHeader("X-MySessionHeader", sessionId);
    response.end("Hello world!");
    return reponse;
}

JAX-RS response builder

NOTE in order to utilize the JAX Response.builder() an existing JAX-RS implementation must be provided.
Vertx.rest uses the Glassfish Jersey implementation for testing:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-common</artifactId>
    <version>2.22.2</version>
</dependency>
@GET
@Path("/login")
public Response jax() {

    return Response
        .accepted("Hello world!!")
        .header("X-MySessionHeader", sessionId)
        .build();
}

User roles & authorization

User access is checked in case REST API is annotated with:

  • @RolesAllowed(role), @RolesAllowed(role_1, role_2, ..., role_N) - check if user is in any given role
  • @PermitAll - allow everyone
  • @DenyAll - deny everyone

User access is checked against the vert.x User entity stored in RoutingContext, calling the User.isAuthorised(role, handler) method.

In order to make this work, we need to fill up the RoutingContext with a User entity.

public void init() {
	
    // 1. register handler to initialize User
    Router router = Router.router(vertx);
    router.route().handler(getUserHandler());

    // 2. REST with @RolesAllowed annotations
    TestAuthorizationRest testRest = new TestAuthorizationRest();
    RestRouter.register(router, testRest);

    vertx.createHttpServer()
        .requestHandler(router::accept)
        .listen(PORT);
}

// simple hanler to push a User entity into the vert.x RoutingContext
public Handler<RoutingContext> getUserHandler() {

    return context -> {

        // read header ... if present ... create user with given value
        String token = context.request().getHeader("X-Token");

        // set user ...
        if (token != null) {
            context.setUser(new SimulatedUser(token)); // push User into context
        }

        context.next();
    };
}
@GET
@Path("/info")
@RolesAllowed("User")
public String info(@Context User user) {

    if (user instanceof SimulatedUser) {
    	SimulatedUser theUser = (SimulatedUser)user;
    	return theUser.name;
    }

    return "hello logged in " + user.principal();
}

Example of User implementation:

public class SimulatedUser extends AbstractUser {

  private final String role; // role and role in one
	
  private final String name;

  public SimulatedUser(String name, String role) {
    this.name = name;
    this.role = role;
  }
  
  /**
   * permission has the value of @RolesAllowed annotation
   */
  @Override
  protected void doIsPermitted(String permission, Handler<AsyncResult<Boolean>> resultHandler) {

    resultHandler.handle(Future.succeededFuture(role != null && role.equals(permission)));
  }

  /**
   * serialization of User entity
   */  
  @Override
  public JsonObject principal() {

    JsonObject json = new JsonObject();
    json.put("role", role);
    json.put("name", name);
    return json;  
  }

  @Override
  public void setAuthProvider(AuthProvider authProvider) {
    // not utilized by Rest.vertx  
  }
}

Implementing a custom value reader

In case needed we can implement a custom value reader.
A value reader must:

  • implement ValueReader interface
  • linked to a class type, mime type or @RequestReader

Example of RequestReader:

/**
 * Converts request body to JSON
 */
public class MyCustomReader implements ValueReader<MyNewObject> {

	@Override
	public MyNewObject read(String value, Class<MyNewObject> type) {

		if (value != null && value.length() > 0) {
			
		    return new MyNewObject(value);
		}
		
		return null;
	}
}

Using a value reader is simple:

Register as global reader:

Global readers are used in case no other reader is specified for given type or content-type!

RestRouter.getReaders().register(MyNewObject.class, MyCustomReader.class);
RestRouter.getReaders().register("application/json", MyCustomReader.class);
RestRouter.getReaders().register(MyCustomReader.class); // if reader is annotated with @Consumes("application/json")

// or  
new RestBuilder(vertx).reader(MyNewObject.class, MyCustomReader.class);
new RestBuilder(vertx).reader("appplication/json", MyCustomReader.class);
new RestBuilder(vertx).reader(MyCustomReader.class); // if reader is annotated with @Consumes("application/json")

Use only local on specific REST endpoint:

@Path("read")
public class ReadMyNewObject {

  @POST
  @Path("object")
  @RequestReader(MyCustomReader.class) // MyCustomReader will provide the MyNewObject to REST API
  public String add(MyNewObject item) {
    return "OK";
  }
  
  // or
  
  @PUT
  @Path("object")
  public String add(@RequestReader(MyCustomReader.class) MyNewObject item) {
      return "OK";
  }
}

We can utilize request readers also on queries, headers and cookies:

@Path("read")
public class ReadMyNewObject {
 
   @GET
   @Path("query")
   public String add(@QueryParam("value") @RequestReader(MyCustomReader.class) MyNewObject item) {
     return item.getName();
   }
}

Implementing a custom response writer

In case needed we can implement a custom response writer.
A request writer must:

  • implement HttpResponseWriter interface
  • linked to a class type, mime type or @ResponseWriter

Example of ResponseWriter:

/**
 * Converts request body to JSON
 */
public class MyCustomResponseWriter implements HttpResponseWriter<MyObject> {

  /**
   * result is the output of the corresponding REST API endpoint associated 
   */  
  @Override
  public void write(MyObject data, HttpServerRequest request, HttpServerResponse response) {
    
    response.putHeader("X-ObjectId", data.id);
    response.end(data.value);
  }
}

Using a response writer is simple:
Register as global writer:

RestRouter.getWriters().register(MyObject.class, MyCustomResponseWriter.class);  
// or  
new RestBuilder(vertx).writer(MyObject.class, MyCustomResponseWriter.class);

Use only local on specific REST endpoint:

@Path("write")
public class WriteMyObject {
  
  @GET
  @Path("object")
  @ResponseWriter(MyCustomResponseWriter.class) // MyCustomResponseWriter will take output and fill up response 
  public MyObject output() {
    
  	return new MyObject("test", "me");
  }
}

Consuming / Producing JSONs

By default Rest.Vertx binds application/json mime type to internal JsonValueReader and JsonResponseWriter to read and write JSONs. This reader/writer utilizes Jackson with Vert.x internal io.vertx.core.json.Json.mapper ObjectMapper.
In order to change serialization/deserialization of JSON via Jackson the internal io.vertx.core.json.Json.mapper should be altered.

Ordering routes

By default routes area added to the Router in the order they are listed as methods in the class when registered. One can manually change the route REST order with the @RouteOrder annotation.

By default each route has the order of 0.
If route order is != 0 then vertx.route order is set. The higher the order - the later each route is listed in Router. Order can also be negative, e.g. if you want to ensure a route is evaluated before route number 0.

Example: despite multiple identical paths the route order determines the one being executed.

@RouteOrder(20)
@GET
@Path("/test")
public String third() {
  return "third";
}

@RouteOrder(10)
@GET
@Path("/test")
public String first() {
  return "first";
}

@RouteOrder(15)
@GET
@Path("/test")
public String second() {
  return "second";
}
> GET /test -> "first" 

Rest events

version 0.8.6 or later

Rest events are a useful when some additional work/action must be performed based on the response produced.
For instance we want to send out a registration confirmation e-mail on a 200 response (a successful registration).

Rest events are triggered after the response has been generated, but before the REST has ended.
One or more events are executed synchronously after the REST execution.
The order of events triggered is not defined, nor should one event rely on the execution of another event.

Rest events can be bound to:

  • http response code
  • thrown exception
  • or both

This is the place to trigger some async operation via event bus or some other response based operation.

A RestEvent processor must implement the RestEvent interface (similar to ResponseWriters). The event input is either the produced response entity or the exception thrown.
If the event/entity pair does not match, the event is not triggered.

Example

@GET
@Path("trigger/{status}")
@Events({@Event(SimpleEvent.class), // triggered on OK respons >=200 <300
         @Event(value = FailureEvent.class, exception = IllegalArgumentException.class), // triggered via exception thrown
         @Event(value = SimpleEvent.class, response = 301)}) // triggered on response code 301
public Dummy returnOrFail(@PathParam("status") int status) {

    if (status >= 200 && status < 300) {
        return new Dummy("one", "event");
    }

    if (status >= 300 && status < 400) {
        response.setStatusCode(301);
        return new Dummy("two", "failed");
    }

    throw new IllegalArgumentException("Failed: " + status);
}  
public class SimpleEvent implements RestEvent<Dummy> {

	@Override
	public void execute(Dummy entity, RoutingContext context) throws Throwable {

		System.out.println("Event triggered: " + entity.name + ": " + entity.value);
		context.vertx().eventBus().send("rest.vertx.testing", JsonUtils.toJson(entity)); // send as JSON to event bus ...
	}
}
public class FailureEvent implements RestEvent<Exception> {

	@Override
	public void execute(Exception entity, RoutingContext context) throws Throwable {
		log.error("Error: ", entity);
	}
}

Enabling CORS requests

version 0.7.4 or later

Router router = new RestBuilder(vertx)
    .enableCors("*", true, 1728000, allowedHeaders, HttpMethod.OPTIONS, HttpMethod.GET)
    .register(apiRest) // /api endpoint
    .notFound(RestNotFoundHandler.class) // rest not found (last resort)
    .build();

or

RestRouter.enableCors(router,            // to bind handler to
	                  allowedOriginPattern, // origin pattern
	                  allowCredentials,     // alowed credentials (true/false)
	                  maxAge,               // max age in seconds
	                  allowedHeaders,       // set of allowed headers
	                  methods)              // list of methods or empty for all

Error handling

Unhandled exceptions can be addressed via a designated ExceptionHandler:

  1. for a given method path
  2. for a given root path
  3. globally assigned to the RestRouter

NOTE: An exception handler is a designated response writer bound to a Throwable class

If no designated exception handler is provided, a default exception handler kicks in trying to match the exception type with a build in exception handler.

Bind exception handler to specific exception

Exception handlers are bound to an exception type - first matching exception / handler pair is used.

Example

public class MyExceptionClass extends Throwable {

	private final String error;
	private final int status;

	public MyExceptionClass(String message, int code) {
		error = message;
		status = code;
	}

	public String getError() {
		return error;
	}

	public int getStatus() {
		return status;
	}
}

// bind exception handler to exception type
public class MyExceptionHandler implements ExceptionHandler<MyExceptionClass> {
	@Override
        public void write(MyExceptionClass result, HttpServerRequest request, HttpServerResponse response) {
    
            response.setStatusCode(result.getCode());
            response.end(result.getError());
        }
}

// throw your exception
@GET
@Path("/throw")
@CatchWith(MyExceptionHandler.class)
public String fail() {

  throw new MyExceptionClass("Not implemented.", 404); 
}
> GET /throw -> 404 Not implemented

Path / Method error handler

Both class and methods support @CatchWith annotation.

@CatchWith annotation must provide an ExceptionHandler implementation that handles the thrown exception:

@GET
@Path("/test")
@CatchWith(MyExceptionHandler.class)
public String fail() {

  throw new IllegalArgumentExcetion("Bang!"); 
}
public class MyExceptionHandler implements ExceptionHandler<Throwable> {
    @Override
    public void write(Throwable result, HttpServerRequest request, HttpServerResponse response) {

        response.setStatusCode(406);
        response.end("I got this ... : '" + result.getMessage() + "'");
    }
}

Multiple exception handlers

Alternatively multiple handlers can be bound to a method / class, serving different exceptions.
Handlers are considered in order given, first matching handler is used.

@GET
@Path("/test")
@CatchWith({IllegalArgumentExceptionHandler.class, MyExceptionHandler.class})
public String fail() {

    throw new IllegalArgumentException("Bang!"); 
}
public class IllegalArgumentExceptionHandler implements ExceptionHandler<IllegalArgumentException> {

	@Override
	public void write(IllegalArgumentException result, HttpServerRequest request, HttpServerResponse response) {

		response.setStatusCode(400);
		response.end("Invalid parameters '" + result.getMessage() + "'");
	}
}
public class MyExceptionHandler implements ExceptionHandler<MyExceptionClass> {

	@Override
	public void write(MyExceptionClass result, HttpServerRequest request, HttpServerResponse response) {

		response.setStatusCode(result.getStatus());
		response.end(result.getError());
	}
}

Global error handler(s)

The global error handler is invoked in case no other error handler is provided or no other exception type maches given handlers.
In case no global error handler is associated a default (generic) error handler is invoked.

  Router router = RestRouter.register(vertx, SomeRest.class);
  RestRouter.getExceptionHandlers().register(MyExceptionHandler.class);  
    
  vertx.createHttpServer()
    .requestHandler(router::accept)
    .listen(PORT);

or alternatively we bind multiple exception handlers.
Handlers are considered in order given, first matching handler is used.

  Router router = RestRouter.register(vertx, SomeRest.class);
  RestRouter.getExceptionHandlers().register(MyExceptionHandler.class, GeneralExceptionHandler.class);  

Page not found helper

version 0.7.4 or later

To ease page/resource not found handling a special notFound() handler can be be utilized.

We can

  • handle a subpath (regular expression pattern) where a handler was not found
  • handle all not matching requests
Router router = new RestBuilder(vertx)
    .register(MyRest.class)
    .notFound(".*\\/other/?.*", OtherNotFoundHandler.class) // handle all calls to an /other request
    .notFound("/rest/.*", RestNotFoundHandler.class) // handle all calls to /rest subpath
    .notFound(NotFoundHandler.class) // handle all other not found requests
    .build();

or

RestRouter.notFound(router, "rest", RestNotFoundHandler.class);

The not found handler must extend NotFoundResponseWriter:

public class NotFoundHandler extends NotFoundResponseWriter {
                                  
    @Override
    public void write(HttpServerRequest request, HttpServerResponse response) {
    
        response.end("404 HTTP Resource: '" + request.path() + "' not found!");
    }
}

Serving static/resource files

version 0.8 or later

Rest.vertx simplifies serving of static resource files. All you need to do is to create a REST endpoint that returns the relative path of the desired resource file, bound with FileResponseWriter writer.

For example:

@Path("docs")
public class StaticFileRest {

	@GET
	@Path("/{path:.*}")
	@ResponseWriter(FileResponseWriter.class)
	public String serveDocFile(@PathParam("path") String path) {

		return "html/" + path;
	}
}

will load resource file in html/{path} and return it's content.

> GET docs/page.html -> returns page.html content via FileResponseWriter

Blocking and Async RESTs

version 0.8.1 or later

Default

By default all REST utilize vertx().executeBlocking() call. Therefore the vertx event loop is not blocked. It will utilize the default vertx thread pool:

DeploymentOptions options = new DeploymentOptions();
options.setWorkerPoolSize(poolSize);
options.setMaxWorkerExecuteTime(maxExecuteTime);
options.setWorkerPoolName("rest.vertx.example.worker.pool");

vertx.deployVerticle(new RestVertxVerticle(settings), options);

Responses are always terminated (ended).

Async

If desired a REST endpoint can return io.vertx.core.Future and will be executed asynchronously waiting for the future object to finish. If used with non default (provided) HttpResponseWriter the response must be terminated manually.

This should be used in case we need to use a specific vertx worker pool
... thus we can manually execute the Future<> with that specific worker pool.

The output writer is determined upon the Future type returned. If returned future object is null then due to Java generics limitations, the object type can not be determined. Therefore the response will be produced by the best matching response writer instead.

suggestion: wrap null responses to object instances

Simple async example

WorkerExecutor executor = Vertx.vertx().createSharedWorkerExecutor("SlowServiceExecutor", 20);
@GET
@Path("async")
public Future<Dummy> create(@Context Vertx vertx) throws InterruptedException {

    Future<Dummy> res = Future.future();
    asyncCall(executor, res);
    return res;
}
public void asyncCall(WorkerExecutor executor, Future<Dummy> value) throws InterruptedException {

    executor.executeBlocking(
            fut -> {
                try {
                    Thread.sleep(1000);
                }
                catch (InterruptedException e) {
                    value.fail("Fail");
                }
                value.complete(new Dummy("async", "called"));
                fut.complete();
            },
            false,
            fut -> {}
        );
}

Injection

version 8.0 or later

Allows @Inject (JSR330) injection of RESTs, writers and readers.

To provide injection an InjectionProvider interface needs to be implemented.

Binding injection provider

Router router = new RestBuilder(vertx)
		                .injectWith(GuiceInjectionProvider.class)
		                .register(GuicedRest.class)
		                .build();

or

RestRouter.injectWith(GuiceInjectionProvider.class);

Implement injection provider

Following is a simple implementation of a Guice injection provider.

public class GuiceInjectionProvider implements InjectionProvider {

	private Injector injector;

	public GuiceInjectionProvider(Module[] modules) {
		injector = Guice.createInjector(modules);
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object getInstance(Class clazz) {
		return injector.getInstance(clazz);
	}
}
Router router = new RestBuilder(vertx).injectWith(new GuiceInjectionProvider(getModules())).build();
vertx.createHttpServer()
		     .requestHandler(router::accept)
		     .listen(port);

private Module[] getModules() {
		return new Module[]{
			new ServiceModule(),
			new SecurityModule() ... 
		};
	}

Implement service (use @Inject if needed)

public MyServiceImpl implements MyService {
	
	private final OtherService other;
	
	@Inject
	public MyServiceImpl(OtherService service) {
		other = service;
	}
	
	public String call() {
		return "something";
	}
}

Use @Inject in RESTs

@Path("rest")
public class GuicedRest {

	private final MyService service;

	@Inject
	public GuicedRest(MyService someService) {

		service = someService;
	}

	@GET
	@Path("test")
	public String get() {
		return service.call();
	}
}

Injection can also be used od RequestReader, ResponseWriters or ExceptionHandler if needed.

@Context fields

since version 0.8.1 or later

Rest api classes can not use @Context fields, @Context is provided via method parameters instead.

In case needed a RequestReader, ResponseWriter or ExceptionHandler can use a @Context annotated field, see Request context for details.

Use @Context fields only when really necessary, as the readers, writers and handlers are not cached but initialized on the fly on every request when needed.

This is done in order to ensure thread safety, so one context does not jump into another thread.

public class StringWriter implements HttpResponseWriter<String> {

	@Context
	RoutingContext context;

	@Override
	public void write(String path, HttpServerRequest request, HttpServerResponse response) throws FileNotFoundException {

		if (context.data().get("myData") == null) {
			...
		}
		else { ... }
	}

Internal caching

since version 0.8.1 or later

Caching and singletons

  • All registered REST classes are singletons by default, no need to annotate them with @Singleton annotation.
  • By default all HttpResponseWriter, ValueReader and ExceptionHandler classes are singletons that are cached once initialized.
  • In case HttpResponseWriter, ValueReader or ExceptionHandler are utilizing a @Context field they are initialized on every request for thread safety

Disabling caching

since version 0.8.6 or later

To disabled caching use the @NoCache annotation.

@NoCache
public class NotCachedClass() {
}

Validation

since version 0.8.4 or later

Rest.vertx can utilize any JSR 380 validation implementation, we only need to provide the appropriate validator implementation.
For instance we can use Hibernate implementation:

HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class)
	                                                          .configure();

Validator validator = configuration.buildValidatorFactory()
	                               .getValidator();

Link validator with rest.vertx:

Router router = new RestBuilder(vertx)
		                .validateWith(validator)
		                .register(Rest.class)
		                .build();

or

RestRouter.validateWith(validator);

and annotate REST calls:

@POST("valid")
public int sum(@Max(10) @QueryParam("one") int one,
               @Min(1) @QueryParam("two") int two,
               @Valid Dummy body) {
    return one + two + body.value;
}

In case of a violation a 400 Bad request response will be generated using ConstraintExceptionHandler.

Static data annotations

@Produces on response writers

Additional to REST endpoints @Produces can also be applied to response writers.
This will add the appropriate content-type header to the output, plus will register writer to the given content-type if no other association is given.

Example:

@Produces("application/json")
public class JsonExceptionHandler implements ExceptionHandler<String> {

	@Override
	public void write(String result, HttpServerRequest request, HttpServerResponse response) {
		...
	}
}

@Header annotation

since version 0.8.4 or later

The @Header annotation adds one or multiple static header to the response. It can be applied either to REST endpoints or to response writers.

Example:

@Header("X-Status-Reason: Validation failed")
public class ConstraintExceptionHandler implements ExceptionHandler<ConstraintException> {

	@Override
	public void write(ConstraintException result, HttpServerRequest request, HttpServerResponse response) {
        ...
	}
}

Logging

Rest.vertx uses Slf4j logging API. In order to see all messages produced by Rest.vertx use a Slf4j compatible logging implementation.

Logback logging settings example

<logger name="com.zandero.rest" level="DEBUG" />

Experimental features

NOTE: not necessary to stay in the final release

Shorthands for method, path, consumes and produces

Instead of the following:

@GET
@Path("/test")
@Consumes("application/json")
@Produces("application/json")
public String method() { ... }

a shorthand form can be used combining all into one

@Get("/test")
@Consumes("application/json")
@Produces("application/json")
public String method() { ... }

or even:

@Get(value = "/test", consumes = "application/json", produces = "application/json")
public String method() { ... }