mirror of
https://github.com/corda/corda.git
synced 2024-12-29 01:08:57 +00:00
Replaced DoormanIntegrationTest with NodeRegistrationTest which does a simple end-to-end registration test using the node driver. (#279)
This commit is contained in:
parent
bbcdb639ab
commit
2832eb489b
@ -1,203 +0,0 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.doReturn
|
|
||||||
import com.nhaarman.mockito_kotlin.whenever
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
|
||||||
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
|
||||||
import net.corda.core.crypto.Crypto
|
|
||||||
import net.corda.core.crypto.SecureHash
|
|
||||||
import net.corda.core.crypto.sign
|
|
||||||
import net.corda.core.identity.PartyAndCertificate
|
|
||||||
import net.corda.core.internal.createDirectories
|
|
||||||
import net.corda.core.internal.div
|
|
||||||
import net.corda.core.node.NodeInfo
|
|
||||||
import net.corda.core.serialization.serialize
|
|
||||||
import net.corda.core.utilities.NetworkHostAndPort
|
|
||||||
import net.corda.core.utilities.minutes
|
|
||||||
import net.corda.core.utilities.seconds
|
|
||||||
import net.corda.node.services.config.NodeConfiguration
|
|
||||||
import net.corda.node.services.network.NetworkMapClient
|
|
||||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
|
||||||
import net.corda.node.utilities.registration.NetworkRegistrationHelper
|
|
||||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
|
||||||
import net.corda.nodeapi.internal.crypto.*
|
|
||||||
import net.corda.testing.ALICE_NAME
|
|
||||||
import net.corda.testing.SerializationEnvironmentRule
|
|
||||||
import net.corda.testing.common.internal.testNetworkParameters
|
|
||||||
import net.corda.testing.internal.rigorousMock
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.rules.TemporaryFolder
|
|
||||||
import java.net.URL
|
|
||||||
import java.security.cert.X509Certificate
|
|
||||||
import java.util.*
|
|
||||||
import javax.security.auth.x500.X500Principal
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNotNull
|
|
||||||
|
|
||||||
class DoormanIntegrationTest {
|
|
||||||
@Rule
|
|
||||||
@JvmField
|
|
||||||
val tempFolder = TemporaryFolder()
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
@JvmField
|
|
||||||
val testSerialization = SerializationEnvironmentRule(true)
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `initial registration`() {
|
|
||||||
val rootCertAndKey = createDoormanRootCertificateAndKeyPair()
|
|
||||||
val intermediateCertAndKey = createDoormanIntermediateCertificateAndKeyPair(rootCertAndKey)
|
|
||||||
|
|
||||||
//Start doorman server
|
|
||||||
val doorman = startDoorman(intermediateCertAndKey, rootCertAndKey.certificate)
|
|
||||||
val doormanHostAndPort = doorman.hostAndPort
|
|
||||||
// Start Corda network registration.
|
|
||||||
val config = createConfig().also {
|
|
||||||
doReturn(URL("http://${doormanHostAndPort.host}:${doormanHostAndPort.port}")).whenever(it).compatibilityZoneURL
|
|
||||||
}
|
|
||||||
config.trustStoreFile.parent.createDirectories()
|
|
||||||
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
|
|
||||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCertAndKey.certificate)
|
|
||||||
it.save(config.trustStoreFile, config.trustStorePassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
|
||||||
|
|
||||||
// Checks the keystore are created with the right certificates and keys.
|
|
||||||
assert(config.nodeKeystore.toFile().exists())
|
|
||||||
assert(config.sslKeystore.toFile().exists())
|
|
||||||
assert(config.trustStoreFile.toFile().exists())
|
|
||||||
|
|
||||||
val intermediateCACert = intermediateCertAndKey.certificate
|
|
||||||
val rootCACert = rootCertAndKey.certificate
|
|
||||||
|
|
||||||
loadKeyStore(config.nodeKeystore, config.keyStorePassword).apply {
|
|
||||||
assert(containsAlias(X509Utilities.CORDA_CLIENT_CA))
|
|
||||||
assertEquals(ALICE_NAME.x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_CA).subjectX500Principal)
|
|
||||||
assertEquals(listOf(intermediateCACert, rootCACert), getCertificateChain(X509Utilities.CORDA_CLIENT_CA).drop(1).toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
loadKeyStore(config.sslKeystore, config.keyStorePassword).apply {
|
|
||||||
assert(containsAlias(X509Utilities.CORDA_CLIENT_TLS))
|
|
||||||
assertEquals(ALICE_NAME.x500Principal, getX509Certificate(X509Utilities.CORDA_CLIENT_TLS).subjectX500Principal)
|
|
||||||
assertEquals(listOf(intermediateCACert, rootCACert), getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).drop(2).toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
loadKeyStore(config.trustStoreFile, config.trustStorePassword).apply {
|
|
||||||
assert(containsAlias(X509Utilities.CORDA_ROOT_CA))
|
|
||||||
assertEquals(rootCACert.subjectX500Principal, getX509Certificate(X509Utilities.CORDA_ROOT_CA).subjectX500Principal)
|
|
||||||
}
|
|
||||||
|
|
||||||
doorman.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `nodeInfo is published to the network map`() {
|
|
||||||
// Given
|
|
||||||
val rootCertAndKey = createDoormanRootCertificateAndKeyPair()
|
|
||||||
val intermediateCertAndKey = createDoormanIntermediateCertificateAndKeyPair(rootCertAndKey)
|
|
||||||
|
|
||||||
//Start doorman server
|
|
||||||
val doorman = startDoorman(intermediateCertAndKey, rootCertAndKey.certificate)
|
|
||||||
val doormanHostAndPort = doorman.hostAndPort
|
|
||||||
|
|
||||||
// Start Corda network registration.
|
|
||||||
val config = createConfig().also {
|
|
||||||
doReturn(URL("http://${doormanHostAndPort.host}:${doormanHostAndPort.port}")).whenever(it).compatibilityZoneURL
|
|
||||||
}
|
|
||||||
config.trustStoreFile.parent.createDirectories()
|
|
||||||
loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword).also {
|
|
||||||
it.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCertAndKey.certificate)
|
|
||||||
it.save(config.trustStoreFile, config.trustStorePassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
NetworkRegistrationHelper(config, HTTPNetworkRegistrationService(config.compatibilityZoneURL!!)).buildKeystore()
|
|
||||||
|
|
||||||
// Publish NodeInfo
|
|
||||||
val networkMapClient = NetworkMapClient(config.compatibilityZoneURL!!, rootCertAndKey.certificate)
|
|
||||||
|
|
||||||
val keyStore = loadKeyStore(config.nodeKeystore, config.keyStorePassword)
|
|
||||||
val clientCertPath = keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA)
|
|
||||||
val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, config.keyStorePassword)
|
|
||||||
val identityKeyPair = Crypto.generateKeyPair()
|
|
||||||
val identityCert = X509Utilities.createCertificate(
|
|
||||||
CertificateType.LEGAL_IDENTITY,
|
|
||||||
clientCA.certificate,
|
|
||||||
clientCA.keyPair,
|
|
||||||
ALICE_NAME.x500Principal,
|
|
||||||
identityKeyPair.public)
|
|
||||||
val certPath = X509CertificateFactory().generateCertPath(identityCert, *clientCertPath)
|
|
||||||
val nodeInfo = NodeInfo(listOf(NetworkHostAndPort("my.company.com", 1234)), listOf(PartyAndCertificate(certPath)), 1, serial = 1L)
|
|
||||||
val nodeInfoBytes = nodeInfo.serialize()
|
|
||||||
|
|
||||||
// When
|
|
||||||
val signedNodeInfo = SignedNodeInfo(nodeInfoBytes, listOf(identityKeyPair.private.sign(nodeInfoBytes.bytes)))
|
|
||||||
networkMapClient.publish(signedNodeInfo)
|
|
||||||
|
|
||||||
// Then
|
|
||||||
val networkMapNodeInfo = networkMapClient.getNodeInfo(nodeInfoBytes.hash)
|
|
||||||
assertNotNull(networkMapNodeInfo)
|
|
||||||
assertEquals(nodeInfo, networkMapNodeInfo)
|
|
||||||
|
|
||||||
doorman.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createConfig(): NodeConfiguration {
|
|
||||||
return rigorousMock<NodeConfiguration>().also {
|
|
||||||
doReturn(tempFolder.root.toPath()).whenever(it).baseDirectory
|
|
||||||
doReturn(ALICE_NAME).whenever(it).myLegalName
|
|
||||||
doReturn(it.baseDirectory / "certificates").whenever(it).certificatesDirectory
|
|
||||||
doReturn(it.certificatesDirectory / "truststore.jks").whenever(it).trustStoreFile
|
|
||||||
doReturn(it.certificatesDirectory / "nodekeystore.jks").whenever(it).nodeKeystore
|
|
||||||
doReturn(it.certificatesDirectory / "sslkeystore.jks").whenever(it).sslKeystore
|
|
||||||
doReturn("trustpass").whenever(it).trustStorePassword
|
|
||||||
doReturn("cordacadevpass").whenever(it).keyStorePassword
|
|
||||||
doReturn("iTest@R3.com").whenever(it).emailAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun createDoormanIntermediateCertificateAndKeyPair(rootCa: CertificateAndKeyPair): CertificateAndKeyPair {
|
|
||||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val intermediateCACert = X509Utilities.createCertificate(
|
|
||||||
CertificateType.INTERMEDIATE_CA,
|
|
||||||
rootCa.certificate,
|
|
||||||
rootCa.keyPair,
|
|
||||||
X500Principal("CN=Integration Test Corda Node Intermediate CA,O=R3 Ltd,L=London,C=GB"),
|
|
||||||
keyPair.public)
|
|
||||||
return CertificateAndKeyPair(intermediateCACert, keyPair)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createDoormanRootCertificateAndKeyPair(): CertificateAndKeyPair {
|
|
||||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
|
||||||
val rootCaCert = X509Utilities.createSelfSignedCACertificate(
|
|
||||||
X500Principal("CN=Integration Test Corda Node Root CA,O=R3 Ltd,L=London,C=GB"),
|
|
||||||
keyPair)
|
|
||||||
return CertificateAndKeyPair(rootCaCert, keyPair)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString()): Properties {
|
|
||||||
val props = Properties()
|
|
||||||
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
|
||||||
props.setProperty("dataSource.url", "jdbc:h2:mem:${nodeName}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
|
|
||||||
props.setProperty("dataSource.user", "sa")
|
|
||||||
props.setProperty("dataSource.password", "")
|
|
||||||
return props
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startDoorman(intermediateCACertAndKey: CertificateAndKeyPair, rootCACert: X509Certificate): NetworkManagementServer {
|
|
||||||
val signer = LocalSigner(intermediateCACertAndKey.keyPair, arrayOf(intermediateCACertAndKey.certificate, rootCACert))
|
|
||||||
//Start doorman server
|
|
||||||
return startDoorman(signer)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startDoorman(localSigner: LocalSigner? = null): NetworkManagementServer {
|
|
||||||
val database = configureDatabase(makeTestDataSourceProperties())
|
|
||||||
//Start doorman server
|
|
||||||
val server = NetworkManagementServer()
|
|
||||||
server.start(NetworkHostAndPort("localhost", 0), database, localSigner, testNetworkParameters(emptyList()), NetworkMapConfig(1.minutes.toMillis(), 1.minutes.toMillis()), DoormanConfig(true, null, 3.seconds.toMillis()))
|
|
||||||
|
|
||||||
return server
|
|
||||||
}
|
|
@ -0,0 +1,166 @@
|
|||||||
|
package com.r3.corda.networkmanage.doorman
|
||||||
|
|
||||||
|
import com.r3.corda.networkmanage.common.persistence.configureDatabase
|
||||||
|
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
|
||||||
|
import net.corda.cordform.CordformNode
|
||||||
|
import net.corda.core.crypto.random63BitValue
|
||||||
|
import net.corda.core.identity.CordaX500Name
|
||||||
|
import net.corda.core.internal.concurrent.transpose
|
||||||
|
import net.corda.core.internal.div
|
||||||
|
import net.corda.core.internal.exists
|
||||||
|
import net.corda.core.internal.list
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.utilities.OpaqueBytes
|
||||||
|
import net.corda.core.utilities.getOrThrow
|
||||||
|
import net.corda.core.utilities.seconds
|
||||||
|
import net.corda.finance.DOLLARS
|
||||||
|
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||||
|
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
|
||||||
|
import net.corda.nodeapi.internal.network.NetworkParameters
|
||||||
|
import net.corda.testing.SerializationEnvironmentRule
|
||||||
|
import net.corda.testing.common.internal.testNetworkParameters
|
||||||
|
import net.corda.testing.driver.NodeHandle
|
||||||
|
import net.corda.testing.driver.PortAllocation
|
||||||
|
import net.corda.testing.internal.IntegrationTest
|
||||||
|
import net.corda.testing.internal.IntegrationTestSchemas
|
||||||
|
import net.corda.testing.internal.createDevIntermediateCaCertPath
|
||||||
|
import net.corda.testing.node.NotarySpec
|
||||||
|
import net.corda.testing.node.internal.CompatibilityZoneParams
|
||||||
|
import net.corda.testing.node.internal.internalDriver
|
||||||
|
import net.corda.testing.singleIdentity
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.*
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
|
// This is the same test as the one in net.corda.node.utilities.registration but using the real doorman and with some
|
||||||
|
// extra checks on the network map.
|
||||||
|
class NodeRegistrationTest : IntegrationTest() {
|
||||||
|
companion object {
|
||||||
|
private val notaryName = CordaX500Name("NotaryService", "Zurich", "CH")
|
||||||
|
private val aliceName = CordaX500Name("Alice", "London", "GB")
|
||||||
|
private val genevieveName = CordaX500Name("Genevieve", "London", "GB")
|
||||||
|
|
||||||
|
@ClassRule
|
||||||
|
@JvmField
|
||||||
|
val databaseSchemas = IntegrationTestSchemas(notaryName.organisation, aliceName.organisation, genevieveName.organisation)
|
||||||
|
|
||||||
|
private val timeoutMillis = 5.seconds.toMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
@JvmField
|
||||||
|
val testSerialization = SerializationEnvironmentRule(true)
|
||||||
|
|
||||||
|
private val portAllocation = PortAllocation.Incremental(10000)
|
||||||
|
private val serverAddress = portAllocation.nextHostAndPort()
|
||||||
|
private val dbId = random63BitValue().toString()
|
||||||
|
|
||||||
|
private lateinit var rootCaCert: X509Certificate
|
||||||
|
private lateinit var intermediateCa: CertificateAndKeyPair
|
||||||
|
|
||||||
|
private var server: NetworkManagementServer? = null
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun init() {
|
||||||
|
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
|
||||||
|
rootCaCert = rootCa.certificate
|
||||||
|
this.intermediateCa = intermediateCa
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
server?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `register nodes with doorman and then they transact with each other`() {
|
||||||
|
// Start the server without the network parameters since we don't have them yet
|
||||||
|
server = startNetworkManagementServer(networkParameters = null)
|
||||||
|
val compatibilityZone = CompatibilityZoneParams(
|
||||||
|
URL("http://$serverAddress"),
|
||||||
|
publishNotaries = { notaryInfos ->
|
||||||
|
// Restart the server once we're able to generate the network parameters
|
||||||
|
server!!.close()
|
||||||
|
server = startNetworkManagementServer(testNetworkParameters(notaryInfos))
|
||||||
|
// Once restarted we delay starting the nodes to make sure the network map server has processed the
|
||||||
|
// network parameters, otherwise the nodes will fail to start as the network parameters won't be
|
||||||
|
// available for them to download.
|
||||||
|
Thread.sleep(2 * timeoutMillis)
|
||||||
|
},
|
||||||
|
rootCert = rootCaCert
|
||||||
|
)
|
||||||
|
|
||||||
|
internalDriver(
|
||||||
|
portAllocation = portAllocation,
|
||||||
|
compatibilityZone = compatibilityZone,
|
||||||
|
initialiseSerialization = false,
|
||||||
|
notarySpecs = listOf(NotarySpec(notaryName)),
|
||||||
|
extraCordappPackagesToScan = listOf("net.corda.finance")
|
||||||
|
) {
|
||||||
|
val (alice, notary) = listOf(
|
||||||
|
startNode(providedName = aliceName),
|
||||||
|
defaultNotaryNode
|
||||||
|
).transpose().getOrThrow()
|
||||||
|
|
||||||
|
alice.onlySeesFromNetworkMap(alice, notary)
|
||||||
|
notary.onlySeesFromNetworkMap(alice, notary)
|
||||||
|
|
||||||
|
val genevieve = startNode(providedName = genevieveName).getOrThrow()
|
||||||
|
|
||||||
|
// Wait for the nodes to poll again
|
||||||
|
Thread.sleep(timeoutMillis * 2)
|
||||||
|
|
||||||
|
// Make sure the new node is visible to everyone
|
||||||
|
alice.onlySeesFromNetworkMap(alice, genevieve, notary)
|
||||||
|
notary.onlySeesFromNetworkMap(alice, genevieve, notary)
|
||||||
|
genevieve.onlySeesFromNetworkMap(alice, genevieve, notary)
|
||||||
|
|
||||||
|
// Check the nodes can communicate among themselves (and the notary).
|
||||||
|
val anonymous = false
|
||||||
|
genevieve.rpc.startFlow(
|
||||||
|
::CashIssueAndPaymentFlow,
|
||||||
|
1000.DOLLARS,
|
||||||
|
OpaqueBytes.of(12),
|
||||||
|
alice.nodeInfo.singleIdentity(),
|
||||||
|
anonymous,
|
||||||
|
defaultNotaryIdentity
|
||||||
|
).returnValue.getOrThrow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun startNetworkManagementServer(networkParameters: NetworkParameters?): NetworkManagementServer {
|
||||||
|
return NetworkManagementServer().apply {
|
||||||
|
start(
|
||||||
|
serverAddress,
|
||||||
|
configureDatabase(makeTestDataSourceProperties()),
|
||||||
|
LocalSigner(intermediateCa.keyPair, arrayOf(intermediateCa.certificate, rootCaCert)),
|
||||||
|
networkParameters,
|
||||||
|
networkParameters?.let { NetworkMapConfig(cacheTimeout = timeoutMillis, signInterval = timeoutMillis) },
|
||||||
|
DoormanConfig(approveAll = true, jiraConfig = null, approveInterval = timeoutMillis)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NodeHandle.onlySeesFromNetworkMap(vararg nodes: NodeHandle) {
|
||||||
|
// Make sure the nodes aren't getting the node infos from their additional directories
|
||||||
|
val nodeInfosDir = configuration.baseDirectory / CordformNode.NODE_INFO_DIRECTORY
|
||||||
|
if (nodeInfosDir.exists()) {
|
||||||
|
assertThat(nodeInfosDir.list { it.toList() }).isEmpty()
|
||||||
|
}
|
||||||
|
assertThat(rpc.networkMapSnapshot()).containsOnlyElementsOf(nodes.map { it.nodeInfo })
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Use the other dbs in the integration tests
|
||||||
|
private fun makeTestDataSourceProperties(): Properties {
|
||||||
|
val props = Properties()
|
||||||
|
props.setProperty("dataSourceClassName", "org.h2.jdbcx.JdbcDataSource")
|
||||||
|
props.setProperty("dataSource.url", "jdbc:h2:mem:${dbId}_persistence;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
|
||||||
|
props.setProperty("dataSource.user", "sa")
|
||||||
|
props.setProperty("dataSource.password", "")
|
||||||
|
return props
|
||||||
|
}
|
||||||
|
}
|
@ -88,6 +88,7 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence, privat
|
|||||||
return database.transaction {
|
return database.transaction {
|
||||||
val builder = session.criteriaBuilder
|
val builder = session.criteriaBuilder
|
||||||
val query = builder.createQuery(NetworkParametersEntity::class.java).run {
|
val query = builder.createQuery(NetworkParametersEntity::class.java).run {
|
||||||
|
// TODO a limit of 1 since we only need the first result
|
||||||
from(NetworkParametersEntity::class.java).run {
|
from(NetworkParametersEntity::class.java).run {
|
||||||
orderBy(builder.desc(get<String>(NetworkParametersEntity::version.name)))
|
orderBy(builder.desc(get<String>(NetworkParametersEntity::version.name)))
|
||||||
}
|
}
|
||||||
@ -102,6 +103,7 @@ class PersistentNetworkMapStorage(private val database: CordaPersistence, privat
|
|||||||
val builder = session.criteriaBuilder
|
val builder = session.criteriaBuilder
|
||||||
val query = builder.createQuery(NetworkMapEntity::class.java).run {
|
val query = builder.createQuery(NetworkMapEntity::class.java).run {
|
||||||
from(NetworkMapEntity::class.java).run {
|
from(NetworkMapEntity::class.java).run {
|
||||||
|
// TODO a limit of 1 since we only need the first result
|
||||||
where(builder.isNotNull(get<ByteArray?>(NetworkMapEntity::signature.name)))
|
where(builder.isNotNull(get<ByteArray?>(NetworkMapEntity::signature.name)))
|
||||||
orderBy(builder.desc(get<String>(NetworkMapEntity::version.name)))
|
orderBy(builder.desc(get<String>(NetworkMapEntity::version.name)))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user