In my last blog post - many months ago - I discussed the possibility of automatically creating a model object from a web request, in much the same way as SpringMVC and JSF do. I wasn't sure if it was possible, but over the last month I've created a library which does just that. Caisson is a library, written in Kotlin, which uses reflection to build model objects from spark-kotlin web requests. It introduces an extension method on the spark request object called bind(), which parses the request.queryMap an constructs a model object. It's a little easier to explain this with a short demonstration:

Given a web form with the following fields:

<form id="personForm">
 <div class="input-field col s6">
  <input placeholder="Name" id="person_name" type="text" class="validate" name="name"/>
  <label for="person_name">Your name</label>
  <input placeholder="38" id="person_age" type="number" class="validate" name="age"/>
  <label for="person_age">Your age</label>
  <div id="personSubmit" class="btn waves-effect waves-light" th:onclick="'submit(\'personForm\',\'results\');'">Submit
   <i class="material-icons right">send</i>
  </div>
 </div>
</form>

And a corresponding kotlin data class:

data class Person(val name: String, val age: Int)

Then we can use Caisson to construct an instance of the Person class straight from the spark request:

post("personForm") {
   val person = request.bind<Person>()
   println("person is ${person?.name}, born on ${person?.date}")
   model.put("resultingString", person?.toString())
   render(resultView)
}

In the current version of Caisson, the HTML input names must match the kotlin class constructor parameter names. I'm thinking about ways of easing that restriction in the future. Caisson only works with primary constructors, so a kotlin data class is an obvious choice for a binding model. But so long as your class has a primary constructor matching the form input names, Caisson will do its best to create a matching object through reflection.

This works pretty seamlessly for all the basic Kotlin types - String, Int, Long, Float, Double and Boolean. It does not currently work with Enum.

If the web form is missing one of the expected values, Caisson will still create a bound object using the default value for the basic type. You can always specify a default value in your kotlin constructor:

data class Person(val name: String, val age: Int = 18)

Caisson can work with more complicated data types as well, if you provide a custom converter class which can turn the simple request string into the correct type. For instance, a date string can be turned into a LocalDate class with a custom converter class:

data class Person(val name: String, @CConverter(LocalDateConverter::class) val dob: LocalDate)

And the converter class (which must implement the Converter interface from Caisson):

class LocalDateConverter : Converter {
	override fun convert(from: String): LocalDate? {
		val dateTimeFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
		try {
			return LocalDate.parse(from,dateTimeFormat)
		} catch(e: java.time.format.DateTimeParseException) {
			return LocalDate.now()
		}
	}
}

Motivation

I've written Caisson because I didn't really like all the request.queryParams wrangling needed in spark-kotlin. I've also used it as a test-bed to learn more about reflection in Kotlin. There are no hooks into the rendering engine or templating system. No additional hidden values are required in your web form, and form names are not mangled in any way (I'm looking at you, JSF!). I'm pretty happy with the way it has turned out.

Caisson can also handle file uploads. Your kotlin class must contain a parameter of type CaissonMultipartContent - or even a List of these if you are uploading multiple files in one request.

There's a companion project in github called spark-caisson-integration spark-caisson-integration to demonstrate and test all these features. Please excuse the ugliness; I haven't added any working CSS.

Future Plans

When I started Caisson, I was thinking about trying to bring the functionality from SpringMVC into spark-kotlin, especially around validation of form elements. But now that Caisson is working, I've backed off a little from that. spark-kotlin is deliberately lightweight and overloading Caisson with too many extra features would complicate things unnecessarily. Nonetheless, I'm still thinking about a validation framework based around Caisson (perhaps as a separate module?). Caisson also contains an AbstractController class which I'm using in spark-caisson-integration which adds additional logging, debugging and flash-scope mechanisms. I think that's the wrong approach, so I'm going to unpick it and narrow the focus. Kotlin's extension functions can help with this - I can imagine implementing a flash session scope with a simple request.flash("object","value") extension method.