Skip to content

Instantly share code, notes, and snippets.

@sgrimm
Last active December 20, 2019 15:25
Show Gist options
  • Save sgrimm/aed2d7707826f42869969154dc96745b to your computer and use it in GitHub Desktop.
Save sgrimm/aed2d7707826f42869969154dc96745b to your computer and use it in GitHub Desktop.
Compact-format XStream converters for Axon 4 tracking tokens
package com.thesegovia.payment.serial
import com.thoughtworks.xstream.converters.Converter
import com.thoughtworks.xstream.converters.MarshallingContext
import com.thoughtworks.xstream.converters.UnmarshallingContext
import com.thoughtworks.xstream.io.HierarchicalStreamReader
import com.thoughtworks.xstream.io.HierarchicalStreamWriter
import org.axonframework.eventhandling.GapAwareTrackingToken
/**
* Converts GapAwareTrackingToken to and from XML. The representation is designed to be compact
* while still remaining more or less human-readable: `<gatt i="123456" g="123450,123451,123452"/>`.
*
* To support migrating a clustered application to the new format, this can run in a bidirectionally
* compatible mode where it writes both the old and new formats. Upgrade the whole cluster to this
* converter, then turn off backward compatibility (by not passing a fallback converter) to get the
* compact form.
*
* TODO: Detect contiguous ranges of gaps and write them in a more compact form.
*/
class GapAwareTrackingTokenConverter(
/** Converter to use to write tokens in backward-compatible form. */
private val backwardCompatibleConverter: Converter? = null
) : Converter {
override fun unmarshal(reader: HierarchicalStreamReader, context: UnmarshallingContext): Any {
val index = reader.getAttribute("i")
if (index != null) {
val gaps = reader.getAttribute("g")?.split(',')?.map { it.toLong() }
return GapAwareTrackingToken.newInstance(
index.toLong(),
gaps ?: emptyList()
)
}
// Unmarshal old-style format
return backwardCompatibleConverter?.unmarshal(reader, context)
?: throw RuntimeException("No index attribute found")
}
override fun marshal(
original: Any,
writer: HierarchicalStreamWriter,
context: MarshallingContext
) {
if (original !is GapAwareTrackingToken) {
throw RuntimeException("Unable to marshal object of type ${original.javaClass.name}")
}
writer.addAttribute("i", original.index.toString())
if (original.gaps.isNotEmpty()) {
writer.addAttribute("g", original.gaps.joinToString(",") { "$it" })
}
backwardCompatibleConverter?.marshal(original, writer, context)
}
override fun canConvert(type: Class<*>): Boolean {
return type.name == GapAwareTrackingToken::class.java.name
}
}
package com.thesegovia.payment.serial
import com.thoughtworks.xstream.converters.Converter
import com.thoughtworks.xstream.converters.MarshallingContext
import com.thoughtworks.xstream.converters.UnmarshallingContext
import com.thoughtworks.xstream.io.HierarchicalStreamReader
import com.thoughtworks.xstream.io.HierarchicalStreamWriter
import org.axonframework.eventhandling.GapAwareTrackingToken
import org.axonframework.eventhandling.MultiSourceTrackingToken
import org.axonframework.eventhandling.TrackingToken
/**
* Converts MultiSourceTrackingToken to and from XML. The representation is designed to be compact
* but still human-readable: `<mstt><t n="name1" i="12345"/><t n="name2" i="319479"/></mstt>`.
*
* To support migrating a clustered application to the new format, this can run in a bidirectionally
* compatible mode where it writes both the old and new formats. In that case it includes an
* additional attribute in the top-level tag so it can detect a new-style token. Upgrade the whole
* cluster to this converter, then turn off backward compatibility (by not passing a fallback
* converter) to get the compact form.
*
* This prototype implementation assumes that the underlying tokens are all [GapAwareTrackingToken].
*
* TODO: Cleaner backward compatibility logic; we could look at the child tags rather than adding
* a new attribute.
*
* TODO: Support underlying tokens of arbitrary type.
*
* TODO: Support not including names at all (if the caller passes in a list of names, we can
* make the underlying tokens represent those names in that order).
*
* TODO: If the tokens are all GapAwareTrackingToken and we have a fixed list of names, we could
* ultimately render this as something like `<mstt i="12345,319479" g="12344;319475,319477"/>`
* when there are gaps, or `<mstt i="12345,319479"/>` when there aren't.
*/
class MultiSourceTrackingTokenConverter(
/** Converter to use to write tokens in backward-compatible form. */
private val backwardCompatibleConverter: Converter? = null
) : Converter {
override fun unmarshal(reader: HierarchicalStreamReader, context: UnmarshallingContext): Any {
if (reader.attributeCount > 0 || backwardCompatibleConverter == null) {
// We marshalled this object.
val tokens = mutableMapOf<String, TrackingToken>()
while (reader.hasMoreChildren()) {
try {
reader.moveDown()
val name = reader.getAttribute("n")
val token = context.convertAnother(null, GapAwareTrackingToken::class.java)
tokens[name] = token as TrackingToken
} finally {
reader.moveUp()
}
}
return MultiSourceTrackingToken(tokens)
}
// Unmarshal old-style format
return backwardCompatibleConverter.unmarshal(reader, context)
}
override fun marshal(
original: Any,
writer: HierarchicalStreamWriter,
context: MarshallingContext
) {
if (original !is MultiSourceTrackingToken) {
throw RuntimeException("Unable to marshal object of type ${original.javaClass.name}")
}
if (backwardCompatibleConverter != null) {
writer.addAttribute("v", "1")
}
original.trackingTokens.forEach { (name, token) ->
writer.startNode("t")
context.convertAnother(token)
writer.addAttribute("n", name)
writer.endNode()
}
backwardCompatibleConverter?.marshal(original, writer, context)
}
override fun canConvert(type: Class<*>): Boolean {
return type.name == MultiSourceTrackingToken::class.java.name
}
}
private static void configureCompactTrackingTokens(
XStreamSerializer serializer, boolean backwardCompatibilityMode) {
XStream xStream = serializer.getXStream();
serializer.addAlias("mstt", MultiSourceTrackingToken.class);
serializer.addAlias("gatt", GapAwareTrackingToken.class);
xStream.registerConverter(
new GapAwareTrackingTokenConverter(
backwardCompatibilityMode
? xStream.getConverterLookup().lookupConverterForType(GapAwareTrackingToken.class)
: null));
xStream.registerConverter(
new MultiSourceTrackingTokenConverter(
backwardCompatibilityMode
? xStream
.getConverterLookup()
.lookupConverterForType(MultiSourceTrackingToken.class)
: null));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment