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:
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 {
@Override
def T parse(
final Context context,
final TypedData body,
final Parse 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:
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.