Git Product home page Git Product 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.8</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.

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

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" 

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.

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

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 / 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 a /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!");
    }
}

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 extends AbstractModule implements InjectionProvider  {

	private Injector injector;

	public GuiceInjectionProvider() {
		injector = Guice.createInjector(this);
	}

	@Override
	protected void configure() {
		bind(MyService.class).to(MyServiceImpl.class);
		bind(OtherService.class).to(MyOtherServiceImpl.class);
	}

	@Override
	public Object getInstance(Class clazz) {
		return injector.getInstance(clazz);
	}
}

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 8.1 or later

In case needed a RequestReader, ResponseWriter or ExceptionHandler and utilize 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.

Internal caching

since version 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

rest.vertx's People

Contributors

drejc avatar

Watchers

 avatar  avatar

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.