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 : Quantity<Q>> roundtrip(sut: Quantity<Q>): ComparableQuantity<Q> {
    val unitFormatter = UCUMFormat.getInstance(UCUMFormat.Variant.CASE_SENSITIVE)
    val formattedUnit = unitFormatter.format(sut.unit)

    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)

    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<Nothing> {
  @Suppress("UNCHECKED_CAST")
  return this as Quantity<Nothing>
}

fun Unit<*>.toWildcard(): Unit<Nothing> {
  @Suppress("UNCHECKED_CAST")
  return this as Unit<Nothing>
}

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<Nothing> = 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)
  }
}

2 comments

  1. Pingback: JSR-363 Units of Measurement API in Practice – Blogpost Series Introduction – Meshcloud

  2. Pingback: JSR-363 Units of Measurement API in Practice – Binary Prefixes – Meshcloud

Leave a comment

Your email address will not be published. Required fields are marked *