Logo

Redpipe is a Web Framework that unites the power and versatility of Eclipse Vert.x, the conciseness of JAX-RS (with Resteasy), and the non-blocking reactive composition of RxJava.

The main idea is that with Redpipe you write your endpoints in JAX-RS, using RxJava composition if you want, and underneath it all, Vert.x is running the show and you can always access it if you need it.

Redpipe is opinionated, favors convention over configuration, and lets you combine the following technologies easily to write reactive web applications:

Source code / license / issue tracker

Redpipe is open-source software licensed under the Apache License 2.0.

You can file issues at our GitHub issues tracker, or better yet: send us pull requests.

Set-up

Include the following dependency in your pom.xml:

<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-engine</artifactId>
  <version></version>
</dependency>

Quick Examples

Create a Main class to start your server:

import net.redpipe.engine.core.Server;

import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

public class Main {
  public static void main( String[] args ){
    new Server()
     .start(HelloResource.class)
     .subscribe(
        v -> System.err.println("Server started"),
        x -> x.printStackTrace());
  }
}

Creating a resource

Create a resource:

import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/")
public class HelloResource {
  @GET
  public String hello() {
    return "Hello World";
  }
}

Start your Main program and if you head over to http://localhost:9000 you should see Hello World.

Creating a reactive resource

If you want to return a reactive resource, you can simply return a reactive type from your resource, such as those provided by RxJava.

For a reactive Hello World, simply add this method to your resource:

@Path("reactive")
@GET
public Single<String> helloReactive() {
  return Single.just("Hello Reactive World");
}

Restart your Main program and if you head over to http://localhost:9000/reactive you should see Hello Reactive World.

Streams

If you want to return a stream, you have three options for how to serialise them (see below).

To send Hello World as a stream, simply add this method to your resource:

@Stream
@Path("stream")
@GET
public Observable<String> helloStream() {
  return Observable.from(new String[] {"Hello", "World"});
}

Restart your Main program and if you head over to http://localhost:9000/stream you should see HelloWorld.

RxJava support

Out of the box, we support RxJava’s Single and Observable types.

If your resource returns a Single<T>, the T will be send to your client asynchronously as if you returned it directly. In particular, standard and custom MessageBodyWriter<T> apply as soon as the Single is resolved, as do interceptors.

If your resource returns an Observable<T>:

  • By default, every item will be collected, and once complete, assembled in a List<T> and treated as if your method had returned it directly (standard and custom body writers and interceptors apply to it).
  • If you annotate your method with @Stream (from org.jboss.resteasy.annotations), then every item will be sent to the client as soon as it is produced (again, via standard and custom body writers). This is mostly useful for streaming bytes, buffers, or strings, which can be split up and buffered.
  • If you annotate your method with @Produces(MediaType.SERVER_SENT_EVENTS), then every item will be sent to the client over Server-Sent Events (SSE). As always, standard and custom body writers are called to serialise your entities to events.

Fibers

The optional module redpipe-fibers allows you to write your reactive code in fibers, which are light-weight threads, also known as coroutines, while interacting seamlessly with RxJava.

Consider the following traditional RxJava code to forward the result of two web service invocations:

@Path("composed")
@GET
public Single<String> helloComposed(@Context Vertx vertx) {
  Single<String> request1 = get(vertx, getURI(HelloResource::hello));
  Single<String> request2 = get(vertx, getURI(HelloResource::helloReactive));
      
  return request1.zipWith(request2, (hello1, hello2) -> hello1 + "\n" + hello2);
}

private Single<String> get(Vertx vertx, URI uri){
  WebClient client = WebClient.create(vertx);
  Single<HttpResponse<Buffer>> responseHandler = 
    client.get(uri.getPort(), uri.getHost(), 
               uri.getPath()).rxSend();

  return responseHandler.map(response -> response.body().toString());
}

Rather than composing sequential Single values, you can write a fiber that can await those values in what now looks like sequential code:

@Path("fiber")
@GET
public Single<String> helloFiber(@Context Vertx vertx) {
  return Fibers.fiber(() -> {
    String hello1 = Fibers.await(get(vertx, getURI(HelloResource::hello)));
    String hello2 = Fibers.await(get(vertx, getURI(HelloResource::helloReactive)));
    
    return hello1 + "\n" + hello2;
  });
}

A fiber, here, is also a Single which can be integrated with the rest of your RxJava operations, and it can await RxJava Single values to obtain their resolved value.

You can call await from any fiber body, or from any method annotated with @Suspendable.

You need to import the following module in order to use fibers:

<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-fibers</artifactId>
  <version></version>
</dependency>

And also the following Maven build plug-in to set-up your fibers at compile-time:

<build>
  <plugins>
    <plugin>
      <groupId>com.vlkan</groupId>
      <artifactId>quasar-maven-plugin</artifactId>
      <version>0.7.9</version>
      <configuration>
        <check>true</check>
        <debug>true</debug>
        <verbose>true</verbose>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>instrument</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

See Quasar’s documentation for more information on fibers.

Reverse-routing

In order to get URIs that map to resource methods, you can use the redpipe routing module:

<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-router</artifactId>
  <version></version>
</dependency>

With this module, you can simply get a URI for a resource method HelloResource.hello by calling the Router.getURI() method and passing it a reference to your resource method and any required parameters:

URI uri1 = Router.getURI(HelloResource::hello);
URI uri2 = Router.getURI(HelloResource::helloWithParameters, "param1", 42);

Within templates you can use the context.route method which takes a string literal pointing to your resource method, and any required parameters:

<a href="${context.route('WikiResource.renderPage', page)}">${page}</a>

Resource scanning

You can either declare your JAX-RS resources and providers when instantiating the Server (as shown above), or you can use two options to scan your packages to discover them.

Fast-classpath-scanner

If you include this dependency, fast-classpath-scanner will be used to scan your classpath for resources and providers:

<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-fast-classpath-scanner</artifactId>
  <version></version>
</dependency>

You just need to set the scan configuration to an array of package names to scan.

CDI

Alternately, you can delegate scanning of resources and providers to CDI:

<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-cdi</artifactId>
  <version></version>
</dependency>

All your jars containing a META-INF/beans.xml file will be scanned by CDI and your resources and providers will become CDI beans, with CDI injection supported.

Note that this uses Weld and the weld-vertx extension which brings a lot of goodies for CDI and Vert.x.

Configuration

Unless you call Server.start() with a JsonObject configuration to override it, the conf/config.json file will be loaded and used for configuration. Here are the configuration options available:

General options
Name Type Description Default
db_url String Jdbc Url to use for connections to the database. jdbc:hsqldb:file:db/wiki
db_driver String Jdbc driver class. org.hsqldb.jdbcDriver
db_max_pool_size Integer Maximum pool size for database connections. 30
http_port Integer Http port to bind to. 9000
For the `redpipe-fast-classpath-scanner` module
Name Type Description Default
scan String[] List of packages to scan for JAX-RS resources and providers.

You can access the current configuration in your application via the AppGlobals.getConfig() method.

Injection

On top of optional CDI support, JAX-RS supports injection of certain resources via the @Context annotation on method parameters and members. Besides the regular JAX-RS resources, the following resources can be injected:

Injectable global resources
Type Description
io.vertx.rxjava.core.Vertx The Vert.x instance.
net.redpipe.engine.core.AppGlobals A global context object.
Injectable per-request resources
Type Description
io.vertx.rxjava.ext.web.RoutingContext The Vert.x Web RoutingContext.
io.vertx.rxjava.core.http.HttpServerRequest The Vert.x request.
io.vertx.rxjava.core.http.HttpServerResponse The Vert.x response.
io.vertx.rxjava.ext.auth.AuthProvider The Vert.x AuthProvider instance, if any (defaults to null).
io.vertx.rxjava.ext.auth.User The Vert.x User, if any (defaults to null).
io.vertx.rxjava.ext.web.Session The Vert.x Web Session instance, if any (defaults to null).

Templating

We support the following plugable template engines, which you just have to add a dependency on:

Plugable template engine modules
Name Dependency
FreeMarker
<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-templating-freemarker</artifactId>
  <version></version>
</dependency>

In order to declare templates, simply place them in the src/main/resources/templates folder. For example, here’s our src/main/resources/templates/index.ftl template:

<html>
 <head>
  <title>${context.title}</title>
 </head>
 <body>${context.message}</body>
</html>

In order to return a rendered template, just return them from your resource, directly or as a Single:

@GET
@Path("template")
public Template template(){
  return new Template("templates/index.ftl")
          .set("title", "My page")
          .set("message", "Hello");
}

Writing your own template renderer

You can write your own template renderer by declaring a META-INF/services/net.redpipe.engine.template.TemplateRenderer file in your src/main/resources folder, containing the name of the class that extends the net.redpipe.engine.template.TemplateRenderer interface.

For example, this is the FreeMarker renderer, which simply wraps the Vert.x Web support for freemarker:

public class FreeMarkerTemplateRenderer implements TemplateRenderer {

  private final FreeMarkerTemplateEngine templateEngine = 
    FreeMarkerTemplateEngine.create();

  @Override
  public boolean supportsTemplate(String name) {
    return name.toLowerCase().endsWith(".ftl");
  }

  @Override
  public Single<Response> render(String name, 
      Map<String, Object> variables) {
    RoutingContext context = ResteasyProviderFactory.getContextData(RoutingContext.class);
    for (Entry<String, Object> entry : variables.entrySet()) {
      context.put(entry.getKey(), entry.getValue());
    }
    return templateEngine.rxRender(context, name)
            .map(buffer -> Response.ok(buffer, MediaType.TEXT_HTML).build());
  }
}

Serving static files

If you want to serve static files you can place them in src/main/resources/webroot and declare the following resource:

@Path("")
public class AppResource extends FileResource {

  @Path("webroot{path:(/.*)?}")
  @GET
  public Response get(@PathParam("path") String path) throws IOException {
    return super.getFile(path);
  }
}

Your files will then be accessible from the /webroot/ path prefix.

Plugins

You can write plugins by declaring a META-INF/services/net.redpipe.engine.spi.Plugin file in your src/main/resources folder, containing the name of the class that extends the net.redpipe.engine.spi.Plugin class.

From there you can hook into various parts of the application (start-up, requests…).

Swagger

By default, your application includes Swagger OpenAPI Specification (OAS) generation, located at /swagger.json and /swagger.yaml.

Examples

Check out the following examples for best-practices and real-world usage:

How-to

DB

You can get a Single<SQLConnection> with SQLUtil.getConnection(). Alternately, you can call SQLUtil.doInConnection as such:

@POST
@Path("pages")
public Single<Response> apiCreatePage(JsonObject page, 
                                      @Context HttpServerRequest req){
  JsonArray params = new JsonArray();
  params.add(page.getString("name"))
        .add(page.getString("markdown"));

  return SQLUtil.doInConnection(connection -> connection.rxUpdateWithParams(SQL.SQL_CREATE_PAGE, params))
          .map(res -> Response.status(Status.CREATED).build());
}

Custom data-base set-up

You can either set-up your database using the config options db_url, db_driver, and db_max_pool_size, or you can override the createDb method in Server, as in the Jooq example:

@Override
protected SQLClient createDbClient(JsonObject config) {
  JsonObject myConfig = new JsonObject();
  if(config.containsKey("db_host"))
      myConfig.put("host", config.getString("db_host"));
  if(config.containsKey("db_port"))
      myConfig.put("port", config.getInteger("db_port"));
  if(config.containsKey("db_user"))
      myConfig.put("username", config.getString("db_user"));
  if(config.containsKey("db_pass"))
      myConfig.put("password", config.getString("db_pass"));
  if(config.containsKey("db_name"))
      myConfig.put("database", config.getString("db_name"));
  myConfig.put("max_pool_size", config.getInteger("db_max_pool_size", 30));
  
  Vertx vertx = AppGlobals.get().getVertx();
  AsyncSQLClient dbClient =  PostgreSQLClient.createNonShared(vertx, myConfig);
  AsyncJooqSQLClient client = AsyncJooqSQLClient.create(vertx, dbClient);

  Configuration configuration = new DefaultConfiguration();
  configuration.set(SQLDialect.POSTGRES);

  PagesDao dao = new PagesDao(configuration);
  dao.setClient(client);
  
  AppGlobals.get().setGlobal("dao", dao);
  
  return dbClient;
}

Authentication

By default, no authentication is set up, so if you want to set up authentication you can override the Server.setupAuthenticationRoutes method, as in the following two examples.

If you do set up authentication, your AuthProvider will be injectable in your resources, as will the User and Session.

Authorization

You can use the following annotations on your resource methods/classes to enable resource-level authorization checks:

  • @RequiresPermissions({ "perm1", "perm2"}): requires that the current user exists and has both permissions,
  • @RequiresUser: requires that the current user exists,
  • @NoAuthFilter: disables authorization checks,
  • @NoAuthRedirect: return an HTTP FORBIDDEN (403) instead of a redirect to the login page, if authorization fails.

Keycloak

Install Keycloak, start and configure it.

Add the following dependency:

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-auth-oauth2</artifactId>
</dependency>

For the Keycloak example, we need this set-up:

@Override
protected AuthProvider setupAuthenticationRoutes() {
  JsonObject keycloackConfig = AppGlobals.get().getConfig().getJsonObject("keycloack");
  OAuth2Auth authWeb = KeycloakAuth.create(AppGlobals.get().getVertx(), keycloackConfig);
  OAuth2Auth authApi = KeycloakAuth.create(AppGlobals.get().getVertx(), 
                                           OAuth2FlowType.PASSWORD, 
                                           keycloackConfig);
  
  OAuth2AuthHandler authHandler = 
    OAuth2AuthHandler.create((OAuth2Auth) authWeb, 
                             "http://localhost:9000/callback");
  Router router = AppGlobals.get().getRouter();
  AuthProvider authProvider = AuthProvider.newInstance(authWeb.getDelegate());
  router.route().handler(UserSessionHandler.create(authProvider));

  authHandler.setupCallback(router.get("/callback"));
  
  router.route().handler(authHandler);
  
  return AuthProvider.newInstance(authApi.getDelegate());
}

Apache Shiro

Add the following dependency:

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-auth-shiro</artifactId>
</dependency>

For the Apache Shiro example, we need this set-up:

@Override
protected AuthProvider setupAuthenticationRoutes() {
  AppGlobals globals = AppGlobals.get();
  AuthProvider auth = ShiroAuth.create(globals.getVertx(), 
                                       new ShiroAuthOptions()
                                         .setType(ShiroAuthRealmType.PROPERTIES)
                                         .setConfig(new JsonObject()
                                           .put("properties_path", 
                                                globals.getConfig().getString("security_definitions"))));
  
  globals.getRouter().route().handler(UserSessionHandler.create(auth));
  
  return null;
}

Now you can write your handler for the log-in form:

@Path("/")
public class SecurityResource extends BaseSecurityResource {

  @Override
  public Template login(@Context UriInfo uriInfo){
    return new Template("templates/login.ftl")
            .set("title", "Login")
            .set("uriInfo", uriInfo)
            .set("SecurityResource", 
                 BaseSecurityResource.class);
  }
}

Note that the log-in and log-out handlers are set-up in the BaseSecurityResource class, but you can override them if you need to.

JWT

Add the following dependency:

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-auth-shiro</artifactId>
</dependency>

For the JWT example, we need this set-up:

@Override
protected AuthProvider setupAuthenticationRoutes() {
  AppGlobals globals = AppGlobals.get();
  
  // Your regular authentication
  AuthProvider authProvider = ...;

  // attempt to load a Key file
  JWTAuth jwtAuth = JWTAuth.create(globals.getVertx(), new JWTAuthOptions(keyStoreOptions));
  JWTAuthHandler jwtAuthHandler = JWTAuthHandler.create(jwtAuth);

  globals.setGlobal(JWTAuth.class, jwtAuth);
  globals.getRouter().route().handler(context -> {
    // only filter if we have a header, otherwise 
    // it will try to force auth, regardless whether
    // we want auth
    if(context.request().getHeader(HttpHeaders.AUTHORIZATION) != null)
      jwtAuthHandler.handle(context);
    else
      context.next();
  });
  
  return authProvider;
}

Then you can set create a resource that will serve your token (in a resource with no authorization checks):

@Produces("text/plain")
@GET
@Path("token")
public Single<Response> token(@HeaderParam("login") String username, 
                              @HeaderParam("password") String password,
                              @Context JWTAuth jwt,
                              @Context AuthProvider auth){
  
  JsonObject creds = new JsonObject()
          .put("username", username)
          .put("password", password);
  return fiber(() -> {
    User user;
    try {
      user = await(auth.rxAuthenticate(creds));
    }catch(VertxException x) {
      return Response.status(Status.FORBIDDEN).build();
    }
    
    boolean canCreate = await(user.rxIsAuthorised("create"));
    boolean canUpdate = await(user.rxIsAuthorised("update"));
    boolean canDelete = await(user.rxIsAuthorised("delete"));
    JsonArray permissions = new JsonArray();
    if(canCreate)
      permissions.add("create");
    if(canUpdate)
      permissions.add("update");
    if(canDelete)
      permissions.add("delete");
    
    String jwtToken = jwt.generateToken(new JsonObject()
                                         .put("username", username)
                                         .put("permissions", permissions),
                                         new JWTOptions()
                                           .setSubject("Wiki API")
                                           .setIssuer("Vert.x"));
    return Response.ok(jwtToken).build();
  });
}