Baratine on GitHub

Getting Started

Baratine is a fast reactive web server for web services that pass messages between each other. Baratine abstracts this messaging to high-level method calls. This level of abstraction allows you to write reactive apps quickly using simple POJOs. Baratine streamlines reactive programming for a wide variety of applications like RESTful services and in-memory services.

Maven Dependency

First, create a new Maven project with the following dependency:

<dependency>
  <groupId>io.baratine</groupId>
  <artifactId>baratine</artifactId>
  <version>1.0.1</version>
</dependency>

Creating Your First Web Service

Baratine is a lock-free web server of asynchronous services. Services should not block, but instead pass values around using callbacks. Here is a simple web service:

@Service
public class HelloService {
  @Get("/hello")
  public void doHello(RequestWeb request) {
    request.ok("hello");
  }

  public static void main(String[] args) throws Exception {
    Web.include(HelloService.class);

    Web.go(args);
  }
}

RequestWeb

In the code above, RequestWeb is the callback that also holds information about the request and response like cookies and headers. It can be in any position in the method argument list. Calling RequestWeb.ok(value) completes the request and sends the response to the browser as JSON. Calling RequestWeb.fail(exception) fails the request with an exception.

HTTP Methods

HTTP methods are supported with @Get, @Post, @Put, etc. If you do not provide a value in the annotation, then Baratine will use the method name as the path. For example:

@Get // the path is implied from the method name
public void doHello(RequestWeb request) {
  ...
}

Request

RequestWeb provides the following information about the request:

String scheme();                     // protocol scheme (http vs. https)
String version();                    // protocol version (e.g. HTTP/1.1)
String method();                     // GET, POST, etc.
String uri();                        // full request URI
String uriRaw();                     // full request URI with entities not decoded
String path();                       // matching path mapping
String pathInfo();                   // matching path mapping
String path("id");                   // path in the parameterized URI (/hello/{id})
Map<String,String> pathMap();        // map of all the paths in the parameterized URI
String query();                      // full query string
String query(String name);           // query parameter for given name
MultiMap<String,String> queryMap();  // map of all query parameters
String header(String name);          // request header for given name
MultiMap<String,String> headerMap(); // map of all request headers
String cookie(String name);          // request cookie for given name
Map<String,String> cookieMap();      // map of all request cookies
String host();                       // value of Host header
int port();                          // server port
InetSocketAddress ipRemote();        // client remote address
InetSocketAddress ipLocal();         // server local address
String ip();                         // client remote address as a string
SecureWeb secure();                  // request SSL information

<X> X attribute(Class<X> key);       // stores request attribute, keyed on Class of val
<X> void attribute(X value);         // retrieves request attribute for given Class
<X> void body(Class<X> type, Result<X> result); // asynchronously reads body into object of given class

ServiceRef session(String name);     // retrieves a session service for given name
<X> X session(Class<X> type);        // retrieves a session service for given Class

Config config();                     // returns the Config object
Injector injector();                 // returns the Injector
Services services();                 // returns the Services service manager
ServiceRef service(String address);  // returns the ServiceRef for the service at given address
<X> X service(Class<X> type);        // returns the service proxy for the service of given type

@Query

@Query injects a request parameter into a method parameter:

@Get("/hello")
public void doHello(@Query("name") String name, RequestWeb request) {
  request.ok("hello " + name);
}

@Path

@Path injects the parameterized path in the URI into a method parameter:

@Get("/hello/{name}")
public void doHello(@Path("name") String name, RequestWeb request) {
  request.ok("hello " + name);
}

@Body

Baratine can read the PUT/POST body into a String or POJO:

@Post("/update")
public void doUpdate(@Body String body, RequestWeb request) {
  ...
}

For a body that is of Content-Type: application/x-www-form-urlencoded, you can use a Form to map HTML form inputs:

@Post("/update")
public void doUpdate(@Body Form body, RequestWeb request) {
  String value = body.first("input0");
  ...
}

Instead of @Body, you may use RequestWeb.body(type, result) to read the body asynchronously into a POJO of given type:

request.body(String.class, (value, e) -> {
  // do further processing
  ...

  // then complete the request
  request.ok("body is: " + value);
});

Response

RequestWeb provides the following response methods:

RequestWeb header(String key, String value);    // sets a response header
CookieBuilder cookie(String key, String value); // adds a response cookie
RequestWeb length(long length);                 // sets response length
RequestWeb type(Strign contentType);            // sets the Content-Type response header
RequestWeb encoding(String enc);                // sets thes response encoding
void upgrade(Object service);                   // upgrades the request to WebSockets, handled by given service
void ok();                                      // completes the request with an empty response
void ok(Object value);                          // completes the request with specified value
void fail(Throwable e);                         // closes the request with a fail status and exception
void halt();                                    // same as ok()
void halt(HttpStatus status);                   // same as status(status) then ok()
void redirect(String url);                      // redirects to url with an HTTP 302 status

Buffers buffers();                              // returns the factory for creating a Buffer
RequestWeb write(Buffer buffer);                // writes the Buffer to output
RequestWeb write(byte[] buf, int off, int len); // writes the array to output
RequestWeb write(String value);                 // writes the string to output
RequestWeb write(char[] buf, int off, int len); // writes the characters to output
RequestWeb flush();                             // flushes the output
Writer writer();                                // returns the Writer
OutputStream output();                          // returns the OutputStream

RequestWeb push(OutFilterWeb filter);           // pushes an OutFilterWeb output filter
Credits credits();                              // returns the Credits for managing flow control

Completing the Response

The request is held open until you call RequestWeb.ok(), RequestWeb.fail(), or RequestWeb.halt(). This allows processing to occur asynchronously and be completed at a later time without blocking.

Response Cookies

You can set cookies with RequestWeb.cookie(key, value). That method returns a CookieBuilder that you can set further options on:

request.cookie(key, value).httpOnly(true).secure(true).domain("foo.com");

Views

By default, Baratine serializes Strings as plain characters and objects as JSON. You can override it to handle a specific type by creating a ViewRender:

public class HelloService {
  @Get("/hello")
  public void doHello(RequestWeb request) {
    request.ok("hello");
  }

  public static void renderString(RequestWeb request, String value) {
    request.ok(value + value);
  }

  public static void main(String[] args) throws Exception {
    Web.view((ViewRender<String>) HelloService::renderString);

    Web.go(args);
  }
}

The code above creates a lamda ViewRender that overrides output handling for values of type String.class. The custom renderer outputs hellohello instead of hello.

Templates

Baratine supports the following popular templating engines out-of-the-box:

  • Freemarker (.ftl)
  • Jade (.jade)
  • Jetbrick (.jetx)
  • Mustache (.mustache)
  • Thymeleaf (.html)
  • Velocity (.vm)

To use a template, first Web.scanAutoConf() to enable the 3rd party templates. Then create a template file named src/main/resources/hello.mustache:

hello {{name}}

Finally complete the request with the file name and a value map

request.ok(View.newView("hello.mustache").add("name", "world"));

Baratine will pick the right template engine based on the file extension. Example:

public class HelloService {
  @Get("/hello")
  public void doHello(RequestWeb request) {
    request.ok(View.newView("hello.mustache").add("name", "world"));
  }

  public static void main(String []args) throws Exception {
    // enable 3rd party libraries
    Web.scanAutoConf();

    Web.include(HelloService.class);
    Web.go(args);
  }
}

You will also need to include the template library dependency, which in this case is mustache:

<dependency>
  <groupId>com.github.spullara.mustache.java</groupId>
  <artifactId>compiler</artifactId>
  <version>0.9.3</version>
</dependency>

WebSockets

Baratine’s asynchronous architecture matches well with WebSockets asynchronous messaging. You can upgrade requests to WebSockets by calling RequestWeb.upgrade() and passing in a ServiceWebSocket instance:

public class EchoWebSocket implements ServiceWebSocket<String,String> {
  public void open(WebSocket<S> webSocket) throws Exception {
    System.out.println("opened websocket connection");
  }

  public void next(String value, WebSocket<String> webSocket) throws Exception {
    webSocket.next("echoing: " + value);
  }

  public void close(WebSocket<String> webSocket) {
    System.out.println("closed websocket connection");
  }
}

public class MyWebService {
  @WebSocketPath("/echo")
  public void doUpgradePath(RequestWeb request) {
    request.upgrade(new EchoWebSocket());
  }

  public static void main(String[] args) throws Exception {
    Web.include(MyWebService.class);

    Web.go(args);
  }
}

@WebSocketPath is a shortcut for and is functionally equivalent to the following:

@Get("/echo")
public void doUpgrade(RequestWeb request) {
  if (request.header("Connection").toLowerCase().contains("upgrade")) {
    request.upgrade(new EchoWebSocket());
  }
  else {
    request.fail("not a websockets request");
  }
}

Services

@Service creates an isolated service with its own inbox queue and thread.

Whereas the RequestWeb callback is only available for HTTP contexts, Result is a general callback for all other Baratine services. You may use Result in place of RequestWeb, but not vice versa. The following is a valid GET method:

@Get("/hello")
public void doHello(Result<String> result) {
  result.ok("hello");
}

Service methods not annotated with @GET, @POST, etc. must use Result or none at all.

To create a service, simply annotate a class with @Service:

@Service
public class CounterService implements CounterServiceApi {
  private long count;

  public void addAndGet(long increment, Result<Long> result) {
    count += increment;

    result.ok(count);
  }
}

The public class methods become callable service methods. Although not required, it is best practice to define a service API:

@Service
public interface CounterServiceApi {
  void addAndGet(long increment, Result<Long> result);
}

@Service may go on the interface or the implementing class. By having it on the interface, you can inject services based on the interface instead of the class.

Then tell Baratine about this service with:

Web.include(CounterService.class);

Other services may inject this service with:

@Service
public class HelloService {
  @Inject @Service
  private CounterServiceApi counter;

  @Get("/hello")
  public void doHello(RequestWeb request) {
    counter.addAndGet(
      1,
      (count, e) -> {
          if (e != null) {
            request.fail(e);
          }
          else {
            request.ok("hello " + count);
          }
      }
    );
  }

  public static void main(String[] args) throws Exception {
    Web.include(HelloService.class);
    Web.include(CounterService.class);

    Web.go(args);
  }
}

The example above first calls CounterServiceApi.getAndIncrement(). Once the result comes back, it completes the HTTP request with RequestWeb.ok("hello " + count).

Propagating Exceptions

It is a hassle to have to check exceptions every time in your callback. It gets worse for nested callbacks. There is a shorthand to write them compactly. RequestWeb and Result come with the then() method to automatically propagate values and exceptions from nested service calls. The HelloService.doHello() above may be succintly rewritten as:

@Get("/hello")
public void doHello(RequestWeb request) {
  _counter.addAndGet(1, request.then(count -> "hello " + count));
}

Persistence

A service that is annotated with @Asset is persisted automatically into Baratine’s internal document database, getting-started-kraken, when at least one @Modify method is called:

@Asset
@Service
public class CounterService implements CounterServiceApi {
  private long count;

  @Modify
  public void addAndGet(long increment, Result<Long> result) {
    count += increment;

    result.ok(count);
  }
}

@Asset may go on either the interface or the implementing class.

You can manage persistence yourself by hooking into the Service Lifecycle @OnLoad and @OnSave. The example below uses JDBC to store the data into an SQL database:

@Service
public class CounterService implements CounterServiceApi {
  @Inject @Service("jdbc:///foo")
  private JdbcService jdbc;

  private long count;

  @Modify
  public void addAndGet(long increment, Result<Long> result) {
    count += increment;

    result.ok(count);
  }

  @OnLoad
  public void onLoad(Result<Void> result) {
    jdbc.query(
      result.then(this::onLoadComplete),
      "SELECT count FROM test LIMIT 1"
    );
  }

  private Void onLoadComplete(ResultSet rs) {
    try {
      if (rs.next()) {
        count = rs.getLong(1);
      }

      return null;
    }
    catch (SQLException e) {
      throw new RuntimeException(e);
    }
  }

  @OnSave
  public void onSave(Result<Void> result) {
    jdbc.execute(
      result.then(updateCount -> null),
      "INSERT INTO test VALUES (?)",
      count
    );
  }
}

@OnLoad is called only once on service startup. On the other hand, @OnSave may be called multiple times to checkpoint the state of the service.

Service Lifecycle

A service supports lifecycle hooks for intialization and persistence. They are represented by method annotations:

  • @OnInit : called to initialize the service
  • @OnActive : called when service is ready
  • @OnDestroy: called when service is going away
  • @OnLoad : called to load initial data into the service
  • @OnSave : called when @Modify methods have been called at least once

Example:

@OnSave
public void onSave(Result<Void> result) {
  // do something when @Modify has been called and a save is requested

  // then complete the callback
  result.ok(null);
}

@OnSave is called if and only if one or more @Modify methods have been called in the past:

@Modify
public void addAndGet(long increment, Result<Long> result) {
  count += increment;

  result.ok(count);
}

Because requests are batched and journaled (enabled with @Journal), Baratine can choose to persist whenever it decides it is a good time to do so. For M calls to @Modify, @OnSave is only called N times, where N <= M. This means that for 10k modifications, potentially only one save is requested, removing the database bottleneck entirely from applications.

Singleton Services

By default, a service is a singleton. See Vault Services for how to implement a map of persistent services.

Vault Services

A Vault is a map of @Asset (persistent) service instances. It is useful for implementing RESTful services. It is composed of 2 components: a vault and an asset:

BookVault
@Service
public interface BookVault extends Vault<IdAsset,BookAsset>
{
  void create(Book book, Result<IdAsset> result);

  void findByTitle(String title, Result<BookAsset> result);

  void delete(IdAsset id);
}

The vault is itself a service with CRUD (create, replace, update, delete) operations on a persistent datastore (defaults to Kraken Embedded Database). It may be an interface in which Baratine automatically fills out the implementation.

Book
public class Book
{
  protected String title;
  protected String author;
}
BookAsset
@Asset
public class BookAsset extends Book
{
  @Id
  private IdAsset id;

  public void get(Result<Book> result)
  {
    // okShim() calls ok() with a shallow copy of this service to prevent
    // accidental modifications of this service object from outside
    result.okShim(this);
  }

  @Modify
  public void set(Book book, Result<Boolean> result)
  {
    this.title = book.title;
    this.author = book.author;

    result.ok(true);
  }
}

The book is an @Asset with an injected ID that is the primary key.

Usage example:

@Service
public class BookRest {
  @Get("/book/{id}")
  public void getBook(@Path("id") String id,
                      RequestWeb request) {
    BookAsset asset = request.service(BookAsset.class, id);

    asset.get(request.then());
  }

  @Post("/book/{id}")
  public void setBook(@Path("id") String id,
                      @Body Book book,
                      RequestWeb request) {
    BookAsset asset = request.service(BookAsset.class, id);

    asset.set(book, request.then(isSuccessful -> "ok"));
  }

  @Get("/book")
  public void createBook(@Query("title") String title,
                         @Query("author") String author,
                         RequestWeb request) {
    BookVault vault = request.service(BookVault.class);

    Book book = new Book();
    book.title = title;
    book.author = author;

    vault.create(book, request.then(id -> id.toString()));
  }

  public static void main(String[] args) throws Exception {
    Logger.getLogger("").setLevel(Level.FINER);

    Web.include(BookRest.class);
    Web.include(BookVault.class);

    Web.go(args);
  }
}

BookAsset asset = request.service(BookAsset.class, id) retrieves the service proxy/handle by going through the vault and loading the asset from the database. Because it is just a service handle, the proxy does not hold any data. To actually get data out of the service, you’ll need to call a method on the asset like asset.get().

Embedded Services

You can embed Baratine into your existing applications. Given a service named CounterService, you can start it and call it with:

ServicesBuilder builder = Services.newManager();
builder.service(CounterService.class);

Services services = builder.start();

// then look up and obtain a proxy to your new service
CounterService counter = services.service(CounterService.class);

Sessions

In Baratine, sessions are handled by session services. A session service instance is tied to a user as determined by the session cookie. To define a session service, use @Session:

@Session
public class MySession {
  @Id // injects the session cookie ID into this field
  private String id;

  private String user;

  @Get("/login/{user}")
  public void login(@Path("user") String user, RequestWeb request) {
    this.user = user;

    request.ok("user logged in as: " + user );
  }

  public void getUser(Result<String> result) {
    result.ok(user);
  }
}

Then to use it:

@Service
public class HelloService {
  @Inject @Service
  private MySession _session;

  @Get("/hello")
  public void hello(RequestWeb request) {
    _session.getUser(request.then(user -> "hello " + user);
  }

  public static void main(String[] args) throws Exception {
    Web.include(HelloService.class);
    Web.include(MySession.class);

    Web.go(args);
  }
}

If a user logs in by sending a request to /login/user0, then a subsequent request to /hello would print out hello user0.

Instead of injection, you may call RequestWeb.session(MySession.class):

@Service
public class HelloService {
  @Get("/hello")
  public void hello(RequestWeb request) {
    MySession session = request.session(MySession.class);

    session.getUser(request.then(user -> "hello " + user));
  }

  public static void main(String[] args) throws Exception {
    Web.include(HelloService.class);
    Web.include(MySession.class);

    Web.go(args);
  }
}

Unless you persist sessions with @Asset or @OnLoad/@OnSave, sessions are only kept in memory and are lost when Baratine shuts down.

Databases

JDBC

Note

JdbcService is only available in 1.0.1+.

JdbcService is a Baratine service for querying JDBC databases asynchronously. To use it, first include your driver dependency:

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.39</version>
</dependency>

Then configure JdbcService:

public static void main(String[] args) throws Exception {
  String serviceUrl = "jdbc:///foo";

  Web.property(serviceUrl + ".url",  "jdbc:mysql://localhost/myDb")
  Web.property(serviceUrl + ".user", "root");
  Web.property(serviceUrl + ".pass", "mypassword");

  Web.go(args);
}

Then inject that specific JdbcService into your service:

@Inject @Service("jdbc:///foo")
private JdbcService jdbc;

Finally you can start sending queries asynchronously:

jdbc.query(
  (JdbcResultSet rs, Throwable e) -> {
      int columns = rs.getColumnCount();
      int row = 0;
      for (JdbcRowSet rowSet : rs) {
        System.out.println("row: " + row++);
        for (int i = 0; i < columns; i++) {
          System.out.println("\t" + rowSet.getObject(i));
        }
      }
  },
  "SELECT * FROM test"
);

JdbcService supports operating on the java.sql.Connection directly with a SqlFunction<T> (a java.util.function.Function<Connection,T>):

jdbc.query(
  (value, e) -> {
      System.out.println("result is: " + value);
  },
  (conn) -> {
      PreparedStatement stmt = conn.prepareStatement("INSERT INTO testTable VALUES(?, ?)");
      stmt.setInt(1, 333);
      stmt.setString(2, "new0");

      stmt.execute();

      int updateCount = stmt.getUpdateCount();
      stmt.close();

      // can return arbitrary types and values
      return updateCount;
  }
);

Embedded Database

Baratine comes with a fast built-in document database called Kraken that is fully asynchronous. Whereas JdbcService is an asynchronous facade to blocking databases, Kraken’s entire architecture is 100% lock-free with zero blocking. To use it:

KrakenBuilder krakenBuilder = Kraken.newDatabase();
Kraken kraken = krakenBuilder.get();

DatabaseKraken db = kraken.database();

db.execute(Result.ignore(), "CREATE TABLE test(id INT PRIMARY KEY, data VARCHAR)");
db.execute(Result.ignore(), "INSERT INTO test (id, data) VALUES (0, 'Hello World!')");

db.query(
  (ResultSetKraken rs, Throwable e) -> {
    for (List<Object> row : rs) {
      System.out.println(row);
    }
  },
  "SELECT id, data FROM test"
);