Moved JsonSupport to new webserver module. Fixed a few compile errors.

Fixed compile issues caused by webserver being split to a separate project.
WebServer now starts and stops correctly as a separate module.
This commit is contained in:
Clinton Alexander
2017-01-30 14:00:54 +00:00
committed by Clinton Alexander
parent bc9f86905c
commit 8414c97a61
37 changed files with 450 additions and 300 deletions

View File

@ -0,0 +1,34 @@
package net.corda.webserver
import com.google.common.net.HostAndPort
import net.corda.core.getOrThrow
import net.corda.node.driver.addressMustBeBound
import net.corda.node.driver.addressMustNotBeBound
import net.corda.node.driver.driver
import org.junit.Test
import java.util.concurrent.Executors
class DriverTests {
companion object {
val executorService = Executors.newScheduledThreadPool(2)
fun webserverMustBeUp(webserverAddr: HostAndPort) {
addressMustBeBound(executorService, webserverAddr)
}
fun webserverMustBeDown(webserverAddr: HostAndPort) {
addressMustNotBeBound(executorService, webserverAddr)
}
}
@Test
fun `starting a node and independent web server works`() {
val addr = driver {
val node = startNode("test").getOrThrow()
val webserverAddr = startWebserver(node).getOrThrow()
webserverMustBeUp(webserverAddr)
webserverAddr
}
webserverMustBeDown(addr)
}
}

View File

@ -1,181 +1,71 @@
package net.corda.node.webserver
@file:JvmName("WebServer")
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.utilities.loggerFor
import net.corda.node.printBasicNodeInfo
package net.corda.webserver
import com.typesafe.config.ConfigException
import net.corda.core.div
import net.corda.core.rootCause
import net.corda.node.ArgsParser
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.CordaRPCClient
import net.corda.node.webserver.internal.APIServerImpl
import net.corda.node.webserver.servlets.AttachmentDownloadServlet
import net.corda.node.webserver.servlets.DataUploadServlet
import net.corda.node.webserver.servlets.ObjectMapperConfig
import net.corda.node.webserver.servlets.ResponseFilter
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.eclipse.jetty.server.*
import org.eclipse.jetty.server.handler.HandlerCollection
import org.eclipse.jetty.servlet.DefaultServlet
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.eclipse.jetty.webapp.WebAppContext
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.server.ServerProperties
import org.glassfish.jersey.servlet.ServletContainer
import java.lang.reflect.InvocationTargetException
import java.util.*
import net.corda.webserver.internal.NodeWebServer
import org.slf4j.LoggerFactory
import java.lang.management.ManagementFactory
import java.net.InetAddress
import kotlin.system.exitProcess
// TODO: Split into a separate module under client that packages into WAR formats.
class WebServer(val config: FullNodeConfiguration) {
private companion object {
val log = loggerFor<WebServer>()
val retryDelay = 1000L // Milliseconds
fun main(args: Array<String>) {
val startTime = System.currentTimeMillis()
val argsParser = ArgsParser()
val cmdlineOptions = try {
argsParser.parse(*args)
} catch (ex: Exception) {
println("Unknown command line arguments: ${ex.message}")
exitProcess(1)
}
val address = config.webAddress
private lateinit var server: Server
fun start() {
printBasicNodeInfo("Starting as webserver: ${config.webAddress}")
server = initWebServer(retryConnectLocalRpc())
// Maybe render command line help.
if (cmdlineOptions.help) {
argsParser.printHelp(System.out)
exitProcess(0)
}
fun run() {
while (server.isRunning) {
Thread.sleep(100) // TODO: Redesign
}
// Set up logging.
if (cmdlineOptions.logToConsole) {
// This property is referenced from the XML config file.
System.setProperty("consoleLogLevel", "info")
}
private fun initWebServer(localRpc: CordaRPCOps): Server {
// Note that the web server handlers will all run concurrently, and not on the node thread.
val handlerCollection = HandlerCollection()
System.setProperty("log-path", (cmdlineOptions.baseDirectory / "web/logs").toString())
val log = LoggerFactory.getLogger("Main")
println("Logs can be found in ${System.getProperty("log-path")}")
// TODO: Move back into the node itself.
// Export JMX monitoring statistics and data over REST/JSON.
if (config.exportJMXto.split(',').contains("http")) {
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
val warpath = classpath.firstOrNull { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
if (warpath != null) {
handlerCollection.addHandler(WebAppContext().apply {
// Find the jolokia WAR file on the classpath.
contextPath = "/monitoring/json"
setInitParameter("mimeType", "application/json")
war = warpath
})
} else {
log.warn("Unable to locate Jolokia WAR on classpath")
}
}
val conf = try {
FullNodeConfiguration(cmdlineOptions.baseDirectory, cmdlineOptions.loadConfig())
} catch (e: ConfigException) {
println("Unable to load the configuration file: ${e.rootCause.message}")
exitProcess(2)
}
// API, data upload and download to services (attachments, rates oracles etc)
handlerCollection.addHandler(buildServletContextHandler(localRpc))
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
val info = ManagementFactory.getRuntimeMXBean()
log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
log.info("Application Args: ${args.joinToString(" ")}")
log.info("bootclasspath: ${info.bootClassPath}")
log.info("classpath: ${info.classPath}")
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
log.info("Machine: ${InetAddress.getLocalHost().hostName}")
log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
log.info("Starting as webserver on ${conf.webAddress}")
val server = Server()
val connector = if (config.useHTTPS) {
val httpsConfiguration = HttpConfiguration()
httpsConfiguration.outputBufferSize = 32768
httpsConfiguration.addCustomizer(SecureRequestCustomizer())
val sslContextFactory = SslContextFactory()
sslContextFactory.keyStorePath = config.keyStoreFile.toString()
sslContextFactory.setKeyStorePassword(config.keyStorePassword)
sslContextFactory.setKeyManagerPassword(config.keyStorePassword)
sslContextFactory.setTrustStorePath(config.trustStoreFile.toString())
sslContextFactory.setTrustStorePassword(config.trustStorePassword)
sslContextFactory.setExcludeProtocols("SSL.*", "TLSv1", "TLSv1.1")
sslContextFactory.setIncludeProtocols("TLSv1.2")
sslContextFactory.setExcludeCipherSuites(".*NULL.*", ".*RC4.*", ".*MD5.*", ".*DES.*", ".*DSS.*")
sslContextFactory.setIncludeCipherSuites(".*AES.*GCM.*")
val sslConnector = ServerConnector(server, SslConnectionFactory(sslContextFactory, "http/1.1"), HttpConnectionFactory(httpsConfiguration))
sslConnector.port = address.port
sslConnector
} else {
val httpConfiguration = HttpConfiguration()
httpConfiguration.outputBufferSize = 32768
val httpConnector = ServerConnector(server, HttpConnectionFactory(httpConfiguration))
httpConnector.port = address.port
httpConnector
}
server.connectors = arrayOf<Connector>(connector)
server.handler = handlerCollection
try {
val server = NodeWebServer(conf)
server.start()
log.info("Started webserver on address $address")
return server
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
println("Webserver started up in $elapsed sec")
server.run()
} catch (e: Exception) {
log.error("Exception during node startup", e)
exitProcess(1)
}
private fun buildServletContextHandler(localRpc: CordaRPCOps): ServletContextHandler {
return ServletContextHandler().apply {
contextPath = "/"
setAttribute("rpc", localRpc)
addServlet(DataUploadServlet::class.java, "/upload/*")
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
val resourceConfig = ResourceConfig()
resourceConfig.register(ObjectMapperConfig(localRpc))
resourceConfig.register(ResponseFilter())
resourceConfig.register(APIServerImpl(localRpc))
val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis }
for (webapi in webAPIsOnClasspath) {
log.info("Add plugin web API from attachment $webapi")
val customAPI = try {
webapi.apply(localRpc)
} catch (ex: InvocationTargetException) {
log.error("Constructor $webapi threw an error: ", ex.targetException)
continue
}
resourceConfig.register(customAPI)
}
val staticDirMaps = pluginRegistries.map { x -> x.staticServeDirs }
val staticDirs = staticDirMaps.flatMap { it.keys }.zip(staticDirMaps.flatMap { it.values })
staticDirs.forEach {
val staticDir = ServletHolder(DefaultServlet::class.java)
staticDir.setInitParameter("resourceBase", it.second)
staticDir.setInitParameter("dirAllowed", "true")
staticDir.setInitParameter("pathInfoOnly", "true")
addServlet(staticDir, "/web/${it.first}/*")
}
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
val container = ServletContainer(resourceConfig)
val jerseyServlet = ServletHolder(container)
addServlet(jerseyServlet, "/api/*")
jerseyServlet.initOrder = 0 // Initialise at server start
}
}
private fun retryConnectLocalRpc(): CordaRPCOps {
while (true) {
try {
return connectLocalRpcAsNodeUser()
} catch (e: ActiveMQNotConnectedException) {
log.debug("Could not connect to ${config.artemisAddress} due to exception: ", e)
Thread.sleep(retryDelay)
// This error will happen if the server has yet to create the keystore
// Keep the fully qualified package name due to collisions with the Kotlin stdlib
// exception of the same name
} catch (e: java.nio.file.NoSuchFileException) {
log.debug("Tried to open a file that doesn't yet exist, retrying", e)
Thread.sleep(retryDelay)
}
}
}
private fun connectLocalRpcAsNodeUser(): CordaRPCOps {
log.info("Connecting to node at ${config.artemisAddress} as node user")
val client = CordaRPCClient(config.artemisAddress, config)
client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER)
return client.proxy()
}
/** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */
val pluginRegistries: List<CordaPluginRegistry> by lazy {
ServiceLoader.load(CordaPluginRegistry::class.java).toList()
}
}
}

View File

@ -1,4 +1,4 @@
package net.corda.node.webserver.api
package net.corda.webserver.api
import net.corda.core.node.NodeInfo
import java.time.LocalDateTime

View File

@ -1,4 +1,4 @@
package net.corda.node.webserver.api
package net.corda.webserver.api
/**
* Extremely rudimentary query language which should most likely be replaced with a product.

View File

@ -1,7 +1,7 @@
package net.corda.node.webserver.internal
package net.corda.webserver.internal
import net.corda.core.messaging.CordaRPCOps
import net.corda.node.webserver.api.*
import net.corda.webserver.api.*
import java.time.LocalDateTime
import java.time.ZoneId
import javax.ws.rs.core.Response

View File

@ -0,0 +1,176 @@
package net.corda.webserver.internal
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.utilities.loggerFor
import net.corda.node.printBasicNodeInfo
import net.corda.node.services.config.FullNodeConfiguration
import net.corda.node.services.messaging.ArtemisMessagingComponent
import net.corda.node.services.messaging.CordaRPCClient
import net.corda.webserver.servlets.AttachmentDownloadServlet
import net.corda.webserver.servlets.DataUploadServlet
import net.corda.webserver.servlets.ObjectMapperConfig
import net.corda.webserver.servlets.ResponseFilter
import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException
import org.eclipse.jetty.server.*
import org.eclipse.jetty.server.handler.HandlerCollection
import org.eclipse.jetty.servlet.DefaultServlet
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletHolder
import org.eclipse.jetty.util.ssl.SslContextFactory
import org.eclipse.jetty.webapp.WebAppContext
import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.server.ServerProperties
import org.glassfish.jersey.servlet.ServletContainer
import java.lang.reflect.InvocationTargetException
import java.net.InetAddress
import java.util.*
class NodeWebServer(val config: FullNodeConfiguration) {
private companion object {
val log = loggerFor<NodeWebServer>()
val retryDelay = 1000L // Milliseconds
}
val address = config.webAddress
private lateinit var server: Server
fun start() {
printBasicNodeInfo("Starting as webserver: ${config.webAddress}")
server = initWebServer(retryConnectLocalRpc())
}
fun run() {
while (server.isRunning) {
Thread.sleep(100) // TODO: Redesign
}
}
private fun initWebServer(localRpc: CordaRPCOps): Server {
// Note that the web server handlers will all run concurrently, and not on the node thread.
val handlerCollection = HandlerCollection()
// Export JMX monitoring statistics and data over REST/JSON.
if (config.exportJMXto.split(',').contains("http")) {
val classpath = System.getProperty("java.class.path").split(System.getProperty("path.separator"))
val warpath = classpath.firstOrNull { it.contains("jolokia-agent-war-2") && it.endsWith(".war") }
if (warpath != null) {
handlerCollection.addHandler(WebAppContext().apply {
// Find the jolokia WAR file on the classpath.
contextPath = "/monitoring/json"
setInitParameter("mimeType", "application/json")
war = warpath
})
} else {
log.warn("Unable to locate Jolokia WAR on classpath")
}
}
// API, data upload and download to services (attachments, rates oracles etc)
handlerCollection.addHandler(buildServletContextHandler(localRpc))
val server = Server()
val connector = if (config.useHTTPS) {
val httpsConfiguration = HttpConfiguration()
httpsConfiguration.outputBufferSize = 32768
httpsConfiguration.addCustomizer(SecureRequestCustomizer())
val sslContextFactory = SslContextFactory()
sslContextFactory.keyStorePath = config.keyStoreFile.toString()
sslContextFactory.setKeyStorePassword(config.keyStorePassword)
sslContextFactory.setKeyManagerPassword(config.keyStorePassword)
sslContextFactory.setTrustStorePath(config.trustStoreFile.toString())
sslContextFactory.setTrustStorePassword(config.trustStorePassword)
sslContextFactory.setExcludeProtocols("SSL.*", "TLSv1", "TLSv1.1")
sslContextFactory.setIncludeProtocols("TLSv1.2")
sslContextFactory.setExcludeCipherSuites(".*NULL.*", ".*RC4.*", ".*MD5.*", ".*DES.*", ".*DSS.*")
sslContextFactory.setIncludeCipherSuites(".*AES.*GCM.*")
val sslConnector = ServerConnector(server, SslConnectionFactory(sslContextFactory, "http/1.1"), HttpConnectionFactory(httpsConfiguration))
sslConnector.port = address.port
sslConnector
} else {
val httpConfiguration = HttpConfiguration()
httpConfiguration.outputBufferSize = 32768
val httpConnector = ServerConnector(server, HttpConnectionFactory(httpConfiguration))
log.info("Starting webserver on address $address")
httpConnector.port = address.port
httpConnector
}
server.connectors = arrayOf<Connector>(connector)
server.handler = handlerCollection
//runOnStop += Runnable { server.stop() }
server.start()
log.info("Server started")
log.info("Embedded web server is listening on", "http://${InetAddress.getLocalHost().hostAddress}:${connector.port}/")
return server
}
private fun buildServletContextHandler(localRpc: CordaRPCOps): ServletContextHandler {
return ServletContextHandler().apply {
contextPath = "/"
setAttribute("rpc", localRpc)
addServlet(DataUploadServlet::class.java, "/upload/*")
addServlet(AttachmentDownloadServlet::class.java, "/attachments/*")
val resourceConfig = ResourceConfig()
resourceConfig.register(ObjectMapperConfig(localRpc))
resourceConfig.register(ResponseFilter())
resourceConfig.register(APIServerImpl(localRpc))
val webAPIsOnClasspath = pluginRegistries.flatMap { x -> x.webApis }
for (webapi in webAPIsOnClasspath) {
log.info("Add plugin web API from attachment $webapi")
val customAPI = try {
webapi.apply(localRpc)
} catch (ex: InvocationTargetException) {
log.error("Constructor $webapi threw an error: ", ex.targetException)
continue
}
resourceConfig.register(customAPI)
}
val staticDirMaps = pluginRegistries.map { x -> x.staticServeDirs }
val staticDirs = staticDirMaps.flatMap { it.keys }.zip(staticDirMaps.flatMap { it.values })
staticDirs.forEach {
val staticDir = ServletHolder(DefaultServlet::class.java)
staticDir.setInitParameter("resourceBase", it.second)
staticDir.setInitParameter("dirAllowed", "true")
staticDir.setInitParameter("pathInfoOnly", "true")
addServlet(staticDir, "/web/${it.first}/*")
}
// Give the app a slightly better name in JMX rather than a randomly generated one and enable JMX
resourceConfig.addProperties(mapOf(ServerProperties.APPLICATION_NAME to "node.api",
ServerProperties.MONITORING_STATISTICS_MBEANS_ENABLED to "true"))
val container = ServletContainer(resourceConfig)
val jerseyServlet = ServletHolder(container)
addServlet(jerseyServlet, "/api/*")
jerseyServlet.initOrder = 0 // Initialise at server start
}
}
private fun retryConnectLocalRpc(): CordaRPCOps {
while (true) {
try {
return connectLocalRpcAsNodeUser()
} catch (e: ActiveMQNotConnectedException) {
log.debug("Could not connect to ${config.artemisAddress} due to exception: ", e)
Thread.sleep(retryDelay)
}
}
}
private fun connectLocalRpcAsNodeUser(): CordaRPCOps {
log.info("Connecting to node at ${config.artemisAddress} as node user")
val client = CordaRPCClient(config.artemisAddress, config)
client.start(ArtemisMessagingComponent.NODE_USER, ArtemisMessagingComponent.NODE_USER)
return client.proxy()
}
/** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */
val pluginRegistries: List<CordaPluginRegistry> by lazy {
ServiceLoader.load(CordaPluginRegistry::class.java).toList()
}
}

View File

@ -1,4 +1,4 @@
package net.corda.node.webserver.servlets
package net.corda.webserver.servlets
import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.StorageService

View File

@ -1,9 +1,7 @@
package net.corda.node.webserver.servlets
package net.corda.webserver.servlets
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.loggerFor
import net.corda.node.internal.Node
import net.corda.node.services.api.AcceptsFileUpload
import org.apache.commons.fileupload.servlet.ServletFileUpload
import java.util.*
import javax.servlet.http.HttpServlet
@ -11,7 +9,7 @@ import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
/**
* Accepts binary streams, finds the right [AcceptsFileUpload] implementor and hands the stream off to it.
* Uploads to the node via the [CordaRPCOps] uploadFile interface.
*/
class DataUploadServlet: HttpServlet() {
private val log = loggerFor<DataUploadServlet>()
@ -41,6 +39,7 @@ class DataUploadServlet: HttpServlet() {
try {
val dataType = req.pathInfo.substring(1).substringBefore('/')
@Suppress("DEPRECATION") // TODO: Replace the use of uploadFile
messages += rpc.uploadFile(dataType, item.name, item.openStream())
log.info("${item.name} successfully accepted: ${messages.last()}")
} catch(e: RuntimeException) {

View File

@ -1,9 +1,8 @@
package net.corda.node.webserver.servlets
package net.corda.webserver.servlets
import com.fasterxml.jackson.databind.ObjectMapper
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.ServiceHub
import net.corda.node.utilities.JsonSupport
import net.corda.webserver.utilities.JsonSupport
import javax.ws.rs.ext.ContextResolver
import javax.ws.rs.ext.Provider

View File

@ -1,4 +1,4 @@
package net.corda.node.webserver.servlets
package net.corda.webserver.servlets
import javax.ws.rs.container.ContainerRequestContext
import javax.ws.rs.container.ContainerResponseContext

View File

@ -0,0 +1,252 @@
package net.corda.webserver.utilities
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers
import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.*
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.corda.core.node.services.IdentityService
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime
/**
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
* the java.time API, some core types, and Kotlin data classes.
*
* TODO: This does not belong in node. It should be moved to the client module or a dedicated webserver module.
*/
object JsonSupport {
interface PartyObjectMapper {
fun partyFromName(partyName: String): Party?
fun partyFromKey(owningKey: CompositeKey): Party?
}
class RpcObjectMapper(val rpc: CordaRPCOps) : PartyObjectMapper, ObjectMapper() {
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
}
class IdentityObjectMapper(val identityService: IdentityService) : PartyObjectMapper, ObjectMapper(){
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
}
class NoPartyObjectMapper: PartyObjectMapper, ObjectMapper() {
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
}
val javaTimeModule: Module by lazy {
SimpleModule("java.time").apply {
addSerializer(LocalDate::class.java, ToStringSerializer)
addDeserializer(LocalDate::class.java, LocalDateDeserializer)
addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
addSerializer(LocalDateTime::class.java, ToStringSerializer)
}
}
val cordaModule: Module by lazy {
SimpleModule("core").apply {
addSerializer(AnonymousParty::class.java, AnonymousPartySerializer)
addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer)
addSerializer(Party::class.java, PartySerializer)
addDeserializer(Party::class.java, PartyDeserializer)
addSerializer(BigDecimal::class.java, ToStringSerializer)
addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
addSerializer(SecureHash::class.java, SecureHashSerializer)
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
// For composite keys
addSerializer(CompositeKey::class.java, CompositeKeySerializer)
addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
// For NodeInfo
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
addSerializer(NodeInfo::class.java, NodeInfoSerializer)
addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
}
}
/* Mapper requiring RPC support to deserialise parties from names */
fun createDefaultMapper(rpc: CordaRPCOps): ObjectMapper = configureMapper(RpcObjectMapper(rpc))
/* For testing or situations where deserialising parties is not required */
fun createNonRpcMapper(): ObjectMapper = configureMapper(NoPartyObjectMapper())
/* For testing with an in memory identity service */
fun createInMemoryMapper(identityService: IdentityService) = configureMapper(IdentityObjectMapper(identityService))
private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply {
enable(SerializationFeature.INDENT_OUTPUT)
enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
registerModule(javaTimeModule)
registerModule(cordaModule)
registerModule(KotlinModule())
}
object ToStringSerializer : JsonSerializer<Any>() {
override fun serialize(obj: Any, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
object LocalDateDeserializer : JsonDeserializer<LocalDate>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDate {
return try {
LocalDate.parse(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid LocalDate ${parser.text}: ${e.message}")
}
}
}
object LocalDateKeyDeserializer : KeyDeserializer() {
override fun deserializeKey(text: String, p1: DeserializationContext): Any? {
return LocalDate.parse(text)
}
}
object AnonymousPartySerializer : JsonSerializer<AnonymousParty>() {
override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.owningKey.toBase58String())
}
}
object AnonymousPartyDeserializer : JsonDeserializer<AnonymousParty>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): AnonymousParty {
if (parser.currentToken == JsonToken.FIELD_NAME) {
parser.nextToken()
}
val mapper = parser.codec as PartyObjectMapper
// TODO this needs to use some industry identifier(s) instead of these keys
val key = CompositeKey.parseFromBase58(parser.text)
val party = mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${parser.text}")
return party.toAnonymous()
}
}
object PartySerializer : JsonSerializer<Party>() {
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.name)
}
}
object PartyDeserializer : JsonDeserializer<Party>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Party {
if (parser.currentToken == JsonToken.FIELD_NAME) {
parser.nextToken()
}
val mapper = parser.codec as PartyObjectMapper
// TODO this needs to use some industry identifier(s) not just these human readable names
return mapper.partyFromName(parser.text) ?: throw JsonParseException(parser, "Could not find a Party with name ${parser.text}")
}
}
object NodeInfoSerializer : JsonSerializer<NodeInfo>() {
override fun serialize(value: NodeInfo, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(Base58.encode(value.serialize().bytes))
}
}
object NodeInfoDeserializer : JsonDeserializer<NodeInfo>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): NodeInfo {
if (parser.currentToken == JsonToken.FIELD_NAME) {
parser.nextToken()
}
try {
return Base58.decode(parser.text).deserialize<NodeInfo>()
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid NodeInfo ${parser.text}: ${e.message}")
}
}
}
object SecureHashSerializer : JsonSerializer<SecureHash>() {
override fun serialize(obj: SecureHash, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toString())
}
}
/**
* Implemented as a class so that we can instantiate for T.
*/
class SecureHashDeserializer<T : SecureHash> : JsonDeserializer<T>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
if (parser.currentToken == JsonToken.FIELD_NAME) {
parser.nextToken()
}
try {
@Suppress("UNCHECKED_CAST")
return SecureHash.parse(parser.text) as T
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid hash ${parser.text}: ${e.message}")
}
}
}
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
return try {
val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
}
}
}
object PublicKeySerializer : JsonSerializer<EdDSAPublicKey>() {
override fun serialize(obj: EdDSAPublicKey, generator: JsonGenerator, provider: SerializerProvider) {
check(obj.params == ed25519Curve)
generator.writeString(obj.toBase58String())
}
}
object PublicKeyDeserializer : JsonDeserializer<EdDSAPublicKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): EdDSAPublicKey {
return try {
parsePublicKeyBase58(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid public key ${parser.text}: ${e.message}")
}
}
}
object CompositeKeySerializer : JsonSerializer<CompositeKey>() {
override fun serialize(obj: CompositeKey, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(obj.toBase58String())
}
}
object CompositeKeyDeserializer : JsonDeserializer<CompositeKey>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): CompositeKey {
return try {
CompositeKey.parseFromBase58(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid composite key ${parser.text}: ${e.message}")
}
}
}
}

View File

@ -0,0 +1,26 @@
package net.corda.webserver
import com.pholser.junit.quickcheck.From
import com.pholser.junit.quickcheck.Property
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
import net.corda.core.testing.PublicKeyGenerator
import net.corda.webserver.utilities.JsonSupport
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.junit.runner.RunWith
import java.security.PublicKey
import kotlin.test.assertEquals
@RunWith(JUnitQuickcheck::class)
class JsonSupportTest {
companion object {
val mapper = JsonSupport.createNonRpcMapper()
}
@Property
fun publicKeySerializingWorks(@From(PublicKeyGenerator::class) publicKey: PublicKey) {
val serialized = mapper.writeValueAsString(publicKey)
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
assertEquals(publicKey, parsedKey)
}
}