Code Monkey home page Code Monkey logo

rest.vertx's Introduction

rest.vertx

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

Setup

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

See also: older versions

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

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 (in case they have an empty constructor).

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

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

RestBuilder

since version 0.7

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

Not recommended but possible are vert.x style paths with regular expressions.
In this case method parameters correspond to path expressions by index.

@GET
@Path("/\\d+/minus/\\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

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 method fromString(String value) or valueOf(String value)

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 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.

RestRouter.addContextProvider(Token.class, request -> {
		String token = request.getHeader("X-Token");
		if (token != null) {
			return new Token(token);
		}
			
		return null;
	});
@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();
	};
}

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) {
    
    }

Response building

Response writers

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

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)
	public SomeClass write() {

		return new SomeClass();
	}
}

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

RestRouter.getWriters().register(SomeClass.class, SomeClassWriter.class);

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

RestRouter.getWriters().register("application/json", MyJsonWriter.class);
@Path("produces")
public class ConsumeJSON {

	@GET
	@Path("write")
	@Produces("application/json")
	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:

@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:

@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");
  }
}

Blocking handler

In case the request handler should be a blocking handler the @Blocking annotation has to be used.

@GET
@Path("/blocking")
@Blocking
public String waitForMe() {
  
  return "done";
}

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" 

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.

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 ... : '" + cause.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({HandleRestException.class, WebApplicationExceptionHandler.class})
public String fail() {

    throw new IllegalArgumentExcetion("Bang!"); 
}

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);  

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.