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:

Reactive / Asynchronous super quick introduction

Redpipe is a reactive asynchronous web framework. This mostly means that you should not make blocking calls when Redpipe calls your methods, because that would block the Vert.x event loop and would prevent you from the performance benefits of asynchronous reactive programming.

Typically, instead of making a blocking call, asynchronous APIs will let you register a callback to invoke when the action is done. But callbacks are error-prone and do not compose well. The Promise model was introduced to abstract over callbacks and asynchronous computations, where a Promise represents a producer of a value that you can listen for, and be notified when the value is produced (we call that resolving a Promise).

RxJava is an implementation of the Promise model with support for single-value Promises (Single) and multiple-value Promise streams (Observable).

The idea of Redpipe is that you can either return normal values from your methods (if your code does not need to block), or RxJava promises (if it does), which Redpipe will register on, and be notified when the promises resolve and forward the resolved values to the client.

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>0.0.2</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(Mode.RAW)
@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, Completable and Maybe types for both RxJava 1 and 2, although use of RxJava 2 is recommended because RxJava 1 is deprecated.

If your resource returns a Single<T>, the T will be sent 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.

If your resource returns a Completable, an HTTP Response with status code 204 will be returned once the completable is resolved.

If your resource returns a Maybe<T>, then the following response will be sent asynchronously once the maybe is resolved:

  • If the maybe is empty: an empty HTTP response using HTTP status code 404 indicating the resource has not been found
  • If the maybe has been fulfilled with an object of type T: an HTTP response with status code 200, using the proper MessageBodyWriter<T>

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

In addition to the standard Vertx options, these 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
http_host String Http host to bind to. 0.0.0.0 (all interfaces)
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.reactivex.core.Vertx The Vert.x instance (Also available as RxJava 1 & core).
net.redpipe.engine.core.AppGlobals A global context object.
Injectable per-request resources
Type Description
io.vertx.reactivex.ext.web.RoutingContext The Vert.x Web RoutingContext (Also available as RxJava 1 & core).
io.vertx.reactivex.core.http.HttpServerRequest The Vert.x request (Also available as RxJava 1 & core).
io.vertx.reactivex.core.http.HttpServerResponse The Vert.x response (Also available as RxJava 1 & core).
io.vertx.reactivex.ext.auth.AuthProvider The Vert.x AuthProvider instance, if any (defaults to null) (Also available as RxJava 1 & core).
io.vertx.reactivex.ext.auth.User The Vert.x User, if any (defaults to null) (Also available as RxJava 1 & core).
io.vertx.reactivex.ext.web.Session The Vert.x Web Session instance, if any (defaults to null) (Also available as RxJava 1 & core).
io.vertx.reactivex.ext.sql.SQLConnection An SQLConnection (Also available as RxJava 1 & core).
@HasPermission("permission") boolean True if there is a current user and he has that permission.

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

Serving index.html at / by default

Usually web servers do this by default.

@Path("/{path:(.*)?}")
public class AppResource extends FileResource {

  @GET
  public Response index(@PathParam("path") String path) throws IOException {
    return super.getFile(path.equals("") ? "index.html" : path);
  }
}

Your home page will then be accessible from the root path (eg: http://localhost:9000).

Service discovery

You can declare and inject service records using the following optional module:

<dependency>
  <groupId>net.redpipe</groupId>
  <artifactId>redpipe-service-discovery</artifactId>
  <version>0.0.2</version>
</dependency>

Use the @ServiceName annotation to declare a service:

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

    @ServiceName("hello-service")
    @Path("hello")
    @GET
    public String hello() {
        return "hello";
    }
}

The same annotation can be used to look up services:

    @Path("service-user")
    @GET
    public String serviceDiscovery(@Context @ServiceName("hello-service") Record record,
            @Context @ServiceName("hello-service") WebClient client) {
        // You can use Record, or directly get a WebClient
    }

By default, if running on Kubernetes, Kubernetes services will be imported and available. Note that services declared using @ServiceName will not be exported to Kubernetes, but you can declare them in your build using Fabric8.

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

It is also possible to get a SQLConnection injected:

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

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

You can also inject permissions as a boolean with the @Context @HasPermission("permission-name") annotations.

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

OpenShift

Redpipe projects are very easy to deploy to OpenShift V3, using the Source-to-Image builders and fat jars.

You can see a sample Hello World application for yourself, but the main idea is to set up your main class so that it runs on the 8080 port:

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

And then configure your pom.xml so that it creates a fat-jar with the proper merging of META-INF/services files, and pointing to your Main class:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>fr.epardaud</groupId>
    <artifactId>redpipe-openshift-helloworld</artifactId>
    <version>0.0.2</version>
    <packaging>jar</packaging>

    <name>redpipe-openshift-helloworld</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>

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

    </dependencies>

    <profiles>
        <profile>
            <id>fat-jar</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <build>
                <finalName>redpipe-helloworld</finalName>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-shade-plugin</artifactId>
                        <executions>
                            <execution>
                                <phase>package</phase>
                                <goals>
                                    <goal>shade</goal>
                                </goals>
                                <configuration>
                                    <transformers>
                                        <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
                                    </transformers>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-jar-plugin</artifactId>
                        <configuration>
                            <archive>
                                <manifest>
                                    <mainClass>fr.epardaud.redpipe_openshift_helloworld.Main</mainClass>
                                </manifest>
                            </archive>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
        <profile>
            <id>flat-classpath-jar</id>
            <build>
                <finalName>redpipe-helloworld</finalName>
                <plugins>
                    <plugin>
                        <artifactId>maven-dependency-plugin</artifactId>
                        <executions>
                            <execution>
                                <phase>generate-sources</phase>
                                <goals>
                                    <goal>copy-dependencies</goal>
                                </goals>
                                <configuration>
                                    <outputDirectory>${project.build.directory}/lib</outputDirectory>
                                    <useRepositoryLayout>true</useRepositoryLayout>
                                </configuration>
                            </execution>
                            <execution>
                                <id>build-classpath</id>
                                <phase>generate-resources</phase>
                                <goals>
                                    <goal>build-classpath</goal>
                                </goals>
                                <configuration>
                                    <outputFile>${project.build.directory}/lib/classpath</outputFile>
                                    <localRepoProperty>lib</localRepoProperty>
                                </configuration>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

</project>

Then, just follow the official guidelines by pushing your code to GitHub and starting an image with it. You can even try our sample app at https://github.com/FroMage/redpipe-openshift-helloworld.git (no context dir).

Alternately, you can use Fabric8 to build and deploy your module by adding this to your pom.xml:

    <build>
        <plugins>
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>fabric8-maven-plugin</artifactId>
                <version>3.5.38</version>
            </plugin>
        </plugins>
    </build>

Declaring services with Fabric8

You can declare Kubernetes services, labels and annotations with this configuration:

    <build>
        <plugins>
            <plugin>
                <groupId>io.fabric8</groupId>
                <artifactId>fabric8-maven-plugin</artifactId>
                <version>3.5.38</version>
                <configuration>
                    <resources>
                        <labels>
                            <service>
                                <property>
                                    <name>MyLabel</name>
                                    <value>MyValue</value>
                                </property>
                            </service>
                        </labels>
                        <annotations>
                            <service>
                                <property>
                                    <name>MyAnnotation</name>
                                    <value>MyValue</value>
                                </property>
                            </service>
                        </annotations>
                        <services>
                            <service>
                                <name>MyService</name>
                            </service>
                        </services>
                    </resources>
                </configuration>
            </plugin>
        </plugins>
    </build>

Run your WebApp with Maven

This is a pom.xml sample to easily run your project. Just run mvn install exec:java

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>demo</groupId>
    <artifactId>demo</artifactId>
    <name>demo</name>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <redpipe.version>0.0.2</redpipe.version>
        <mainClass>demo.Main</mainClass>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>${mainClass}</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <dependency>
            <groupId>net.redpipe</groupId>
            <artifactId>redpipe-engine</artifactId>
            <version>${redpipe.version}</version>
        </dependency>
    </dependencies>
    
</project>