Loading...

January 7, 2016

Ratpacked: Add Chains Via Registry

In a previous blog post we learned how to use HandlerDecorator.prepend to add common handlers via the registry in our application. The type of handlers suitable for this approach were handlers that had common functionality for the rest of the handlers. If we want to add a Chain implementation, containing handlers and maybe even path information, we cannot use the prepend method, must write our own implementation of the HandlerDecorator interface. This can be useful when we want to re-use a Chain in multiple applications. We write a module that adds the Chain implementation to the registry and we don't have to write any code in the handlers section for the Chain to work. This blog post is inspired by a conversation on the Ratpack Slack channel recently.

First we create a simple handler that renders a result:

// File: src/main/groovy/com/mrhaki/ratpack/Ping.groovy
package com.mrhaki.ratpack

import ratpack.groovy.handling.GroovyChainAction

/**
 * Implementation of a {@link ratpack.handling.Chain} interface
 * by extending {@link GroovyChainAction}, so
 * we can use Groovy DSL support in the
 * {@link Ping#execute} method.
 */
class Ping extends GroovyChainAction {
    
    @Override
    void execute() throws Exception {
        // What we normally would write
        // in the handlers{} section
        // of Ratpack.groovy.
        path('ping') {
            render 'Ratpack rules!'
        }
    }
}

First we register an instance of the Ping class using the multiBindInstance method in the bindings section of our Ratpack.groovy file:

import com.mrhaki.ratpack.Ping
import ratpack.handling.HandlerDecorator
import ratpack.handling.Handlers
import ratpack.registry.Registry

import static ratpack.groovy.Groovy.ratpack

ratpack {
    bindings {
        // We use multiBindInstance so multiple
        // implementations of HandlerDecorator
        // can be added to the registry.
        // The HandlerDecorator is implemented with
        // a closure. We add the Ping chain after 
        // other handlers (if set).
        multiBindInstance(
                HandlerDecorator, { Registry registry, Handler rest ->
                    Handlers.chain(rest, Handlers.chain(registry, new Ping()))
                } as HandlerDecorator)

    }
    
    handlers {
        get {
            render 'Ratpack is awesome!'
        } 
    }

}

If we start the application we get results when we invoke http://localhost:5050/ and http://localhost:5050/ping:

$ http -b localhost:5050/
Ratpack is awesome!

$ http -b localhost:5050/ping
Ratpack rules!

$

Instead of using the multiBindInstance method in the bindings section we can create a module and register our Ping class in the module:

// File: src/main/groovy/com/mrhaki/ratpack/PingModule.groovy
package com.mrhaki.ratpack

import com.google.inject.AbstractModule
import com.google.inject.multibindings.Multibinder
import ratpack.handling.Handler
import ratpack.handling.HandlerDecorator
import ratpack.handling.Handlers
import ratpack.registry.Registry

/**
 * Module to register the {@link Ping}
 * chain with the registry.
 */
class PingModule extends AbstractModule {
    
    @Override
    protected void configure() {
        // We need the Multibinder, because other modules
        // can also register HandlerDecorator implementations.
        Multibinder
                .newSetBinder(binder(), HandlerDecorator)
                .addBinding()
                .toInstance({ Registry registry, Handler rest ->
                    Handlers.chain(rest, Handlers.chain(registry, new Ping())
                } as HandlerDecorator)
    }
    
}

The Ratpack.groovy file now is as follows:

// File: src/ratpack/Ratpack.groovy
import com.mrhaki.ratpack.Ping
import com.mrhaki.ratpack.PingModule

import static ratpack.groovy.Groovy.ratpack

ratpack {
    bindings {
        module PingModule
    }
    
    handlers {
        get {
            render 'Ratpack is awesome!'
        } 
    }

}

To make the module more reusable we want to make the path binding for the Ping handler configurable. So users can include the module, but still can set the path binding for their application. Things get a bit more complicated now and we need some changes in our code. First we write a class with the configuration data for the module. We only have one property path that is used for the binding.

// File: src/main/groovy/com/mrhaki/ratpack/PingModuleConfig.groovy
package com.mrhaki.ratpack

class PingModuleConfig {
    String path
}

Our Ping class now accepts an instance of the configuration to get the path value:

// File: src/main/groovy/com/mrhaki/ratpack/Ping.groovy
package com.mrhaki.ratpack

import ratpack.groovy.handling.GroovyChainAction

import javax.inject.Inject

/**
 * Implementation of a {@link ratpack.handling.Chain} interface
 * by extending {@link GroovyChainAction}, so
 * we can use Groovy DSL support in the
 * {@link Ping#execute} method.
 */
class Ping extends GroovyChainAction {
    
    String pingPathBinding = 'ping'
    
    @Inject
    Ping(final PingModuleConfig config) {
        pingPathBinding = config.path
    }
    
    @Override
    void execute() throws Exception {
        path(pingPathBinding) {
            render 'Ratpack rules!'
        }
    }
}

Now we change the module and make it a ConfigurableModule. Notice we bind the Ping class so the dependency injection of our configuration works. Then we use a Provider to get the fully configured instance of the Ping class.

// File: src/main/groovy/com/mrhaki/ratpack/PingModule.groovy
package com.mrhaki.ratpack

import com.google.inject.Provider
import com.google.inject.multibindings.Multibinder
import ratpack.guice.ConfigurableModule
import ratpack.handling.Handler
import ratpack.handling.HandlerDecorator
import ratpack.handling.Handlers
import ratpack.registry.Registry

class PingModule extends ConfigurableModule<PingModuleConfig> {

    @Override
    protected void configure() {
        bind(Ping)

        final Provider<Ping> pingProvider = getProvider(Ping)

        Multibinder
                .newSetBinder(binder(), HandlerDecorator)
                .addBinding()
                .toProvider({ -> 
                    { Registry registry, Handler rest ->
                        Handlers.chain(rest, Handlers.chain(registry, pingProvider.get()))
                    } as HandlerDecorator
                } as Provider<HandlerDecorator>)
    }

}

Finally we change our Ratpack application and configure the path to be pong:

// File: src/ratpack/Ratpack.groovy
import com.mrhaki.ratpack.PingModule
import com.mrhaki.ratpack.PingModuleConfig
import ratpack.config.ConfigData

import static ratpack.groovy.Groovy.ratpack

ratpack {
    bindings {
        final ConfigData configData = ConfigData.of { builder ->
            builder.props(['ping.path': 'pong']).build()
        }
        moduleConfig(PingModule, configData.get('/ping', PingModuleConfig))
    }
    
    handlers {
        get {
            render 'Ratpack is awesome!'
        }
    }

}

We can make our requests and look at the results:

$ http -b localhost:5050/
Ratpack is awesome!

$ http -b localhost:5050/pong
Ratpack rules!

$ http -b localhost:5050/ping
Client error 404

$

@guspower mentioned on the Ratpack Slack channel that we can also implement the HandlerDecorator interface directly. This cleans up our module code. Here is the implementation:

// File: src/main/groovy/com/mrhaki/ratpack/PingHandlerDecorator.groovy
package com.mrhaki.ratpack

import com.google.inject.Provider
import ratpack.handling.Handler
import ratpack.handling.HandlerDecorator
import ratpack.handling.Handlers
import ratpack.registry.Registry

class PingHandlerDecorator implements HandlerDecorator {

    /**
     * Provider for Ping is automatically injected
     * via the constructor.
     */
    private final Provider<Ping> pingProvider

    @Inject
    PingHandlerDecorator(Provider<Ping> pingProvider) {
        this.pingProvider = pingProvider
    }

    @Override
    Handler decorate(Registry registry, Handler rest) throws Exception {
        // Here is the code we had in the module before.
        Handlers.chain rest, Handlers.chain(registry, pingProvider.get())
    }

}

We now change the module class, which is even simpler now:

// File: src/main/groovy/com/mrhaki/ratpack/PingModule.groovy
package com.mrhaki.ratpack

import com.google.inject.Provider
import com.google.inject.multibindings.Multibinder
import ratpack.guice.ConfigurableModule
import ratpack.handling.Handler
import ratpack.handling.HandlerDecorator
import ratpack.handling.Handlers
import ratpack.registry.Registry

class PingModule extends ConfigurableModule<PingModuleConfig> {

    @Override
    protected void configure() {
        bind(Ping)
        bind(PingHandlerDecorator)
    }

}

Written with Ratpack 1.1.1.