meshBlog
JSR-363 Units of Measurement API in Practice – Persisting Quantities with Spring Data
In this post we will look at how to persist Quantity<q>
types offered by the Java Units of Measurement API (JSR-363) using Spring data. We will also use the very handy Kotlin bindings from the Physikal library for the uom-se implementation of JSR-363.
In JSR-363, Quantities are complex objects and contain a circle in their object graph. Hence, you will get a lovely StackoverflowException
when you naively try to serialize or persist them. Spring offers the Converter interface which we can use to teach Spring Data to serialize a Quantity
object by first converting it to a different representation.
Formatting Quantities
Starting with this knowledge, let’s try formatting a Quantity
object to a String
using the QuantityFormat
class in the uom-se library. Let’s first add two tests to see how that works:
private fun roundtrip(sut: Quantity<*>): Quantity<*> {
val serialized = sut.toString()
return QuantityFormat.getInstance().parse(serialized)
}
@Test
fun serializingKilometers() {
val sut = 10.kilo.metre
Assert.assertEquals(sut, roundtrip(sut))
}
@Test
fun serializingGigaBytes() {
val sut = 10.giga.byte
Assert.assertEquals(sut, roundtrip(sut))
}
While the first test passes fine, unfortunately the second test fails. It turns out that uom-se represents bytes
as a derived unit that is actually composed of 8 bits (the primitive unit). So for example, 10 GB
are represented as 10 G(bit*8.0)
. Apparently, the default formatter/parser provided by QuantityFormat
is not able to handle this. Digging in the Physikal source, we find that the byte
extension function we used above uses the BIT
and BYTE
unit constants from its implementation of the UCUM system of units in the uom-systems library.
So next, let’s try to use the included UCUMFormatter
. Turns out the following roundtrip implementation works (although it requires some ugly unchecked casts):
fun <q>> roundtrip(sut: Quantity<q>): ComparableQuantity<q> {
val unitFormatter = UCUMFormat.getInstance(UCUMFormat.Variant.CASE_SENSITIVE)
val formattedUnit = unitFormatter.format(sut.unit)</q></q></q>
val numberFormatter = NumberFormat.getInstance(Locale.ROOT)
val formattedValue = numberFormatter.format(sut.value)
@Suppress("UNCHECKED_CAST")
val parsedUnit: Unit<q> = unitFormatter.parse(formattedUnit) as Unit<q>
val parsedValue = numberFormatter.parse(formattedValue)</q></q>
return Quantities.getQuantity(parsedValue, parsedUnit)
}
Wildcard generics in Kotlin
At this point you may note that the JSR API is written with Java’s generic model in mind and relies on wildcard generics/unchecked casts in some places. This creates some friction when using it from Kotlin, which has a more rigid and also more expressive generic type system than Java. So let’s also add some quick extension methods to do these ugly unchecked casts for us:
fun Quantity<*>.toWildcard(): Quantity {
@Suppress("UNCHECKED_CAST")
return this as Quantity
}
fun Unit<*>.toWildcard(): Unit {
@Suppress("UNCHECKED_CAST")
return this as Unit
}
A Spring Converter for Quantity
With a little more work, we can plug this implementation into two Spring Converters.
object QuantityFormatting {
private val unitFormatter = UCUMFormat.getInstance(UCUMFormat.Variant.CASE_SENSITIVE)
private val numberFormatter = NumberFormat.getInstance(Locale.ROOT)
private val separator = " "
object QuantityToStringConverter : Converter<Quantity<*>, String> {
override fun convert(source: Quantity<*>): String {
val s = source.toWildcard()
val formattedUnit = unitFormatter.format(s.unit)
val formattedValue = numberFormatter.format(s.value)
return "$formattedValue$separator$formattedUnit"
}
}
object StringToQuantityConverter : Converter<String, Quantity<*>> {
override fun convert(source: String): Quantity<*> {
val (formattedValue, formattedUnit) = source.split(separator, limit = 2)
val parsedUnit: Unit = unitFormatter.parse(formattedUnit).toWildcard()
val parsedValue = numberFormatter.parse(formattedValue)
return Quantities.getQuantity(parsedValue, parsedUnit)
}
}
}
These converters can in turn plug into Spring Data. For our example, we’re going to use MongoDB and plug them into AbstractMongoConfiguration
as inspired by this Stackoverflow answer.
@Configuration
open class QuantitiesFormattingMongoDbConfiguration {
@Bean
open fun mongoCustomConversions(): CustomConversions {
val converters = listOf(
QuantityFormatting.QuantityToStringConverter,
QuantityFormatting.StringToQuantityConverter
)
return CustomConversions(converters)
}
}