Baratine on GitHub

HTTP/Web API

Web Server Overview

Baratine’s web server is service-based, using a continuation result to manage asynchronous calls. Applications can be written as single-threaded services, but must not block, instead using the Result to continue the request on a completion.

Because gateway services to existing services are allowed to block, Baratine applications are a combination of non-blocking services, and blocking gateways to existing Java libraries.

The web server supports two styles of configuration:

  • A Java DSL programmatic configuration for straightforward services
  • An annotation-based configuration for services using dependency injection

It also supports two styles of output generation:

  • View style, where the service returns a result object and a view resolver such as a JSON writer renders the output.
  • Direct output with writers and output streams.

Java DSL and Lambdas

Programmatic web configuration builds routes and services using the Web class and its Java DSL. The web server is built and started with routes from the Web class in a main method. A hello, world might look like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import static io.baratine.web.Web.*;

public class Hello
{
  public static void main(String []args)
  {
    get("/hello").to(req->req.ok("Hello, world"));

    go(args);
  }
}

This code example is a complete web server that listens to the default port 8080. It uses the view-style output and the built-in String view renderer.

  • get establishes a HTTP path.
  • to creates a ServiceWeb handler.
  • req.ok completes the request and sends a value to the view processor
  • go executes the command-line and starts the web server.

The ok() sends the value to the view processor, where the view is selected by the result type. This example uses the default String handler. If the result was an object, the default JSON handler would be used.

Baratine services use ok() instead of a return value because of the continuation result style of programming. The result style allows for non-blocking and non-locking services. A method return does not end the request. Instead, requests complete using the ok() method on success or fail() on failure.

When the web service calls other services, such as an authentication service or a database service or a gateway service to a Java library, the continuation result allows efficient reuse of the web-service’s thread while the authentication or database is doing its work. See Baratine Service for more details on the service model.

See Web for programmatic web server configuration.

Annotated Web Methods

The web server is built and started with the Web class in a main method. A typical use might look like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import io.baratine.service.Result;
import static io.baratine.web.Web.*;
import io.baratine.web.Get;

public class Hello
{
  @Get
  void hello(RequestWeb request)
  {
    request.ok("Hello, world");
  }

  public static void main(String []args)
  {
    include(Hello.class);

    go(args);
  }
}

This code example is a complete web server that listens to the default port 8080.

  • include introspects a class for services and HTTP methods.
  • @Get marks a HTTP get method, which uses the method name by default.
  • request.ok sends data to the view processor.
  • go executes the command-line and starts the web server.

ServiceWeb

Web services and Filters implement the ServiceWeb interface.

The request completes when the request.ok() or request.fail() methods are called. If the service returns without an ok or fail, the request is held waiting to complete. Typically, the pending request will complete with a result from an asynchronous call.

A bookstore checkout request might call a backend credit service for validation. While the credit request is processing, the original service call returns its thread to be used by another request. When the credit check completes, the request wakes after reading the response from the inbox, and completes the HTTP request.

package io.baratine.web;

@FunctionalInterface
public interface ServiceWeb
{
  void service(RequestWeb request)
     throws Exception;
}

To support lambdas in the Web dynamic configuration, Web.path().to(), ServiceWeb is a functional interface with a single RequestWeb argument that encapsulates the HTTP request and response.

RequestWeb

The HTTP request and response is available in the RequestWeb.

RequestWeb allows direct output as well as the view-based results.

Requests end with an “ok()” call.

1
2
3
4
5
6
@Get
void hello(RequestWeb request)
{
  request.write("Hello, world");
  request.ok();
}

RequestWeb provides standard HTTP access, as well as access to application sessions and services.

HTTP request and network parameters

HTTP request and network information is available in the following methods.

public interface RequestWeb extends Result<Object>
{
  String protocol();         // http or https
  String version();          // HTTP/1.1

  String method();           // GET, POST, etc.

  String uri();              // full HTTP URI
  String path();             // part of URI matching the path pattern
  String pathInfo();         // trailing path for /* patterns
  String path(String id);    // named path pattern /{id}
  Map<String,String> pathMap();

  String query();           // query string
  String query(String key); // query param
  MultiMap<String,String> queryMap();

  String header(String key);
  String cookie(String key);

  String host();
  int port();

  String ip();
  InetSocketAddress ipRemote();
  InetSocketAddress ipLocal();

  SecureWeb secure();

  ...
}

RequestWeb.path()

RequestWeb.path() methods return parsed URL request paths information, including path variables.

Paths can use pattern strings, whether defined by programmatic Web.get or annotated ``@Get, and other methods.

path("id") returns a component defined by “{id}”, such as “/book/{id}”, which matches “/book/2712”.

View results

View-based output passes the ok(Object) result to the view renderer.

See the view section for a more complete description of view rendering.

public interface RequestWeb extends Result<Object>
{
  ...
  void ok(Object value);
  void fail(Throwable exn);
  ...
}

RequestWeb.ok()

RequestWeb.ok() completes a successful request. There are two forms of ok: a value-based ok() for views and a zero-arg ok() for direct output.

HTTP response headers and output

HTTP headers and output can be set directly instead of using the view system.

When writing to output directly, the request must call the zero-param ok() method or the fail() method to complete the request.

The output will be buffered if necessary to prevent output blocking. A sophisticated application can use credits() for flow-control to minimize the buffering required.

public interface RequestWeb extends ResultChain<Object>
{
  ...

  //
  // response headers
  //

  RequestWeb status(HttpStatus status);
  RequestWeb header(String key, String value);
  CookieBuilder cookie(String key, String value);
  RequestWeb length(long contentLength);
  RequestWeb type(String contentType);
  RequestWeb encoding(String encoding);

  //
  // output results
  //

  void ok();
  void halt();
  void halt(HttpStatus status);
  void redirect(String location);

  void write(String value);
  void write(char []buffer, int offset, int length);

  void write(Buffer buffer);
  void write(byte []buffer, int offset, int length);

  OutputStream output();
  Writer writer();

  Credits credits();

  void flush();

  ...
}

CookieBuilder

CookieBuilder sets cookie parameters like HttpOnly.

By default Baratine sets HttpOnly, sets path to “/*”, and when the connection is SSL it sets Secure.

public interface CookieBuilder
{
  CookieBuilder httpOnly(boolean isHttpOnly);
  CookieBuilder secure(boolean isSecure);
  CookieBuilder path(String path);
  CookieBuilder domain(String domain);
}

body()

The body() method in RequestWeb returns the body programatically.

body() parsing uses an expected type to choose the parsing. The parsers can be customized using injection. The built-in types include:

  • String is parsed as a String using the content-type of the post.
  • Application beans are deserialized from JSON or from a form.
  • Map parses forms and JSON as a map
  • Form parses forms as a multi-map
  • InputStream reads the result after buffering to avoid blocking.
  • Reader reads the result after buffering to avoid blocking.

body() requires a callback, because the input data might not be available yet, and Baratine services must not block.

public interface RequestWeb extends Result<Object>
{
        ...
        <X> void body(Class<X> type, Result<X> result);
        <X> void bodyThen(Class<X> type, BiConsumer<X,RequestWeb> after);
        ...
}

The bodyThen is a convenience method, which combines body() and request.then(after).

In the following example, Book is the expected type, which is parsed as JSON because it’s an application class. While the body is being read from the network, the request releases its thread and the thread processes new requests. When the body is complete, the continuation processes the body.

1
2
3
4
5
6
7
@Put
void book(RequestWeb request)
{
        request.bodyThen(Book.class, (book,request)->{
                request.ok("Book: " + book.title());
        });
}

Sessions

See sessions for more information.

Sessions encapsulate data with a vault/asset service. Each session type has its own session instance. A request may have multiple session instances. Sessions are registered like services, using the Web.include() method or using newService.

Because sessions are services, they use continuation Result instead of method returns.

To get a session instance, use the session() method in RequestWeb with the session’s API or the service address.

public interface RequestWeb extends ResultChain<Object>
{
        ...
        ServiceRef session(String name);
        <X> X session(Class<X> type);
        ...
}

A new session instance and session cookie will be created if necessary.

1
2
3
4
5
6
7
@Get
public void hello(RequestWeb request)
{
        MySession session = request.session(MySession.class);

        session.hello(request.of());
}

Services

See Baratine Service for more information on services.

Services are available in RequestWeb or through injection. Services are registered with Web.include(), or web-service, or with Services.

Services are looked up by an address with URL syntax. By convention, the service class name is used as a default. The class-name convention allows looking up by a class type.

Service lookup returns a ServiceRef or a proxy.

public interface RequestWeb extends ResultChain<Object>
{
        ...
        Services services();

        ServiceRef service(String address);
        <X> X service(Class<X> type);
        <X> X service(Class<X> type, String assetId);
        ...
}

Vault service assets can be looked up with the asset class and the assetId.

The following example looks up a book asset in a book vault with an id segment of its URL. It calls the book’s get() method and uses the return as the method’s view object.

1
2
3
4
5
6
7
@Get("/book/{id}")
public void book(@Path("id") id, RequestWeb request)
{
  Book book = request.service(Book.class, id);

  book.get(request.of());
}

ResultChain - service chaining

ResultWeb provides several chaining methods to simplify continuation programming. Result chaining simplifies continuation/result calls to services. Because web services don’t block, a call to an internal service uses a result callback to continue the request when the result is ready.

A REST bookstore web service might call a book vault service for the book information. Because the book vault is a service, the web service uses request.of() to chain the book’s result to the service’s view.

1
2
3
4
5
6
7
@Get("/book/{id}")
public void book(@Path("id") id, RequestWeb request)
{
  Book book = request.service(Book.class, id);

  book.get(request.of());
}

When book.get returns the book contents, the result will call request.ok on success or request.fail on failure.

request.of()

request.of() chains a result using a function for any post-processing. When the target service completes, the of() function processes the result and calls request.ok() with the final result.

request.of() automatically handles service exceptions by sending them to the request.fail() method.

public interface RequestWeb extends ResultChain<Object>
{
  ...
  <R> Result<R> of(Function<R,Object> after);

  <R> Result<R> of();
  ...
}

The after function allows for post-processing of a result.

The following example gets a book asset’s contents and returns a string with only the title. The lambda function is called after the ``book.get `` completes with the book data as the function argument.

1
2
3
4
5
6
7
8
9
@Get("/book/{id}")
public void book(@Path("id") id, RequestWeb request)
{
  Book book = request.service(Book.class, id);

  book.get(request.of(book->{
    return "title: " + book.title();
  });
}

request.then()

request.then() chains a result using a consumer for post-processing. When the target service completes, the then() consumer processes the result. The post-processor is responible for completing the request by calling ok() or fail().

public interface RequestWeb extends ResultChain<Object>
{
  ...
  <R> Result<R> then(BiConsumer<Object,RequestWeb> after);
  ...
}

Because request.then() can use any RequestWeb method in its continuation, it is more flexible than request.of().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Get("/book/{id}")
public void book(@Path("id") id, RequestWeb request)
{
  Book book = request.service(Book.class, id);

  book.get(request.then((book,req)->{
    req.write("title: " + book.title());
    req.ok();
  });
}

Config

Configuration is available with the config() method.

An application can use baratine.yml and the command-line for configuration. The combined configuration is available in RequestWeb or by injection.

public interface RequestWeb extends ResultChain<Object>
{
  ...
  Config config();
  ...
}

Injector

See Injection for more information on injection.

public interface RequestWeb extends ResultChain<Object>
{
  ...
  Injector injector();
  ...
}

Upgrade

upgrade() is used to upgrade the HTTP connection to websocket.

public class EchoWebSocket implements ServiceWebSocket<String,String> {
  public void open(WebSocket<S> webSocket) throws Exception {
    webSocket.next("hello");
  }

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

  public void close(WebSocket<S> webSocket)
  {
    System.out.println("closing");
  }
}
@Get("/echo")
public void handleChat(RequestWeb request)
{
  String connectionHeader = request.header("Connection");

  if (connectionHeader != null && connectionHeader.contains("Upgrade")) {
    request.upgrade(new EchoWebSocket());
  }
  else {
    request.ok("not a websocket request");
  }
}

Views

View rendering are one of two methods to generate formatted output. The other is direct writing to an OutputStream or Writer. Views can use standard formats like JSON and rendering engines like Mustache or Freemarker.

A web service returns an object in its request-ok method, which sends the object to the view resolver. Based on the object, the view resolver chooses a view renderer. The matching view writes the object to the output using the output or writer methods of RequestWeb.

The View type is a standard view object that has a view template name and a map of values. Rendering engines like Freemarker support View.

The following example uses Freemarker to process the view, as long as Freemarker is in the classpath. The hello.ftl template belongs in the classpath under classpath:template/hello.ftl, which is src/main/resources/template if using a standard build directory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Hello
{
  @Get
  void hello(Result<View> result)
  {
    result.ok(View.newView("hello.ftl")
                  .add("greeting", "hello"));
  }

  public static void main(String []args)
  {
    Web.scanAutoConf();

    Web.include(Hello.class);

    Web.go(args);
  }
}

In the previous example, the Freemarker view engine is detected by by Web.scanAutoConf() if it’s in the classpath. Its view resolver matches the View class and the **.ftl* extension. When the application returns a View to ok(), a view resolver selects Freemarker because the View type and view template match.

Built-in Views

Several view types are built-in including:

  • String
  • View for template engines
  • Other objects render as JSON

String

The String view encodes the output as a content-type of text/plain and a UTF-8 encoding.

Object (JSON)

Other objects are encoded as JSON.

1
2
3
4
5
@Get
void book(Result<Book> result)
{
  result.ok(new Book("Hamlet", "Shakespeare"));
}

View

View value is dispatched according to the view name. When scanAutoConf() is enabled and the view renderer is in the classpath, that renderer is selected.

Templates belong in classpath:/templates. When using a standard build path, they are put in src/main/resources/templates.

Current plugins include:

  • Freemarker
  • Jade
  • Jetbrick
  • Mustache
  • Thymeleaf
  • Velocity
1
2
3
4
5
6
@Get
void hello(Result<View> result)
{
    result.ok(View.newView("hello.ftl")
                   .add("greeting", "hello, world"));
}

Custom View Rendering

ViewRender

Custom view handlers can be bound to the dependency injector. They will take priority over built-in view handlers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import static io.baratine.web.Web.*;
import io.baratine.web.Get;

public class Hello
{
  public static void main(String []args)
  {
    bean((ViewResolver<String>) value->Hello::renderString);

    get("/hello", req->ok("hello"));

    go(args);
  }

  private static void renderString(RequestWeb req, String value)
  {
    req.write("{{" + value + "}}");
    req.ok();
  }
}
    1. The hello method remains unchanged with the new view.
    1. Binding a lambda to ViewWeb<String>. A view class would also work.
    1. The view render method uses RequestWeb to write the data.
    1. ok() is required to complete the request.
    1. The ViewWeb.render returns true to indicate that it’s handling the view.

Web

Web configures and starts a web server with either static methods or access to a WebServerBuilder.

Start and Go

start() and go() start the web server after it’s configured. start() is used in testing because it starts the web server in the background. go() is used in production because it executes the command line, which typically blocks the main thread until the server exits.

public interface Web
{
  ...
  static void go(String ...argv);
  static WebServer start(String ...argv);
  ...
}

Web.go()

go() executes the command line, or uses the default command to start the server if no other command is given. The default start command blocks the server until it exits.

Web.start()

start() starts the web server in a background daemon thread. It is useful for testing.

include and scan - class Introspection

The web server configures annotation-based services by introspecting classes. The include() method adds service classes directly. The scan() method scans the classpath for configuration classes. The scanAutoConf() scans for automatically supported class drivers, such as view rendering engines.

public interface Web
{
  ...
  static WebServerBuilder include(Class<?> includeType);
  static WebServerBuilder scan(Class<?> baseClass);
  static WebServerBuilder scanAutoConf();
  ...
}

Web.include()

Web.include introspects a class for annotated web services, injection beans, and Baratine services.

Web.include scans for the following:

  • HTTP methods, such as @Get, @Post
  • @Service implementation classes and provider methods.
  • @Bean injection provider methods
  • IncludeWeb and IncludeInject classes

Web.include is useful for testing, because it gives precise control over the service classes, allowing mock classes to replace services.

The following hello, world example uses Web.include to configure the HTTP /hello path to a service method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import io.baratine.web.*;

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

  public void main(String []argv)
  {
    Web.include(MyHello.class);

    Web.go(argv);
  }
}

Web.scan()

Web.scan() scans the classpath for include classes, using a root package class to restrict the scan. Only classes in the same package or a child package are selected.

Web.scan() looks for classes annotated by @Include or that implement the IncludeGenerator interface. All other classes are ignored.

Classes that match are processed as if they were configured with Web.include().

Web.scanAutoConf()

Web.scanAutoConf() scans for internal auto-configuration drivers. These drivers add optional services based on the classpath.

For example, view engines such as Mustache are added if the engine exists on the classpath.

HTTP route methods

HTTP services can be configured programmatically using the Web route methods such as get() and post(). The method implementations can use JDK 8 lambdas to implement the services.

public interface Web
{
  ...
  static WebResourceBuilder delete(String path);
  static WebResourceBuilder get(String path);
  static WebResourceBuilder options(String path);
  static WebResourceBuilder patch(String path);
  static WebResourceBuilder post(String path);
  static WebResourceBuilder put(String path);
  static WebResourceBuilder route(String path);
  ...
}

The resource builder typically uses a to() method to define a service.

A complete hello web server would look like the following:

1
2
3
4
5
6
7
8
9
public class MyHello
{
  public void main(String []argv)
  {
    Web.get("/hello").to(req->req.ok("hello"));

    Web.go(argv);
  }
}

Note

HTTP routes use a single Baratine Service. If an application needs multiple services for different routes, the annotation-based HTTP routes can use independent services.

Path patterns

Paths can use patterns:

  • /path/* matches a prefix
  • /path/{id} matches a named component
  • /path/*.html matches a suffix

Web.get()

Web.get() defines a HTTP GET method. Its service is defined in a following to() call.

WebSocket

A WebSocket service can be configured programmatically.

public interface Web
{
 ...
  static WebSocketBuilder websocket(String path);
}

WebSocket services can also be configured using Web.include().

Configuration Properties

Configuration properties are read from the baratine.yml, the command line, the environment and the Web builder.

public interface Web
{
  ...
  static WebServerBuilder port(int port);
  static WebServerBuilder property(String name, String value);
}

Web.property()

Web.property() sets a configuration property programmatically. It has lower priority than the command line, serving as a default.

public class MyServer
{
  public static void main(String []argv)
  {
    Web.property("server.port", "8086");

    Web.get("/hello", req->req.ok("hello"));
    Web.go(argv);
  }
}

Annotation Web Services

Web services can use annotations to be registered. In the main method, Web.include() or Web.scan() registers the web classes. On startup, the classes are scanned for HTTP methods, which are used as web services.

HTTP methods are asynchronous, using continuation style returns instead of method return values. They are Baratine services: single-threaded and non-blocking, using continuation calls to their resources.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import io.baratine.service.Result;
import static io.baratine.web.Web.*;
import io.baratine.web.Get;

public class Hello
{
  @Get
  void hello(RequestWeb request)
  {
    request.ok("Hello, world");
  }

  public static void main(String []args)
  {
    include(Hello.class);

    go(args);
  }
}

RequestWeb and Result - service results

Annotation-configured web services use RequestWeb or request for the service results. RequestWeb can use a view renderer or write the output directly. Result always uses a view renderer.

Both RequestWeb and Result complete a request with RequestWeb.ok(). Until the ok() is called, the request continues, even after the method returns. This continuation style enables the non-blocking calls to service resources.

A web method must have exactly one of RequestWeb or Result as one of its parameters.

1
2
3
4
5
@Get
public void hello(RequestWeb request)
{
  request.ok("Hello, world");
}

Result

The Result parameter is used in a view-based web model. The request will only complete when result.ok() or result.fail() is called.

The result value is passed to the view resolver, which will choose a view for the result. The default resolver encodes objects as JSON and Strings as text/plain.

1
2
3
4
5
@Get
void book(Result<Book> result)
{
  result.ok(new Book("Macbeth", "Shakespeare"));
}
$ curl http://localhost:8080/book
{"title": "Macbeth", "author": "Shakespeare"}

HTTP Method Annotations

HTTP services are defined with one of the HTTP method annotations. The annotation matches a URL path, which can be defined as the annotation value, and can use path patterns, such as /book/{id}.

If the path is unspecified, it defaults to the method name.

package io.baratine.web.*;

@Delete
@Get
@Patch
@Post
@Put
@Route
@Trace

The following matches the URL /hello because the method is hello()

1
2
3
4
5
@Get
public void hello(Result<String> result)
{
  result.ok("hello");
}

@Get

@Get matches a HTTP GET request and executes the annotated method.

@Route

@Route is used to match all HTTP methods.

Path patterns

The following are the path patterns:

  • /my-path - exactly matches a path
  • /my-path/* - matches a path and all children. The tail is available with pathInfo().
  • /my-path/{id1}/{id2} - matches components and saves to the RequestWeb.path() map.
  • /my-path/*.html - matches a suffix

The path defaults to the method name, but can be specified and include path parameters, which can be assigned to method arguments.

1
2
3
4
5
@Get("/book/{id}")
void book(@Path("id") String id, Result<String> result)
{
  result.ok("Book: " + id);
}
  1. The path matches /book/13, but not /book or /book/13/title.

Request Parameters

The service method can extract request parameters as method parameters using parameter annotations.

package io.baratine.web.*;

@Body
@Cookie
@Header
@Path
@Query

The parameter type is automatically converted from the source string. The converter can convert to service proxies.

The following extracts the query parameter “id” as its parameter value.

1
2
3
4
5
@Get
public void book(@Query("id") IdAsset id, Result<String> result)
{
  result.ok("Book: " + id);
}

@Path

@Path extracts a path parameter to a web-service method. The string value is converted to the parameter type if necessary.

1
2
3
4
5
@Get("/book/{id}")
public void book(@Path("id") IdAsset id, Result<String> result)
{
  result.ok("Book: " + id);
}

@Query

@Query extracts a query-string parameter to a web-service method. The string value is converted to the parameter type if necessary.

1
2
3
4
5
@Get
public void book(@Query("id") IdAsset id, Result<String> result)
{
  result.ok("Book: " + id);
}

@Body

@Body is a HTTP method parameter annotation used to read HTTP content.

The HTTP body must be read completely before processing, because Baratine services must not block. When using the @Body annotation, Baratine will read the body completely before calling the service method.

The body type is determined by the configured body resolvers like the views. The following uses the built-in body-form resolver.

1
2
3
4
5
@Put
void book(@Body Form form, Result<String> result)
{
  result.ok("Book: " + form.get("title"));
}

Application objects are deserialized using JSON.

1
2
3
4
5
@Put
void book(@Body Book book, Result<String> result)
{
  result.ok("Book: " + book.title());
}

Low-level InputStream and Reader are also available:

1
2
3
4
5
@Put
void data(@Body InputStream is, Result<String> result)
{
  result.ok(myParse(is));
}

Static Files

Static files are read from classpath:/public.

When using a standard build path, an index file will belong in src/main/resources/public/index.html.

SSL

SSL is enabled with configuration properties. The current configuration properties are:

server.ssl.enable: true
server.ssl.key-store: /path/to/keystore.jks
server.ssl.key-store-password: changeme
server.ssl.key-password: changeme
server:ssl.password: changeme

The minimum to set SSL is the “enable”, “key-store”, and “password”.

These can be configured in the configuration file or using Web.property

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class HelloSSL
{
  public static void main(String []args)
  {
    Web.property("server.port", "8080");
    Web.property("server.ssl.enable", "true");
    Web.property("server.ssl.key-store", "/home/baratine/baratine.jks");
    Web.property("server.ssl.password", "changeme");

    Web.get("/test", req->req.ok("Secure: " + req.secure()));
    Web.go(args);
  }
}

WebSockets

WebSockets are implemented with the ServiceWebSocket interface. They receive and send string or binary messages.

Like other services, WebSockets are registered with Web.include.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Path("/echo")
public class WebSocketEcho implements ServiceWebSocket<String,String>
{
  @Override
  public void next(String  value, WebSocket<String> ws)
    throws IOException
  {
    ws.write(value);
  }

  public static void main(String []argv)
  {
    Web.include(WebSocketEcho.class);
    Web.go(argv);
  }
}

ServiceWebSocket

public interface ServiceWebSocket<T,S>
{
  default void open(WebSocket<S> ws);

  void next(T value, WebSocket<S> ws);

  default void ping(String value, WebSocket<S> ws);
  default void pong(String value, WebSocket<S> ws);

  default void close(WebSocketCode code, String msg, WebSocket ws);
}

WebSocket

public interface WebSocket<S>
{
  String uri();
  String path();
  String pathInfo();

  void next(S data);
  Credits credits();

  void write(Buffer data);
  void writePart(Buffer data);

  void write(byte []buffer, int offset, int length);
  void writePart(byte []buffer, int offset, int length);

  OutputStream outputStream();

  void write(String data);
  void writePart(String data);

  Writer writer();

  void flush();

  void ping(String data);
  void pong(String data);

  boolean isClosed();
  void close(WebSocketClose code, String msg);
  void close();

  void fail(Throwable exn);
}

Filters

Filters can be applied as annotations to the request methods or class. The filters can either be defined explicitly or indirectly as annotations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Hello
{
  @Get
  @FilterBefore(MyHeaderFilter.class)
  void hello(Result<String> result)
  {
    result.ok("hello");
  }
}

public class MyHeaderFilter implements ServiceWeb
{
  public void handle(RequestWeb request)
  {
    request.header("My-Header", "hello");
    request.ok();
  }
}

The request.ok() in the header filter finishes the filter and moves on to the next service in the filter chain. In this case, that’s the hello() method.

Filter Annotations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Retention(RUNTIME)
@FilterBefore(MyHeaderFilter.class)
public @interface MyHeader {
}

@MyHeader
public class Hello
{
  @Get
  void hello(Result<String> result)
  {
    result.ok("hello");
  }
}

Here, the @MyHeader on Hello applies to all route methods.

Dynamic Filter Annotations

Filter annotations can work with injection to provide more dynamic capabilities. There’s a bit of extra work.

The following filter is created using the annotation on the class. It uses an application-defined “hello” value from the annotation.

When the service is initialized, Baratine will pass the MyHeader("hello") to a factory function which will return the dynamic filter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class MyHeaderFilter implements ServiceWeb
{
  private MyHeader _header;

  MyHeaderFilter(MyHeader header)
  {
    _header = header;
  }

  public void handle(RequestWeb request)
  {
    request.header("My-Header", _header.value());
    request.ok();
  }
}

@Retention(RUNTIME)
@FilterBefore(ServiceWeb.class)
public @interface MyHeader {
  String value() default "my-default";
}

@MyHeader("hello")
public class Hello
{
  @Get
  void hello(Result<String> result)
  {
    result.ok("hello");
  }

  @Bean
  Function<MyHeader,ServiceWeb> myFilter()
  {
    return MyHeaderFilter::new;
  }

  public static void main(String []argv)
  {
    Web.include(Hello.class);
    Web.go(argv);
  }
}

Services

See Baratine Service for more details on Baratine services.

By default all web requests run in a single Baratine service, but you can also create new services, each with its own thread and inbox queue.

Baratine services are a strict encapsulation of data and processing.

  • Only the service thread has access to service data.
  • Calls to the service use normal application-supplied interfaces and classes.
  • Outside clients must call through a proxy and inbox.
  • Method calls are ordered in an inbox queue.

A service has the following:

  • One or more service application classes.
  • One thread, dispatched from a thread pool.
  • An inbox queue to order messages from outside the service context.
  • One or more proxies with API methods for clients to call.

Only the application class and its interface are typically visible to the application. Programming is POJO based.

Web Service

A web service handles HTTP methods just like the default web handling. The only difference is that the service has its own thread and inbox, distinct from the default HTTP service.

Web services are created like other HTTP services with a Web.include. The only addition is a @Service.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Service
public class Hello
{
  @Get
  void hello(Result<String> result)
  {
    result.ok("hello");
  }

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

    Web.go(args);
  }
}

Web Vault Service

A web vault service handles URLs like /book/2712/hello, where a each book vault has many book assets. Each book asset gets its own class instance while sharing the parent vault’s service thread and inbox queue.

See Vault Services for more information.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public interface Books extends Vault<IdAsset,Book>
{
  void create(Book data, Result<IdAsset> result);
}

public class Book
{
  private IdAsset _id;

  @Get
  void hello(Result<String> result)
  {
    result.ok("book: " + _id);
  }
}

public class Main {
{
  public static void main(String []args)
  {
    Web.include(Books.class);

    Web.go(args);
  }
}
    1. @Service is required for Vault services.
    1. The Books is a manager for the Book instance, which uses a long identifier.
    1. IdAsset specifies the book identifier, which is injected when the book is created.
    1. @Get on a book instance matches the path “/book/{id}/hello”.
    1. Web.include registers the resource.

Note

Because all book instances share the same service thread and inbox queue, book methods should not block.

Single-thread Service

Services can be independent of the web methods. Often, web methods will call non-web services. Each service gets its own thread and inbox queue.

Services are registered with Web.service or with Web.include or a @Service producer method.

Depending on its purpose, an independent service might block. If it blocks, new requests will fill the inbox queue until it is ready to process more.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyProducer
{
  @Service
  public MyService myService()
  {
    return new MyServiceImpl();
  }
}

public class Main {
{
  @Inject @Service
  MyService _service;

  @Get
  void hello(Result<String> result)
  {
    _service.hello(result.of(value-> "hello: " + value));
  }

  public static void main(String []args)
  {
    Web.include(MyProducer.class);
    Web.include(Main.class);

    Web.go(args);
  }
}
    1. myService() is a producer to create the service. At initialization, the server will create and register MyService.
    1. MyService can be injected. The injected object is a proxy to the service; method calls are queued to the MyService inbox.
    1. A web method can call a service. result.of chains the service result to the web method result. The calls do not block.
    1. The service producer is registered with Web.include.

Multi-worker Service

When you need a multi-threaded, blocking service like a database, you can use a multi-worker service as a bridge. A multi-worker service has multiple threads, one per service instance. Each is allowed to block waiting for the response.

@Worker services are best used for multi-connection resources like databases or HTTP clients.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class MyProducer
{
  @Inject DataSource _jdbcDataSource;

  @Service
  @Workers
  @Api(MyDatabaseService.class)
  public MyDatabaseServiceImpl myService()
  {
    return new MyDatabaseServiceImpl(_jdbcDataSource);
  }
}

In the preceeding, MyDatabaseServiceImpl is a multi-threaded, blocking API. MyDatabaseService is the asynchronous equivalent, where all blocking calls are converted into Result calls.

Sessions

Baratine sessions are service instances. They are resource services that use a session id as the identifier. Because session instances are created for each session, the application can store session values directly in the session. Sessions are identified by the “session:” prefix.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Service("session:")
public class MySession
{
  @Id String _id; // session id

  @Inject Authenticator _authenticator;

  User _user;

  @Get
  public void hello(Result<String> result)
  {
    if (_user != null) {
      result.ok("hello, " + _user.name());
    }
    else {
      result.ok("hello, anonymous");
    }
  }

  @Post
  public void login(@Body("user") String user,
                    @Body("password") String password,
                    Result<String> result)
  {
    _authenticator.login(user, password,
                         result.of(this::onLogin));
  }

  private String onLogin(User user)
  {
    _user = user;
    ...
    return "ok";
  }
}

The preceeding example has two HTTP methods, /hello and /login. When the user it logged in, the /hello method returns the user name. Otherwise it returns the anonymous user.