Merge remote-tracking branch 'remotes/open/master' into merges/march-19-14-41

# Conflicts:
#	client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt
#	node-api/src/main/kotlin/net/corda/nodeapi/internal/persistence/CordaPersistence.kt
#	node/src/integration-test/kotlin/net/corda/node/modes/draining/P2PFlowsDrainingModeTest.kt
#	node/src/main/java/CordaCaplet.java
#	node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt
#	testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt
This commit is contained in:
sollecitom
2018-03-19 15:17:49 +00:00
43 changed files with 783 additions and 342 deletions

View File

@ -10,13 +10,21 @@
package net.corda.tools.shell;
import net.corda.core.messaging.*;
import net.corda.client.jackson.*;
import org.crsh.cli.*;
import org.crsh.command.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import net.corda.client.jackson.StringToMethodCallParser;
import net.corda.core.messaging.CordaRPCOps;
import org.crsh.cli.Argument;
import org.crsh.cli.Command;
import org.crsh.cli.Man;
import org.crsh.cli.Usage;
import org.crsh.command.InvocationContext;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static java.util.Comparator.comparing;
// Note that this class cannot be converted to Kotlin because CRaSH does not understand InvocationContext<Map<?, ?>> which
// is the closest you can get in Kotlin to raw types.
@ -39,8 +47,7 @@ public class RunShellCommand extends InteractiveShellCommand {
emitHelp(context, parser);
return null;
}
return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper());
return InteractiveShell.runRPCFromString(command, out, context, ops(), objectMapper(), isSsh());
}
private void emitHelp(InvocationContext<Map> context, StringToMethodCallParser<CordaRPCOps> parser) {
@ -48,21 +55,37 @@ public class RunShellCommand extends InteractiveShellCommand {
// Each element we emit is a map of column -> content.
Set<Map.Entry<String, String>> entries = parser.getAvailableCommands().entrySet();
ArrayList<Map.Entry<String, String>> entryList = new ArrayList<>(entries);
entryList.sort(Comparator.comparing(Map.Entry::getKey));
entryList.sort(comparing(Map.Entry::getKey));
for (Map.Entry<String, String> entry : entryList) {
// Skip these entries as they aren't really interesting for the user.
if (entry.getKey().equals("startFlowDynamic")) continue;
if (entry.getKey().equals("getProtocolVersion")) continue;
// Use a LinkedHashMap to ensure that the Command column comes first.
Map<String, String> m = new LinkedHashMap<>();
m.put("Command", entry.getKey());
m.put("Parameter types", entry.getValue());
try {
context.provide(m);
context.provide(commandAndDesc(entry.getKey(), entry.getValue()));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Lists.newArrayList(
commandAndDesc("shutdown", "Shuts node down (immediately)"),
commandAndDesc("gracefulShutdown", "Shuts node down gracefully, waiting for all flows to complete first.")
).forEach(stringStringMap -> {
try {
context.provide(stringStringMap);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
@NotNull
private Map<String, String> commandAndDesc(String command, String description) {
// Use a LinkedHashMap to ensure that the Command column comes first.
Map<String, String> abruptShutdown = Maps.newLinkedHashMap();
abruptShutdown.put("Command", command);
abruptShutdown.put("Parameter types", description);
return abruptShutdown;
}
}

View File

@ -34,7 +34,7 @@ class CordaAuthenticationPlugin(private val rpcOps: (username: String, credentia
}
try {
val ops = rpcOps(username, credential)
return CordaSSHAuthInfo(true, ops)
return CordaSSHAuthInfo(true, ops, isSsh = true)
} catch (e: ActiveMQSecurityException) {
logger.warn(e.message)
} catch (e: Exception) {

View File

@ -16,7 +16,7 @@ import net.corda.tools.shell.InteractiveShell.createYamlInputMapper
import net.corda.tools.shell.utlities.ANSIProgressRenderer
import org.crsh.auth.AuthInfo
class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null) : AuthInfo {
class CordaSSHAuthInfo(val successful: Boolean, val rpcOps: CordaRPCOps, val ansiProgressRenderer: ANSIProgressRenderer? = null, val isSsh: Boolean = false) : AuthInfo {
override fun isSuccessful(): Boolean = successful
val yamlInputMapper: ObjectMapper by lazy {

View File

@ -11,13 +11,16 @@
package net.corda.tools.shell
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.common.io.Closeables
import net.corda.client.jackson.JacksonSupport
import net.corda.client.jackson.StringToMethodCallParser
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.PermissionException
import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader
import net.corda.core.CordaException
@ -28,13 +31,9 @@ import net.corda.core.identity.Party
import net.corda.core.internal.*
import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.internal.concurrent.openFuture
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.FlowProgressHandle
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.config.SSLConfiguration
import net.corda.tools.shell.utlities.ANSIProgressRenderer
import net.corda.tools.shell.utlities.StdoutANSIProgressRenderer
import org.crsh.command.InvocationContext
@ -60,12 +59,13 @@ import org.json.JSONObject
import org.slf4j.LoggerFactory
import rx.Observable
import rx.Subscriber
import java.io.*
import java.io.FileDescriptor
import java.io.FileInputStream
import java.io.InputStream
import java.io.PrintWriter
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.UndeclaredThrowableException
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutionException
@ -83,69 +83,30 @@ import kotlin.concurrent.thread
// TODO: Resurrect or reimplement the mail plugin.
// TODO: Make it notice new shell commands added after the node started.
data class SSHDConfiguration(val port: Int) {
companion object {
internal const val INVALID_PORT_FORMAT = "Invalid port: %s"
private const val MISSING_PORT_FORMAT = "Missing port: %s"
/**
* Parses a string of the form port into a [SSHDConfiguration].
* @throws IllegalArgumentException if the port is missing or the string is garbage.
*/
@JvmStatic
fun parse(str: String): SSHDConfiguration {
require(!str.isNullOrBlank()) { SSHDConfiguration.MISSING_PORT_FORMAT.format(str) }
val port = try {
str.toInt()
} catch (ex: NumberFormatException) {
throw IllegalArgumentException("Port syntax is invalid, expected port")
}
return SSHDConfiguration(port)
}
}
init {
require(port in (0..0xffff)) { INVALID_PORT_FORMAT.format(port) }
}
}
data class ShellSslOptions(override val sslKeystore: Path, override val keyStorePassword: String, override val trustStoreFile:Path, override val trustStorePassword: String) : SSLConfiguration {
override val certificatesDirectory: Path get() = Paths.get("")
}
data class ShellConfiguration(
val commandsDirectory: Path,
val cordappsDirectory: Path? = null,
var user: String = "",
var password: String = "",
val hostAndPort: NetworkHostAndPort,
val ssl: ShellSslOptions? = null,
val sshdPort: Int? = null,
val sshHostKeyDirectory: Path? = null,
val noLocalShell: Boolean = false) {
companion object {
const val SSH_PORT = 2222
const val COMMANDS_DIR = "shell-commands"
const val CORDAPPS_DIR = "cordapps"
const val SSHD_HOSTKEY_DIR = "ssh"
}
}
object InteractiveShell {
private val log = LoggerFactory.getLogger(javaClass)
private lateinit var rpcOps: (username: String, credentials: String) -> CordaRPCOps
private lateinit var connection: CordaRPCOps
private lateinit var ops: CordaRPCOps
private lateinit var connection: CordaRPCConnection
private var shell: Shell? = null
private var classLoader: ClassLoader? = null
private lateinit var shellConfiguration: ShellConfiguration
private var onExit: () -> Unit = {}
/**
* Starts an interactive shell connected to the local terminal. This shell gives administrator access to the node
* internals.
*/
fun startShell(configuration: ShellConfiguration, classLoader: ClassLoader? = null) {
shellConfiguration = configuration
rpcOps = { username: String, credentials: String ->
val client = createCordaRPCClientWithSslAndClassLoader(hostAndPort = configuration.hostAndPort,
sslConfiguration = configuration.ssl, classLoader = classLoader)
client.start(username, credentials).proxy
configuration = object : CordaRPCClientConfiguration {
override val maxReconnectAttempts = 1
},
sslConfiguration = configuration.ssl,
classLoader = classLoader)
this.connection = client.start(username, credentials)
connection.proxy
}
InteractiveShell.classLoader = classLoader
val runSshDaemon = configuration.sshdPort != null
@ -170,6 +131,7 @@ object InteractiveShell {
}
fun runLocalShell(onExit: () -> Unit = {}) {
this.onExit = onExit
val terminal = TerminalFactory.create()
val consoleReader = ConsoleReader("Corda", FileInputStream(FileDescriptor.`in`), System.out, terminal)
val jlineProcessor = JLineProcessor(terminal.isAnsiSupported, shell, consoleReader, System.out)
@ -215,18 +177,18 @@ object InteractiveShell {
return super.getPlugins().filterNot { it is JavaLanguage } + CordaAuthenticationPlugin(rpcOps)
}
}
val attributes = emptyMap<String,Any>()
val attributes = emptyMap<String, Any>()
val context = PluginContext(discovery, attributes, commandsFS, confFS, classLoader)
context.refresh()
this.config = config
start(context)
connection = makeRPCOps(rpcOps, localUserName, localUserPassword)
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, connection, StdoutANSIProgressRenderer))
ops = makeRPCOps(rpcOps, localUserName, localUserPassword)
return context.getPlugin(ShellFactory::class.java).create(null, CordaSSHAuthInfo(false, ops, StdoutANSIProgressRenderer))
}
}
fun nodeInfo() = try {
connection.nodeInfo()
ops.nodeInfo()
} catch (e: UndeclaredThrowableException) {
throw e.cause ?: e
}
@ -421,14 +383,16 @@ object InteractiveShell {
}
@JvmStatic
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper): Any? {
fun runRPCFromString(input: List<String>, out: RenderPrintWriter, context: InvocationContext<out Any>, cordaRPCOps: CordaRPCOps, om: ObjectMapper, isSsh: Boolean = false): Any? {
val cmd = input.joinToString(" ").trim { it <= ' ' }
if (cmd.toLowerCase().startsWith("startflow")) {
if (cmd.startsWith("startflow", ignoreCase = true)) {
// The flow command provides better support and startFlow requires special handling anyway due to
// the generic startFlow RPC interface which offers no type information with which to parse the
// string form of the command.
out.println("Please use the 'flow' command to interact with flows rather than the 'run' command.", Color.yellow)
return null
} else if (cmd.substringAfter(" ").trim().equals("gracefulShutdown", ignoreCase = true)) {
return InteractiveShell.gracefulShutdown(out, cordaRPCOps, isSsh)
}
var result: Any? = null
@ -467,6 +431,68 @@ object InteractiveShell {
return result
}
@JvmStatic
fun gracefulShutdown(userSessionOut: RenderPrintWriter, cordaRPCOps: CordaRPCOps, isSsh: Boolean = false) {
fun display(statements: RenderPrintWriter.() -> Unit) {
statements.invoke(userSessionOut)
userSessionOut.flush()
}
var isShuttingDown = false
try {
display {
println("Orchestrating a clean shutdown...")
println("...enabling draining mode")
}
cordaRPCOps.setFlowsDrainingModeEnabled(true)
display {
println("...waiting for in-flight flows to be completed")
}
cordaRPCOps.pendingFlowsCount().updates
.doOnError { error ->
log.error(error.message)
throw error
}
.doOnNext { remaining ->
display {
println("...remaining: ${remaining.first}/${remaining.second}")
}
}
.doOnCompleted {
if (isSsh) {
// print in the original Shell process
System.out.println("Shutting down the node via remote SSH session (it may take a while)")
}
display {
println("Shutting down the node (it may take a while)")
}
cordaRPCOps.shutdown()
isShuttingDown = true
connection.forceClose()
display {
println("...done, quitting standalone shell now.")
}
onExit.invoke()
}.toBlocking().single()
} catch (e: StringToMethodCallParser.UnparseableCallException) {
display {
println(e.message, Color.red)
println("Please try 'man run' to learn what syntax is acceptable")
}
} catch (e: Exception) {
if (!isShuttingDown) {
display {
println("RPC failed: ${e.rootCause}", Color.red)
}
}
} finally {
InputStreamSerializer.invokeContext = null
InputStreamDeserializer.closeAll()
}
}
private fun printAndFollowRPCResponse(response: Any?, out: PrintWriter): CordaFuture<Unit> {
val mapElement: (Any?) -> String = { element -> outputMapper.writerWithDefaultPrettyPrinter().writeValueAsString(element) }
@ -528,6 +554,7 @@ object InteractiveShell {
return printNextElements(response.updates, printerFun, out)
}
if (response is Observable<*>) {
return printNextElements(response, printerFun, out)
}
@ -542,94 +569,4 @@ object InteractiveShell {
return subscriber.future
}
//region Extra serializers
//
// These serializers are used to enable the user to specify objects that aren't natural data containers in the shell,
// and for the shell to print things out that otherwise wouldn't be usefully printable.
private object ObservableSerializer : JsonSerializer<Observable<*>>() {
override fun serialize(value: Observable<*>, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString("(observable)")
}
}
// A file name is deserialized to an InputStream if found.
object InputStreamDeserializer : JsonDeserializer<InputStream>() {
// Keep track of them so we can close them later.
private val streams = Collections.synchronizedSet(HashSet<InputStream>())
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): InputStream {
val stream = object : BufferedInputStream(Files.newInputStream(Paths.get(p.text))) {
override fun close() {
super.close()
streams.remove(this)
}
}
streams += stream
return stream
}
fun closeAll() {
// Clone the set with toList() here so each closed stream can be removed from the set inside close().
streams.toList().forEach { Closeables.closeQuietly(it) }
}
}
// An InputStream found in a response triggers a request to the user to provide somewhere to save it.
private object InputStreamSerializer : JsonSerializer<InputStream>() {
var invokeContext: InvocationContext<*>? = null
override fun serialize(value: InputStream, gen: JsonGenerator, serializers: SerializerProvider) {
try {
val toPath = invokeContext!!.readLine("Path to save stream to (enter to ignore): ", true)
if (toPath == null || toPath.isBlank()) {
gen.writeString("<not saved>")
} else {
val path = Paths.get(toPath)
value.copyTo(path)
gen.writeString("<saved to: ${path.toAbsolutePath()}>")
}
} finally {
try {
value.close()
} catch (e: IOException) {
// Ignore.
}
}
}
}
/**
* String value deserialized to [UniqueIdentifier].
* Any string value used as [UniqueIdentifier.externalId].
* If string contains underscore(i.e. externalId_uuid) then split with it.
* Index 0 as [UniqueIdentifier.externalId]
* Index 1 as [UniqueIdentifier.id]
* */
object UniqueIdentifierDeserializer : JsonDeserializer<UniqueIdentifier>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UniqueIdentifier {
//Check if externalId and UUID may be separated by underscore.
if (p.text.contains("_")) {
val ids = p.text.split("_")
//Create UUID object from string.
val uuid: UUID = UUID.fromString(ids[1])
//Create UniqueIdentifier object using externalId and UUID.
return UniqueIdentifier(ids[0], uuid)
}
//Any other string used as externalId.
return UniqueIdentifier.fromString(p.text)
}
}
/**
* String value deserialized to [UUID].
* */
object UUIDDeserializer : JsonDeserializer<UUID>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UUID {
//Create UUID object from string.
return UUID.fromString(p.text)
}
}
//endregion
}

View File

@ -20,4 +20,5 @@ open class InteractiveShellCommand : BaseCommand() {
fun ops() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).rpcOps
fun ansiProgressRenderer() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).ansiProgressRenderer
fun objectMapper() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).yamlInputMapper
fun isSsh() = ((context.session as CRaSHSession).authInfo as CordaSSHAuthInfo).isSsh
}

View File

@ -26,6 +26,5 @@ fun makeRPCOps(getCordaRPCOps: (username: String, credential: String) -> CordaRP
// Unpack exception.
throw e.targetException
}
}
) as CordaRPCOps
}) as CordaRPCOps
}

View File

@ -0,0 +1,27 @@
package net.corda.tools.shell
data class SSHDConfiguration(val port: Int) {
companion object {
internal const val INVALID_PORT_FORMAT = "Invalid port: %s"
private const val MISSING_PORT_FORMAT = "Missing port: %s"
/**
* Parses a string of the form port into a [SSHDConfiguration].
* @throws IllegalArgumentException if the port is missing or the string is garbage.
*/
@JvmStatic
fun parse(str: String): SSHDConfiguration {
require(!str.isNullOrBlank()) { SSHDConfiguration.MISSING_PORT_FORMAT.format(str) }
val port = try {
str.toInt()
} catch (ex: NumberFormatException) {
throw IllegalArgumentException("Port syntax is invalid, expected port")
}
return SSHDConfiguration(port)
}
}
init {
require(port in (0..0xffff)) { INVALID_PORT_FORMAT.format(port) }
}
}

View File

@ -0,0 +1,103 @@
package net.corda.tools.shell
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.google.common.io.Closeables
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.internal.copyTo
import org.crsh.command.InvocationContext
import rx.Observable
import java.io.BufferedInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
import java.util.*
//region Extra serializers
//
// These serializers are used to enable the user to specify objects that aren't natural data containers in the shell,
// and for the shell to print things out that otherwise wouldn't be usefully printable.
object ObservableSerializer : JsonSerializer<Observable<*>>() {
override fun serialize(value: Observable<*>, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString("(observable)")
}
}
/**
* String value deserialized to [UniqueIdentifier].
* Any string value used as [UniqueIdentifier.externalId].
* If string contains underscore(i.e. externalId_uuid) then split with it.
* Index 0 as [UniqueIdentifier.externalId]
* Index 1 as [UniqueIdentifier.id]
* */
object UniqueIdentifierDeserializer : JsonDeserializer<UniqueIdentifier>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UniqueIdentifier {
//Check if externalId and UUID may be separated by underscore.
if (p.text.contains("_")) {
val ids = p.text.split("_")
//Create UUID object from string.
val uuid: UUID = UUID.fromString(ids[1])
//Create UniqueIdentifier object using externalId and UUID.
return UniqueIdentifier(ids[0], uuid)
}
//Any other string used as externalId.
return UniqueIdentifier.fromString(p.text)
}
}
/**
* String value deserialized to [UUID].
* */
object UUIDDeserializer : JsonDeserializer<UUID>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): UUID {
//Create UUID object from string.
return UUID.fromString(p.text)
}
}
// An InputStream found in a response triggers a request to the user to provide somewhere to save it.
object InputStreamSerializer : JsonSerializer<InputStream>() {
var invokeContext: InvocationContext<*>? = null
override fun serialize(value: InputStream, gen: JsonGenerator, serializers: SerializerProvider) {
value.use {
val toPath = invokeContext!!.readLine("Path to save stream to (enter to ignore): ", true)
if (toPath == null || toPath.isBlank()) {
gen.writeString("<not saved>")
} else {
val path = Paths.get(toPath)
it.copyTo(path)
gen.writeString("<saved to: ${path.toAbsolutePath()}>")
}
}
}
}
// A file name is deserialized to an InputStream if found.
object InputStreamDeserializer : JsonDeserializer<InputStream>() {
// Keep track of them so we can close them later.
private val streams = Collections.synchronizedSet(HashSet<InputStream>())
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): InputStream {
val stream = object : BufferedInputStream(Files.newInputStream(Paths.get(p.text))) {
override fun close() {
super.close()
streams.remove(this)
}
}
streams += stream
return stream
}
fun closeAll() {
// Clone the set with toList() here so each closed stream can be removed from the set inside close().
streams.toList().forEach { Closeables.closeQuietly(it) }
}
}
//endregion

View File

@ -0,0 +1,28 @@
package net.corda.tools.shell
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.internal.config.SSLConfiguration
import java.nio.file.Path
import java.nio.file.Paths
data class ShellConfiguration(
val commandsDirectory: Path,
val cordappsDirectory: Path? = null,
var user: String = "",
var password: String = "",
val hostAndPort: NetworkHostAndPort,
val ssl: ShellSslOptions? = null,
val sshdPort: Int? = null,
val sshHostKeyDirectory: Path? = null,
val noLocalShell: Boolean = false) {
companion object {
const val SSH_PORT = 2222
const val COMMANDS_DIR = "shell-commands"
const val CORDAPPS_DIR = "cordapps"
const val SSHD_HOSTKEY_DIR = "ssh"
}
}
data class ShellSslOptions(override val sslKeystore: Path, override val keyStorePassword: String, override val trustStoreFile:Path, override val trustStorePassword: String) : SSLConfiguration {
override val certificatesDirectory: Path get() = Paths.get("")
}

View File

@ -115,6 +115,7 @@ class StandaloneShell(private val configuration: ShellConfiguration) {
configuration.sshdPort?.apply{ println("SSH server listening on port $this.") }
exit.await()
// because we can't clean certain Crash Shell threads that block on read()
exitProcess(0)
}
}

View File

@ -37,8 +37,8 @@ class CustomTypeJsonParsingTests {
fun setup() {
objectMapper = ObjectMapper()
val simpleModule = SimpleModule()
simpleModule.addDeserializer(UniqueIdentifier::class.java, InteractiveShell.UniqueIdentifierDeserializer)
simpleModule.addDeserializer(UUID::class.java, InteractiveShell.UUIDDeserializer)
simpleModule.addDeserializer(UniqueIdentifier::class.java, UniqueIdentifierDeserializer)
simpleModule.addDeserializer(UUID::class.java, UUIDDeserializer)
objectMapper.registerModule(simpleModule)
}