Loading...

March 7, 2017

Ratpacked: Implement A Custom Request Parser

Ratpack has parsers to parse a request with a JSON body or a HTML form. We simply use the parse method of Context and Ratpack will check if there is a compliant parser in the registry. If there is a parser for that type available then Ratpack will parse the request and return a Promise with the value. To write a new parser we need to implement the Parser interface. The easiest way to implement this interface is by writing a class that extends ParserSupport. Using the ParserSupport class we can also work with an options object that a user can pass on to the parse method of Context. If we don't need options we can also extend the NoOptParserSupport class.

Let's write a custom parser that can parse a request with a hex or base64 encoded value. The parser returns a String object with the decoded value. In our example we also want the user to provide an optional options object of type StringParserOpts which denotes the type of decoding:

// File: src/main/groovy/mrhaki/sample/StringParser.groovy
package mrhaki.ratpack

import ratpack.handling.Context
import ratpack.http.TypedData
import ratpack.parse.Parse
import ratpack.parse.ParserSupport
import ratpack.util.Types

/**
 * Parser to decode hex or base64 values send 
 * in the body of a request. 
 */
class StringParser extends ParserSupport<StringParserOpts> {
    
    @Override
    def <T> T parse(
            final Context context,
            final TypedData body,
            final Parse<T, StringParserOpts> parse) throws Exception {

        // Check if type to be parsed can be handled by
        // this parser. We can also create a check based 
        // on content type of the body for example.
        if (supportsType(parse.type)) {
            // Get request body that is either hex or 
            // base64 encoded.
            final String bodyText = body.text

            // Get optional options. If the options are not set
            // a default instance is given. 
            final StringParserOpts opts = parse.opts.orElse(StringParserOpts.hex())
            
            // Check the options to see if hex or base64 decoding is needed.
            if (opts.hex) {
                return Types.cast(new String(bodyText.decodeHex()))
            } else if (opts.base64) {
                return Types.cast(new String(bodyText.decodeBase64()))    
            }
        }

        // Cannot handle the type to be parsed. 
        // Ratpack will try to find another match.
        return null
    }

    /**
     * Support String parsing.
     * 
     * @param typeToken Type defined to be parsed.
     * @return True if type is String, false if not.
     */
    private boolean supportsType(final typeToken) {
        typeToken.rawType == String
    }

}

/**
 * Class with options used to decode a value. 
 * A user can provide an instance of this class using the 
 * {@link Context#parse(java.lang.Class, java.lang.Object)} method.
 */
class StringParserOpts {
    
    private static enum Decoders { HEX, BASE64 }
    
    private Decoders decoder
    
    private StringParserOpts(final Decoders decoder) {
        this.decoder = decoder
    }

    static StringParserOpts hex() {
        new StringParserOpts(Decoders.HEX)
    }
    
    boolean isHex() {
        decoder == Decoders.HEX
    }

    static StringParserOpts base64() {
        new StringParserOpts(Decoders.BASE64)
    }
    
    boolean isBase64() {
        decoder == Decoders.BASE64
    }
    
}

We have the implementation of our parser, so now we write a specification to test it. We test the parser with a simple handler implementation that uses the parse method and then simply renders the resulting String value. In our specification we use RequestFixture to invoke the handler and inspect the result:

// File: src/test/groovy/mrhaki/ratpack/StringParserSpec.groovy
package mrhaki.ratpack

import ratpack.handling.Handler
import ratpack.test.handling.HandlingResult
import ratpack.test.handling.RequestFixture
import spock.lang.Specification

class StringParserSpec extends Specification {

    void 'parse value in request body with StringParser using default decoder'() {
        given:
        final String content = 'Ratpack is gr8!'.bytes.encodeHex().toString()
        
        and:
        final Handler handler = { context ->
            context.parse(String)
                   .then(context.&render)
        }

        when:
        final HandlingResult result = RequestFixture.handle(handler) { fixture ->
            fixture.body(content, 'text/plain')
                    // Add StringParser to registry, so it can be used by Ratpack.
                   .registry { registry -> registry.add(new StringParser()) }
        }

        then:
        result.rendered(String) == 'Ratpack is gr8!'
    }

    void 'parse hex value in request body with StringParser'() {
        given:
        final String content = 'Ratpack is gr8!'.bytes.encodeHex().toString()

        and:
        final Handler handler = { context ->
            // Parse and set options for hex decoding.
            context.parse(String, StringParserOpts.hex())
                   .then(context.&render)
        }

        when:
        final HandlingResult result = RequestFixture.handle(handler) { fixture ->
            fixture.body(content, 'text/plain')
                    // Add StringParser to registry, so it can be used by Ratpack.
                   .registry { registry -> registry.add(new StringParser()) }
        }

        then:
        result.rendered(String) == 'Ratpack is gr8!'
    }
    
    void 'parse base64 value in request body with StringParser'() {
        given:
        final String content = 'Ratpack is gr8!'.bytes.encodeBase64().toString()

        and:
        final Handler handler = { context ->
            // Parse and set options for base64 decoding.
            context.parse(String, StringParserOpts.base64())
                   .then(context.&render)
        }

        when:
        final HandlingResult result = RequestFixture.handle(handler) { fixture ->
            fixture.body(content, 'text/plain')
                    // Add StringParser to registry, so it can be used by Ratpack.
                   .registry { registry -> registry.add(new StringParser()) }
        }

        then:
        result.rendered(String) == 'Ratpack is gr8!'
    }
    
}

Written with Ratpack 1.4.5.