Interface Chain

  • All Known Subinterfaces:
    GroovyChain
    All Known Implementing Classes:
    GroovyChainAction

    public interface Chain
    A chain is a write only builder for composing handlers.

    A chain object can't be used to handle requests. It can be thought of as a Domain Specific Language (DSL), or API, for constructing a List<Handler>.

    To understand the concept of a all chain, it is important to understand that a Handler can do one of three things:

    1. Respond to the request (terminating processing);
    2. Insert handlers and delegate processing;
    3. Delegate to the next handler.

    Methods like Handlers.chain(ServerConfig, Action) take a function that acts on a Chain, and return a Handler. The returned handler effectively just performs an insert of the handlers added to the chain during the action..

    It is very common to use this API to declare the handlers for an application as part of startup via the RatpackServerSpec.handlers(Action) method.

    Registry

    Chains may be backed by a registry, depending on how the chain was constructed. The RatpackServerSpec.handlers(Action) method backs the chain instance with the server registry. The backing registry can be obtained via getRegistry() on the chain instance.

    This mechanism allows access to “supporting objects” while building the chain. Methods such as all(Class) also allow obtaining all implementations from the registry to use. This can be useful when using the Guice integration (or similar) to allow all instance to be dependency injected through Guice.

    Adding handlers

    The most basic method of Chain API is the all(Handler) method. The word “all” represents that all requests reaching this point in the chain will flow through the given handler. This is in contrast to methods such as path(String, Handler) that will only the request through the given handler if the request path matches.

    Methods such as path(String, Handler), when(Predicate, Action) etc. are merely more convenient forms of all(Handler) and use of the static methods of Handlers.

    For each method that takes a literal Handler, there exists a variant that takes a Class<? extends Handler>. Such methods obtain an instance of the given handler by asking the chain registry for an instance of the given type. This is generally most useful if the chain registry is backed by some kind of dependency injection mechanism (like Google Guice) that can construct the handler and inject its dependencies as needed.

    Path Binding

    Methods such as get(String, Handler), prefix(String, Action), accept a string argument as a request path binding specification. These strings can contain symbols that allow PathTokens to be captured and for path binding to be dynamic. For example, the path string "foo/:val" will match paths such as "foo/bar", "foo/123" or indeed "foo/«anything»".

    The following table describes the types of symbols that can be used in path strings…

    Path binding symbols
    Path Type Syntax Example
    Literal foo "foo"
    Regular Expression Literal ::«regex» "foo/::\d+"
    Optional Path Token :«token-name»? "foo/:val?"
    Mandatory Path Token :«token-name» "foo/:val"
    Optional Regular Expression Path Token :«token-name»?:«regex» "foo/:val?:\d+"
    Mandatory Regular Expression Path Token :«token-name»:«regex» "foo/:val:\d+"

    The following example shows different kinds of binding paths in action.

    
     import ratpack.test.embed.EmbeddedApp;
     import com.google.common.base.MoreObjects;
     import com.google.common.io.BaseEncoding;
     import java.util.Arrays;
     import java.util.Locale;
     import static org.junit.jupiter.api.Assertions.*;
    
     public class Example {
       public static void main(String... args) throws Exception {
         EmbeddedApp.fromHandlers(c -> c
           .get("favorites/food", ctx -> ctx.render("pizza")) // Literal
           .get("favorites/::colou?r", ctx -> ctx.render("blue")) // Regular expression literal
           .get("optionalToken/:tkn?", ctx -> ctx.render(ctx.getPathTokens().toString())) // Optional path token
           .get("greeting/:name?", ctx -> // Optional path token with default handling
             ctx.render("Hello " + MoreObjects.firstNonNull(ctx.getPathTokens().get("name"), "world"))
           )
           .get("convert/hex/:tkn", ctx -> // Mandatory path token
             ctx.render("Hello " + BaseEncoding.base64().encode(ctx.getPathTokens().get("tkn").getBytes("UTF-8")))
           )
           .get("pi/:precision?:[\\d]+", ctx -> // Optional regular expression path token
             ctx.render(String.format(Locale.ENGLISH, "%1." + MoreObjects.firstNonNull(ctx.getPathTokens().get("precision"), "5") + "f", Math.PI))
           )
           .get("sum/:num1:[\\d]+/:num2:[\\d]+", ctx -> // Mandatory regular expression path tokens
             ctx.render(
               Arrays.asList("num1", "num2")
               .stream()
               .map(it -> ctx.getPathTokens().get(it))
               .mapToInt(Integer::valueOf)
               .sum() + ""
             )
           )
         ).test(httpClient -> {
           assertEquals("pizza", httpClient.getText("favorites/food")); // Literal value matched
           assertEquals("blue", httpClient.getText("favorites/color")); // Regular expression literal matched
           assertEquals("blue", httpClient.getText("favorites/colour")); // Regular expression literal matched
           assertEquals("{tkn=val}", httpClient.getText("optionalToken/val")); // Optional path token with value specified
           assertEquals("{tkn=}", httpClient.getText("optionalToken/")); // Optional path token with trailing slash treated as empty string
           assertEquals("{}", httpClient.getText("optionalToken")); // Optional path token without trailing slash treated as missing
           assertEquals("Hello Ratpack", httpClient.getText("greeting/Ratpack")); // Optional path token with value specified
           assertEquals("Hello world", httpClient.getText("greeting")); // Optional path token with default handling
           assertEquals("Hello UmF0cGFjaw==", httpClient.getText("convert/hex/Ratpack")); // Mandatory path token
           assertEquals("3.14159", httpClient.getText("pi")); // Optional regular expression path token with default handling
           assertEquals("3.14", httpClient.getText("pi/2")); // Optional regular expression path token with value specified
           assertEquals("3.1415927", httpClient.getText("pi/7")); // Optional regular expression path token with value specified
           assertEquals("42", httpClient.getText("sum/13/29")); // Mandatory regular expression path tokens
         });
       }
     }
     

    HTTP Method binding

    Methods such as get(Handler), post(Handler) etc. bind based on the HTTP method of the request. They are effectively a combination of the use of path(String, Handler) and the Context.byMethod(Action) construct to declare that the given path ONLY responds to the specified method.

    The following two code snippets are identical:

    
     import ratpack.test.embed.EmbeddedApp;
     import static org.junit.jupiter.api.Assertions.*;
    
     public class Example {
       public static void main(String... args) throws Exception {
         EmbeddedApp.fromHandlers(c -> c
           .path("foo", ctx ->
             ctx.byMethod(m -> m
               .get(() -> ctx.render("ok"))
             )
           )
         ).test(httpClient -> {
           assertEquals("ok", httpClient.getText("foo"));
           assertEquals(405, httpClient.post("foo").getStatusCode());
         });
       }
     }
     
    
     import ratpack.test.embed.EmbeddedApp;
     import static org.junit.jupiter.api.Assertions.*;
    
     public class Example {
       public static void main(String... args) throws Exception {
         EmbeddedApp.fromHandlers(c -> c
           .get("foo", ctx -> ctx.render("ok"))
         ).test(httpClient -> {
           assertEquals("ok", httpClient.getText("foo"));
           assertEquals(405, httpClient.post("foo").getStatusCode());
         });
       }
     }
     

    That is, methods such as get(String, Handler), get(Handler) etc. terminate processing with a 405 (method not supported) client error if the request path matches but the HTTP method does not. They should not be used for URLs that respond differently depending on the method. The correct way to do this is to use path(String, Handler) and Context.byMethod(Action).

    
     import ratpack.test.embed.EmbeddedApp;
     import static org.junit.jupiter.api.Assertions.*;
    
     public class Example {
       public static void main(String... args) throws Exception {
         EmbeddedApp.fromHandlers(c -> c
           .path("foo", ctx ->
             ctx.byMethod(m -> m
               .get(() -> ctx.render("GET"))
               .post(() -> ctx.render("POST"))
             )
           )
         ).test(httpClient -> {
           assertEquals("GET", httpClient.getText("foo"));
           assertEquals("POST", httpClient.postText("foo"));
           assertEquals(405, httpClient.delete("foo").getStatusCode());
         });
       }
     }
     

    Given the following, a POST to /foo will yield a 405 response.

    
     import ratpack.test.embed.EmbeddedApp;
     import static org.junit.jupiter.api.Assertions.assertEquals;
    
     public class Example {
       public static void main(String... args) throws Exception {
         EmbeddedApp.fromHandlers(c -> c
           .get("foo", ctx -> ctx.render("GET"))
           .post("foo", ctx -> ctx.render("POST"))
         ).test(httpClient -> {
           assertEquals("GET", httpClient.getText("foo"));
    
           // NOTE: returns 405, not 200 and "POST"
           assertEquals(405, httpClient.post("foo").getStatusCode());
         });
       }
     }
     

    All methods that match HTTP methods, are synonyms for path(String, Class) in terms of path binding. That is, get(Handler) behaves the same way with regard to path binding as path(Handler), and not all(Handler).

    • Method Detail

      • files

        default Chain files​(Action<? super FileHandlerSpec> config)
                     throws Exception
        Adds a handler that serves files from the file system.

        The given action configures how and what files will be served. The handler binds to a request path and a directory within the current filesystem binding. The portion of the request path past the path binding identifies the target file within the directory.

        
         import ratpack.test.embed.EphemeralBaseDir;
         import ratpack.test.embed.EmbeddedApp;
        
         import static org.junit.jupiter.api.Assertions.assertEquals;
        
         public class Example {
           public static void main(String... args) throws Exception {
             EphemeralBaseDir.tmpDir().use(baseDir -> {
               baseDir.write("public/some.text", "foo");
               baseDir.write("public/index.html", "bar");
               EmbeddedApp.of(s -> s
                 .serverConfig(c -> c.baseDir(baseDir.getRoot()))
                 .handlers(c -> c
                   .files(f -> f.dir("public").indexFiles("index.html"))
                 )
               ).test(httpClient -> {
                 assertEquals("foo", httpClient.getText("some.text"));
                 assertEquals("bar", httpClient.getText());
                 assertEquals(404, httpClient.get("no-file-here").getStatusCode());
               });
             });
           }
         }
         
        Parameters:
        config - the file handler configuration
        Returns:
        this
        Throws:
        Exception - any thrown by config
        See Also:
        Handlers.files(ServerConfig, Action), FileHandlerSpec
      • chain

        default Handler chain​(Action<? super Chain> action)
                       throws Exception
        Constructs a handler using the given action to define a chain.
        Parameters:
        action - The action that defines the all chain
        Returns:
        A all representing the chain
        Throws:
        Exception - any thrown by action
      • fileSystem

        default Chain fileSystem​(String path,
                                 Action<? super Chain> action)
                          throws Exception
        Adds a handler to this chain that changes the FileSystemBinding for the given handler chain.
        Parameters:
        path - the relative path to the new file system binding point
        action - the definition of the all chain
        Returns:
        this
        Throws:
        Exception - any thrown by action
      • getServerConfig

        ServerConfig getServerConfig()
        The server config of the application that this chain is being created for.
        Returns:
        The server config of the application that this chain is being created for.
      • all

        Chain all​(Handler handler)
        Adds the given handler to this.
        Parameters:
        handler - the handler to add
        Returns:
        this
      • path

        default Chain path​(String path,
                           Handler handler)
        Adds a handler that delegates to the given handler if the relative path matches the given path exactly.

        Nesting path handlers will not work due to the exact matching, use a combination of path and prefix instead. See prefix(String, Action) for details.

           // this will not work
           path("person/:id") {
             path("child/:childId") {
               // a request of /person/2/child/1 will not get passed the first all as it will try
               // to match "person/2/child/1" with "person/2" which does not match
             }
        
           // this will work
           prefix("person/:id") {
             path("child/:childId") {
               // a request of /person/2/child/1 will work this time
             }
           }
         

        See Handlers.path(String, Handler) for the details on how path is interpreted.

        Parameters:
        path - the relative path to match exactly on
        handler - the handler to delegate to
        Returns:
        this
        See Also:
        post(String, Handler), get(String, Handler), put(String, Handler), patch(String, Handler), delete(String, Handler)
      • host

        default Chain host​(String hostName,
                           Action<? super Chain> action)
                    throws Exception
        Adds a handler to the chain that delegates to the given handler chain if the request has a Host header that matches the given value exactly.
        
          chain.
            host("foo.com", new Action<Chain>() {
              public void execute(Chain hostChain) {
                hostChain.all(new Handler() {
                  public void handle(Context context) {
                    context.getResponse().send("Host Handler");
                  }
                });
              }
            });
         
        Parameters:
        hostName - the name of the HTTP Header to match on
        action - the handler chain to delegate to if the host matches
        Returns:
        this
        Throws:
        Exception - any thrown by action
      • insert

        default Chain insert​(Action<? super Chain> action)
                      throws Exception
        Inserts the given nested handler chain.

        Shorter form of all(Handler) handler}(chain(action).

        Parameters:
        action - the handler chain to insert
        Returns:
        this
        Throws:
        Exception - any thrown by action
      • options

        default Chain options​(String path,
                              Class<? extends Handler> handler)
        Parameters:
        path - the path to bind to
        handler - a handler
        Returns:
        this
        Since:
        1.1
      • options

        default Chain options​(Handler handler)
        Adds a handler that delegates to the given handler if the request HTTPMethod is OPTIONS and the path is at the current root.
        Parameters:
        handler - the handler to delegate to
        Returns:
        this
        Since:
        1.1
        See Also:
        get(Handler), post(Handler), put(Handler), delete(Handler)
      • options

        default Chain options​(Class<? extends Handler> handler)
        Parameters:
        handler - a handler
        Returns:
        {code this}
        Since:
        1.1
      • prefix

        default Chain prefix​(String prefix,
                             Action<? super Chain> action)
                      throws Exception
        Adds a handler that delegates to the given handlers if the relative path starts with the given prefix.

        All path based handlers become relative to the given prefix.

        
           chain
             .prefix("person/:id", new Action<Chain>() {
               public void execute(Chain personChain) throws Exception {
                 personChain
                   .get("info", new Handler() {
                     public void handle(Context context) {
                       // e.g. /person/2/info
                     }
                   })
                   .post("save", new Handler() {
                     public void handle(Context context) {
                       // e.g. /person/2/save
                     }
                   })
                   .prefix("child/:childId", new Action<Chain>() {
                     public void execute(Chain childChain) {
                       childChain
                         .get("info", new Handler() {
                           public void handle(Context context) {
                             // e.g. /person/2/child/1/info
                           }
                         });
                     }
                   });
               }
             });
         

        See Handlers.prefix(String, Handler) for format details on the prefix string.

        Parameters:
        prefix - the relative path to match on
        action - the handler chain to delegate to if the prefix matches
        Returns:
        this
        Throws:
        Exception - any thrown by action
      • register

        default Chain register​(Registry registry)
        Makes the contents of the given registry available for downstream handlers of the same nesting level.

        The registry is inserted via the Context.next(Registry) method.

        Parameters:
        registry - the registry whose contents should be made available to downstream handlers
        Returns:
        this
      • when

        default Chain when​(boolean test,
                           Action<? super Chain> action)
                    throws Exception
        Inlines the given chain if test is true.

        This is literally just sugar for wrapping the given action in an if statement. It can be useful when conditionally adding handlers based on state available when building the chain.

        
         import ratpack.test.embed.EmbeddedApp;
        
         import static org.junit.jupiter.api.Assertions.assertEquals;
        
         public class Example {
        
           public static void main(String... args) throws Exception {
             EmbeddedApp.of(a -> a
               .registryOf(r -> r.add(1))
               .handlers(c -> c
                 .when(c.getRegistry().get(Integer.class) == 0, i -> i
                   .get(ctx -> ctx.render("ok"))
                 )
               )
             ).test(httpClient ->
               assertEquals(httpClient.get().getStatusCode(), 404)
             );
        
             EmbeddedApp.of(a -> a
               .registryOf(r -> r.add(0))
               .handlers(c -> c
                 .when(c.getRegistry().get(Integer.class) == 0, i -> i
                   .get(ctx -> ctx.render("ok"))
                 )
               )
             ).test(httpClient ->
               assertEquals(httpClient.getText(), "ok")
             );
           }
         }
         
        Parameters:
        test - whether to include the given chain action
        action - the chain action to maybe include
        Returns:
        this
        Throws:
        Exception - any thrown by action
        Since:
        1.4
      • when

        default Chain when​(boolean test,
                           Class<? extends Action<? super Chain>> action)
                    throws Exception
        Inlines the given chain if test is true.

        Similar to when(boolean, Action), except obtains the action instance from the registry by the given type.

        Parameters:
        test - whether to include the given chain action
        action - the chain action to maybe include
        Returns:
        this
        Throws:
        Exception - any thrown by action
        Since:
        1.4
      • when

        default Chain when​(boolean test,
                           Action<? super Chain> onTrue,
                           Action<? super Chain> onFalse)
                    throws Exception
        Inlines the appropriate chain based on the given test.

        This is literally just sugar for wrapping the given action in an if/else statement. It can be useful when conditionally adding handlers based on state available when building the chain.

        
         import ratpack.test.embed.EmbeddedApp;
        
         import static org.junit.jupiter.api.Assertions.assertEquals;
        
         public class Example {
        
           public static void main(String... args) throws Exception {
             EmbeddedApp.of(a -> a
               .registryOf(r -> r.add(1))
               .handlers(c -> c
                 .when(c.getRegistry().get(Integer.class) == 0,
                    i -> i.get(ctx -> ctx.render("ok")),
                    i -> i.get(ctx -> ctx.render("ko"))
                 )
               )
             ).test(httpClient ->
               assertEquals(httpClient.getText(), "ko")
             );
        
             EmbeddedApp.of(a -> a
               .registryOf(r -> r.add(0))
               .handlers(c -> c
                 .when(c.getRegistry().get(Integer.class) == 0,
                    i -> i.get(ctx -> ctx.render("ok")),
                    i -> i.get(ctx -> ctx.render("ko"))
                 )
               )
             ).test(httpClient ->
               assertEquals(httpClient.getText(), "ok")
             );
           }
         }
         
        Parameters:
        test - predicate to decide which action include
        onTrue - the chain action to include when the predicate is true
        onFalse - the chain action to include when the predicate is false
        Returns:
        this
        Throws:
        Exception - any thrown by action
        Since:
        1.5
      • when

        default Chain when​(boolean test,
                           Class<? extends Action<? super Chain>> onTrue,
                           Class<? extends Action<? super Chain>> onFalse)
                    throws Exception
        Inlines the appropriate chain based on the given test.

        Similar to when(boolean, Action, Action), except obtains the action instance from the registry by the given type.

        Parameters:
        test - predicate to decide which action to include
        onTrue - the chain action to include when the predicate is true
        onFalse - the chain action to include when the predicate is false
        Returns:
        this
        Throws:
        Exception - any thrown by action
        Since:
        1.5
      • onlyIf

        default Chain onlyIf​(Predicate<? super Context> test,
                             Handler handler)
        Invokes the given handler only if the predicate passes.

        This method differs from when() in that it does not insert the handler; but directly calls its Handler.handle(Context) method.

        Parameters:
        test - the predicate
        handler - the handler
        Returns:
        this
      • notFound

        default Chain notFound()
        Raises a 404 Context.clientError(int).

        This can be used to effectively terminate processing early. This is sometimes useful when using a scoped client error handler.

        
         import ratpack.core.error.ClientErrorHandler;
         import ratpack.test.embed.EmbeddedApp;
        
         import static org.junit.jupiter.api.Assertions.assertEquals;
        
         public class Example {
           public static void main(String... args) throws Exception {
             EmbeddedApp.of(s -> s
                 .registryOf(r -> r
                     .add(ClientErrorHandler.class, (ctx, code) -> ctx.render("global"))
                 )
                 .handlers(c -> c
                     .prefix("api", api -> api
                         .register(r -> r.add(ClientErrorHandler.class, (ctx, code) -> ctx.render("scoped")))
                         .get("foo", ctx -> ctx.render("foo"))
                         .notFound()
                     )
                 )
             ).test(http -> {
               assertEquals(http.getText("not-there"), "global");
               assertEquals(http.getText("api/foo"), "foo");
               assertEquals(http.getText("api/not-there"), "scoped");
             });
           }
         }
         
        Returns:
        this
        Since:
        1.1