From 8ad540d1c782d75ca097274eca8ad78131bc7f04 Mon Sep 17 00:00:00 2001 From: Konstantinos Chalkias Date: Thu, 6 Sep 2018 15:48:59 +0100 Subject: [PATCH 1/4] Move identity alias prefixes from DevIdentityGenerator to X509Utilities (#3902) --- .../nodeapi/internal/DevIdentityGenerator.kt | 7 ++----- .../nodeapi/internal/crypto/X509Utilities.kt | 4 ++++ .../net/corda/node/internal/AbstractNode.kt | 17 +++++++++-------- .../NetworkRegistrationHelperTest.kt | 6 +++--- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt index ab840ed387..456db77e9b 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/DevIdentityGenerator.kt @@ -12,6 +12,8 @@ import net.corda.nodeapi.internal.config.SslConfiguration import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX import org.slf4j.LoggerFactory import java.nio.file.Path import java.security.KeyPair @@ -25,11 +27,6 @@ import java.security.PublicKey object DevIdentityGenerator { private val log = LoggerFactory.getLogger(javaClass) - // TODO These don't need to be prefixes but can be the full aliases - // TODO Move these constants out of here as the node needs access to them - const val NODE_IDENTITY_ALIAS_PREFIX = "identity" - const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" - /** Install a node key store for the given node directory using the given legal name. */ fun installKeyStoreWithNodeIdentity(nodeDir: Path, legalName: CordaX500Name): Party { val certificatesDirectory = nodeDir / "certificates" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt index d2efc05c85..23fcce28dd 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/crypto/X509Utilities.kt @@ -48,6 +48,10 @@ object X509Utilities { const val CORDA_CLIENT_TLS = "cordaclienttls" const val CORDA_CLIENT_CA = "cordaclientca" + // TODO These don't need to be prefixes, but can be the full aliases. + const val NODE_IDENTITY_ALIAS_PREFIX = "identity" + const val DISTRIBUTED_NOTARY_ALIAS_PREFIX = "distributed-notary" + val DEFAULT_VALIDITY_WINDOW = Pair(0.millis, 3650.days) /** diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index a43cb8d8ba..a7b81fbedd 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -67,10 +67,11 @@ import net.corda.node.utilities.AffinityExecutor import net.corda.node.utilities.JVMAgentRegistry import net.corda.node.utilities.NamedThreadFactory import net.corda.node.utilities.NodeBuildProperties -import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX +import net.corda.nodeapi.internal.crypto.X509Utilities.NODE_IDENTITY_ALIAS_PREFIX import net.corda.nodeapi.internal.persistence.* import net.corda.nodeapi.internal.storeLegalIdentity import net.corda.tools.shell.InteractiveShell @@ -207,7 +208,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, /** * Completes once the node has successfully registered with the network map service - * or has loaded network map data from local database + * or has loaded network map data from local database. */ val nodeReadyFuture: CordaFuture get() = networkMapCache.nodeReady.map { Unit } @@ -350,7 +351,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } - /** Subclasses must override this to create a [StartedNode] of the desired type, using the provided machinery. */ + /** Subclasses must override this to create a "started" node of the desired type, using the provided machinery. */ abstract fun createStartedNode(nodeInfo: NodeInfo, rpcOps: CordaRPCOps, notaryService: NotaryService?): S private fun verifyCheckpointsCompatible(tokenizableServices: List) { @@ -522,7 +523,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass) /** - * This customizes the ServiceHub for each CordaService that is initiating flows + * This customizes the ServiceHub for each CordaService that is initiating flows. */ // TODO Move this into its own file private class AppServiceHubImpl(private val serviceHub: ServiceHub, private val flowStarter: FlowStarter) : AppServiceHub, ServiceHub by serviceHub { @@ -787,7 +788,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // Meanwhile, we let the remote service send us updates until the acknowledgment buffer overflows and it // unsubscribes us forcibly, rather than blocking the shutdown process. - // Run shutdown hooks in opposite order to starting + // Run shutdown hooks in opposite order to starting. for (toRun in runOnStop.reversed()) { toRun() } @@ -807,11 +808,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val keyStore = configuration.signingCertificateStore.get() val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) { - // Node's main identity or if it's a single node notary - Pair(DevIdentityGenerator.NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) + // Node's main identity or if it's a single node notary. + Pair(NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName) } else { // The node is part of a distributed notary whose identity must already be generated beforehand. - Pair(DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) + Pair(DISTRIBUTED_NOTARY_ALIAS_PREFIX, null) } // TODO: Integrate with Key management service? val privateKeyAlias = "$id-private-key" diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt index 09d61e64a6..33922035e2 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelperTest.kt @@ -16,11 +16,11 @@ import net.corda.core.internal.toX500Name import net.corda.core.utilities.seconds import net.corda.node.NodeRegistrationOption import net.corda.node.services.config.NodeConfiguration -import net.corda.nodeapi.internal.DevIdentityGenerator import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair import net.corda.nodeapi.internal.crypto.CertificateType import net.corda.nodeapi.internal.crypto.X509KeyStore import net.corda.nodeapi.internal.crypto.X509Utilities +import net.corda.nodeapi.internal.crypto.X509Utilities.DISTRIBUTED_NOTARY_ALIAS_PREFIX import net.corda.testing.core.ALICE_NAME import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.internal.createDevIntermediateCaCertPath @@ -168,7 +168,7 @@ class NetworkRegistrationHelperTest { assertThat(config.p2pSslOptions.keyStore.getOptional()).isNull() assertThat(config.p2pSslOptions.trustStore.getOptional()).isNull() - val serviceIdentityAlias = "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key" + val serviceIdentityAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key" nodeKeystore.run { assertFalse(contains(X509Utilities.CORDA_INTERMEDIATE_CA)) @@ -233,7 +233,7 @@ class NetworkRegistrationHelperTest { certService, config.certificatesDirectory / networkRootTrustStoreFileName, networkRootTrustStorePassword, - "${DevIdentityGenerator.DISTRIBUTED_NOTARY_ALIAS_PREFIX}-private-key", + "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key", CertRole.SERVICE_IDENTITY) else -> throw IllegalArgumentException("Unsupported cert role.") } From c6400cf34495e95d375cdfff0a2163648622ea07 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Thu, 6 Sep 2018 16:44:18 +0100 Subject: [PATCH 2/4] Some clean up of behave code (#3907) In particular, fixing the recursive call of the "use" method --- .../net/corda/behave/process/Command.kt | 41 ++++++++----------- .../corda/behave/service/ContainerService.kt | 5 +-- .../net/corda/behave/process/CommandTests.kt | 2 +- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt index ec7c112062..05d08f0bf4 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/process/Command.kt @@ -4,7 +4,7 @@ import net.corda.behave.await import net.corda.behave.file.currentDirectory import net.corda.behave.process.output.OutputListener import net.corda.behave.waitFor -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import rx.Observable @@ -20,8 +20,10 @@ open class Command( private val directory: Path = currentDirectory, private val timeout: Duration = 2.minutes ): Closeable { - - protected val log = loggerFor() + companion object { + private val WAIT_BEFORE_KILL: Duration = 5.seconds + private val log = contextLogger() + } private val terminationLatch = CountDownLatch(1) @@ -36,21 +38,16 @@ open class Command( var exitCode = -1 private set - val output: Observable = Observable.create({ emitter -> + val output: Observable = Observable.create { emitter -> outputListener = object : OutputListener { - override fun onNewLine(line: String) { - emitter.onNext(line) - } - - override fun onEndOfStream() { - emitter.onCompleted() - } + override fun onNewLine(line: String) = emitter.onNext(line) + override fun onEndOfStream() = emitter.onCompleted() } - }).share() + }.share() private val thread = Thread(Runnable { try { - log.info("Command: $command") + log.info("Executing command: $command from directory: $directory") val processBuilder = ProcessBuilder(command) .directory(directory.toFile()) .redirectErrorStream(true) @@ -132,12 +129,13 @@ open class Command( } override fun close() { + if (process?.isAlive == true) { + kill() + } waitFor() } - fun run() = use { _ -> } - - fun use(action: (Command) -> Unit): Int { + fun run(action: (Command) -> Unit = { }): Int { use { start() action(this) @@ -145,8 +143,8 @@ open class Command( return exitCode } - fun use(subscriber: Subscriber, action: (Command, Observable) -> Unit = { _, _ -> }): Int { - use { + fun run(subscriber: Subscriber, action: (Command, Observable) -> Unit = { _, _ -> }): Int { + run { output.subscribe(subscriber) start() action(this, output) @@ -155,11 +153,4 @@ open class Command( } override fun toString() = "Command(${command.joinToString(" ")})" - - companion object { - - private val WAIT_BEFORE_KILL: Duration = 5.seconds - - } - } diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt index 71f52a6c5a..70763d06f1 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/service/ContainerService.kt @@ -54,7 +54,6 @@ abstract class ContainerService( log.info("Container $id info: $info") client.startContainer(id) - true } catch (e: Exception) { id = null @@ -79,9 +78,7 @@ abstract class ContainerService( override fun checkPrerequisites() { if (!client.listImages().any { true == it.repoTags()?.contains(imageReference) }) { log.info("Pulling image $imageReference ...") - client.pull(imageReference, { _ -> - run { } - }) + client.pull(imageReference) { } log.info("Image $imageReference downloaded") } } diff --git a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt index 747c82cdae..88152420dd 100644 --- a/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt +++ b/experimental/behave/src/test/kotlin/net/corda/behave/process/CommandTests.kt @@ -20,7 +20,7 @@ class CommandTests { @Test fun `output stream for command can be observed`() { val subscriber = TestSubscriber() - val exitCode = Command(listOf("ls", "/")).use(subscriber) { _, _ -> + val exitCode = Command(listOf("ls", "/")).run(subscriber) { _, _ -> subscriber.awaitTerminalEvent() subscriber.assertCompleted() subscriber.assertNoErrors() From db6c7f38a554c6f3fbce0800683015e77f61fc39 Mon Sep 17 00:00:00 2001 From: Dan Newton Date: Fri, 7 Sep 2018 17:20:21 +0800 Subject: [PATCH 3/4] Support for case insensitive vault queries (#3853) * Make the criteria builder functions case insensitive Add IGNORE_CASE versions of the comparison operator enums Add exactMatch argument to criteria builder functions where strings can be passed in and set its default value to true Use JvmOverrides to provide the default true version to java without needing to specify a value manually If exactMatch is true then the original enums will be used, if false the IGNORE_CASE enums will be used instead HibernateQueryCriteriaParser.columnPredicateToPredicate now takes into account the IGNORE_CASE versions of the enums * Tidy up QueryCriteriaUtils and HibernateQueryCriteriaParser Split HibernateQueryCriteriaParser.columnPredicateToPredicate into smaller functions Reduce duplicated code in QueryCriteriaUtils * Tidy up QueryCriteriaUtils and HibernateQueryCriteriaParser Split HibernateQueryCriteriaParser.columnPredicateToPredicate into smaller functions Reduce duplicated code in QueryCriteriaUtils (missed some code here) * update changelog and api-vault-query docs with new API functions * reorder Operator enums so that the ignore case enums are at the end In case anyone is depending on the order of the enums, to keep compatibility with existing CorDapps the enums should be added at the end to prevent ordinals from breaking. --- .../node/services/vault/QueryCriteriaUtils.kt | 125 +++++++++++---- docs/source/api-vault-query.rst | 38 +++++ docs/source/changelog.rst | 3 + .../vault/HibernateQueryCriteriaParser.kt | 142 ++++++++++++------ .../node/services/vault/VaultQueryTests.kt | 133 ++++++++++++++++ 5 files changed, 361 insertions(+), 80 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt index fa98e1d54b..faac1f7fef 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteriaUtils.kt @@ -5,6 +5,10 @@ package net.corda.core.node.services.vault import net.corda.core.DoNotImplement import net.corda.core.internal.declaredField import net.corda.core.internal.uncheckedCast +import net.corda.core.node.services.vault.CollectionOperator.* +import net.corda.core.node.services.vault.ColumnPredicate.* +import net.corda.core.node.services.vault.EqualityComparisonOperator.* +import net.corda.core.node.services.vault.LikenessOperator.* import net.corda.core.schemas.PersistentState import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Field @@ -24,7 +28,9 @@ enum class BinaryLogicalOperator : Operator { enum class EqualityComparisonOperator : Operator { EQUAL, - NOT_EQUAL + NOT_EQUAL, + EQUAL_IGNORE_CASE, + NOT_EQUAL_IGNORE_CASE } enum class BinaryComparisonOperator : Operator { @@ -41,12 +47,16 @@ enum class NullOperator : Operator { enum class LikenessOperator : Operator { LIKE, - NOT_LIKE + NOT_LIKE, + LIKE_IGNORE_CASE, + NOT_LIKE_IGNORE_CASE } enum class CollectionOperator : Operator { IN, - NOT_IN + NOT_IN, + IN_IGNORE_CASE, + NOT_IN_IGNORE_CASE } @CordaSerializable @@ -251,27 +261,45 @@ object Builder { fun > Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = info().comparePredicate(operator, value) fun > FieldInfo.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) - fun KProperty1.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) - fun KProperty1.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmOverloads + fun KProperty1.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + + @JvmOverloads + fun KProperty1.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.notEqual(value, exactMatch)) + fun > KProperty1.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) + fun > KProperty1.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > KProperty1.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) + fun > KProperty1.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > KProperty1.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) - fun > KProperty1.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) - fun > KProperty1.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + + @JvmOverloads + fun > KProperty1.`in`(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch)) + + @JvmOverloads + fun > KProperty1.notIn(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.equal(value: R) = info().equal(value) - @JvmStatic - fun FieldInfo.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) + fun Field.equal(value: R, exactMatch: Boolean = true) = info().equal(value, exactMatch) @JvmStatic - @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.notEqual(value: R) = info().notEqual(value) + @JvmOverloads + fun FieldInfo.equal(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) + @JvmStatic - fun FieldInfo.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) + @JvmOverloads + @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") + fun Field.notEqual(value: R, exactMatch: Boolean = true) = info().notEqual(value, exactMatch) + + @JvmStatic + @JvmOverloads + fun FieldInfo.notEqual(value: R, exactMatch: Boolean = true) = predicate(Builder.equal(value, exactMatch)) @JvmStatic @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") @@ -304,44 +332,77 @@ object Builder { fun > FieldInfo.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun > Field.`in`(collection: Collection) = info().`in`(collection) - fun > FieldInfo.`in`(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) + fun > Field.`in`(collection: Collection, exactMatch: Boolean = true) = info().`in`(collection, exactMatch) @JvmStatic - @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun > Field.notIn(collection: Collection) = info().notIn(collection) - @JvmStatic - fun > FieldInfo.notIn(collection: Collection) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) + @JvmOverloads + fun > FieldInfo.`in`(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.`in`(collection, exactMatch)) + + @JvmStatic + @JvmOverloads + @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") + fun > Field.notIn(collection: Collection, exactMatch: Boolean = true) = info().notIn(collection, exactMatch) + + @JvmStatic + @JvmOverloads + fun > FieldInfo.notIn(collection: Collection, exactMatch: Boolean = true) = predicate(Builder.notIn(collection, exactMatch)) + + @JvmOverloads + fun equal(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) EQUAL else EQUAL_IGNORE_CASE, value) + + @JvmOverloads + fun notEqual(value: R, exactMatch: Boolean = true) = EqualityComparison(if (exactMatch) NOT_EQUAL else NOT_EQUAL_IGNORE_CASE, value) - fun equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) - fun notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) fun > lessThan(value: R) = compare(BinaryComparisonOperator.LESS_THAN, value) + fun > lessThanOrEqual(value: R) = compare(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) + fun > greaterThan(value: R) = compare(BinaryComparisonOperator.GREATER_THAN, value) + fun > greaterThanOrEqual(value: R) = compare(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) + fun > between(from: R, to: R) = ColumnPredicate.Between(from, to) - fun > `in`(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection) - fun > notIn(collection: Collection) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) - fun like(string: String) = ColumnPredicate.Likeness(LikenessOperator.LIKE, string) - fun notLike(string: String) = ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string) + + @JvmOverloads + fun > `in`(collection: Collection, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) IN else IN_IGNORE_CASE, collection) + + @JvmOverloads + fun > notIn(collection: Collection, exactMatch: Boolean = true) = CollectionExpression(if (exactMatch) NOT_IN else NOT_IN_IGNORE_CASE, collection) + + @JvmOverloads + fun like(string: String, exactMatch: Boolean = true) = Likeness(if (exactMatch) LIKE else LIKE_IGNORE_CASE, string) + + @JvmOverloads + fun notLike(string: String, exactMatch: Boolean = true) = Likeness(if (exactMatch) NOT_LIKE else NOT_LIKE_IGNORE_CASE, string) + fun isNull() = ColumnPredicate.NullExpression(NullOperator.IS_NULL) fun isNotNull() = ColumnPredicate.NullExpression(NullOperator.NOT_NULL) + @JvmOverloads + fun KProperty1.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch)) - fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.like(string: String) = info().like(string) - @JvmStatic - fun FieldInfo.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) + fun Field.like(string: String, exactMatch: Boolean = true) = info().like(string, exactMatch) - fun KProperty1.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) @JvmStatic + @JvmOverloads + fun FieldInfo.like(string: String, exactMatch: Boolean = true) = predicate(Builder.like(string, exactMatch)) + + @JvmOverloads + fun KProperty1.notLike(string: String, exactMatch: Boolean = true) = predicate(Builder.notLike(string, exactMatch)) + + @JvmStatic + @JvmOverloads @Deprecated("Does not support fields from a MappedSuperclass. Use equivalent on a FieldInfo.") - fun Field.notLike(string: String) = info().notLike(string) + fun Field.notLike(string: String, exactMatch: Boolean = true) = info().notLike(string, exactMatch) + @JvmStatic - fun FieldInfo.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) + @JvmOverloads + fun FieldInfo.notLike(string: String, exactMatch: Boolean = true) = predicate(Builder.notLike(string, exactMatch)) fun KProperty1.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) @JvmStatic diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 1b80d50dcf..a012b84ea7 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -166,6 +166,44 @@ An example of a custom query in Java is illustrated here: where an anonymous party does not resolve to an X500 name via the ``IdentityService``, no query results will ever be produced. For performance reasons, queries do not use ``PublicKey`` as search criteria. +Custom queries can be either case sensitive or case insensitive. They are defined via a ``Boolean`` as one of the function parameters of each operator function. By default each operator is case sensitive. + +An example of a case sensitive custom query operator is illustrated here: + +.. container:: codeset + + .. sourcecode:: kotlin + + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode, true) + +.. note:: The ``Boolean`` input of ``true`` in this example could be removed since the function will default to ``true`` when not provided. + +An example of a case insensitive custom query operator is illustrated here: + +.. container:: codeset + + .. sourcecode:: kotlin + + val currencyIndex = PersistentCashState::currency.equal(USD.currencyCode, false) + +An example of a case sensitive custom query operator in Java is illustrated here: + +.. container:: codeset + + .. sourcecode:: java + + FieldInfo attributeCurrency = getField("currency", CashSchemaV1.PersistentCashState.class); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD", true); + +An example of a case insensitive custom query operator in Java is illustrated here: + +.. container:: codeset + + .. sourcecode:: java + + FieldInfo attributeCurrency = getField("currency", CashSchemaV1.PersistentCashState.class); + CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD", false); + Pagination ---------- The API provides support for paging where large numbers of results are expected (by default, a page size is set to 200 diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 48ab176c8d..b0440cf00f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,9 @@ release, see :doc:`upgrade-notes`. Unreleased ---------- + +* Case insensitive vault queries can be specified via a boolean on applicable SQL criteria builder operators. By default queries will be case sensitive. + * Getter added to ``CordaRPCOps`` for the node's network parameters. * The RPC client library now checks at startup whether the server is of the client libraries major version or higher. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 76c4e2b09d..ec612fdeec 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,6 +7,13 @@ import net.corda.core.internal.uncheckedCast import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.BinaryComparisonOperator.* +import net.corda.core.node.services.vault.CollectionOperator.* +import net.corda.core.node.services.vault.ColumnPredicate.* +import net.corda.core.node.services.vault.EqualityComparisonOperator.* +import net.corda.core.node.services.vault.LikenessOperator.* +import net.corda.core.node.services.vault.NullOperator.IS_NULL +import net.corda.core.node.services.vault.NullOperator.NOT_NULL import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef @@ -54,54 +61,93 @@ abstract class AbstractQueryCriteriaParser, in P: protected fun columnPredicateToPredicate(column: Path, columnPredicate: ColumnPredicate<*>): Predicate { return when (columnPredicate) { - is ColumnPredicate.EqualityComparison -> { - val literal = columnPredicate.rightLiteral - when (columnPredicate.operator) { - EqualityComparisonOperator.EQUAL -> criteriaBuilder.equal(column, literal) - EqualityComparisonOperator.NOT_EQUAL -> criteriaBuilder.notEqual(column, literal) - } - } - is ColumnPredicate.BinaryComparison -> { - val literal: Comparable? = uncheckedCast(columnPredicate.rightLiteral) - @Suppress("UNCHECKED_CAST") - column as Path?> - when (columnPredicate.operator) { - BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) - BinaryComparisonOperator.GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal) - BinaryComparisonOperator.LESS_THAN -> criteriaBuilder.lessThan(column, literal) - BinaryComparisonOperator.LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal) - } - } - is ColumnPredicate.Likeness -> { - @Suppress("UNCHECKED_CAST") - column as Path - when (columnPredicate.operator) { - LikenessOperator.LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral) - LikenessOperator.NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral) - } - } - is ColumnPredicate.CollectionExpression -> { - when (columnPredicate.operator) { - CollectionOperator.IN -> column.`in`(columnPredicate.rightLiteral) - CollectionOperator.NOT_IN -> criteriaBuilder.not(column.`in`(columnPredicate.rightLiteral)) - } - } - is ColumnPredicate.Between -> { - @Suppress("UNCHECKED_CAST") - column as Path?> - val fromLiteral: Comparable? = uncheckedCast(columnPredicate.rightFromLiteral) - val toLiteral: Comparable? = uncheckedCast(columnPredicate.rightToLiteral) - criteriaBuilder.between(column, fromLiteral, toLiteral) - } - is ColumnPredicate.NullExpression -> { - when (columnPredicate.operator) { - NullOperator.IS_NULL -> criteriaBuilder.isNull(column) - NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column) - } - } + is EqualityComparison -> equalityComparisonToPredicate(column, columnPredicate) + is BinaryComparison -> binaryComparisonToPredicate(column, columnPredicate) + is Likeness -> likeComparisonToPredicate(column, columnPredicate) + is CollectionExpression -> collectionComparisonToPredicate(column, columnPredicate) + is Between -> betweenComparisonToPredicate(column, columnPredicate) + is NullExpression -> nullComparisonToPredicate(column, columnPredicate) else -> throw VaultQueryException("Not expecting $columnPredicate") } } + + private fun equalityComparisonToPredicate(column: Path, columnPredicate: EqualityComparison<*>): Predicate { + val literal = columnPredicate.rightLiteral + return if (literal is String) { + @Suppress("UNCHECKED_CAST") + column as Path + when (columnPredicate.operator) { + EQUAL -> criteriaBuilder.equal(column, literal) + EQUAL_IGNORE_CASE -> criteriaBuilder.equal(criteriaBuilder.upper(column), literal.toUpperCase()) + NOT_EQUAL -> criteriaBuilder.notEqual(column, literal) + NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(criteriaBuilder.upper(column), literal.toUpperCase()) + } + } else { + when (columnPredicate.operator) { + EQUAL, EQUAL_IGNORE_CASE -> criteriaBuilder.equal(column, literal) + NOT_EQUAL, NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(column, literal) + } + } + } + + private fun binaryComparisonToPredicate(column: Path, columnPredicate: BinaryComparison<*>): Predicate { + val literal: Comparable? = uncheckedCast(columnPredicate.rightLiteral) + @Suppress("UNCHECKED_CAST") + column as Path?> + return when (columnPredicate.operator) { + GREATER_THAN -> criteriaBuilder.greaterThan(column, literal) + GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal) + LESS_THAN -> criteriaBuilder.lessThan(column, literal) + LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal) + } + } + + private fun likeComparisonToPredicate(column: Path, columnPredicate: Likeness): Predicate { + @Suppress("UNCHECKED_CAST") + column as Path + return when (columnPredicate.operator) { + LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral) + LIKE_IGNORE_CASE -> criteriaBuilder.like(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase()) + NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral) + NOT_LIKE_IGNORE_CASE -> criteriaBuilder.notLike(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase()) + } + } + + private fun collectionComparisonToPredicate(column: Path, columnPredicate: CollectionExpression<*>): Predicate { + val literal = columnPredicate.rightLiteral + return if (literal.any { it is String }) { + @Suppress("UNCHECKED_CAST") + column as Path + @Suppress("UNCHECKED_CAST") + literal as Collection + when (columnPredicate.operator) { + IN -> column.`in`(literal) + IN_IGNORE_CASE -> criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() }) + NOT_IN -> criteriaBuilder.not(column.`in`(literal)) + NOT_IN_IGNORE_CASE -> criteriaBuilder.not(criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() })) + } + } else { + when (columnPredicate.operator) { + IN, IN_IGNORE_CASE -> column.`in`(literal) + NOT_IN, NOT_IN_IGNORE_CASE -> criteriaBuilder.not(column.`in`(literal)) + } + } + } + + private fun betweenComparisonToPredicate(column: Path, columnPredicate: Between<*>): Predicate { + @Suppress("UNCHECKED_CAST") + column as Path?> + val fromLiteral: Comparable? = uncheckedCast(columnPredicate.rightFromLiteral) + val toLiteral: Comparable? = uncheckedCast(columnPredicate.rightToLiteral) + return criteriaBuilder.between(column, fromLiteral, toLiteral) + } + + private fun nullComparisonToPredicate(column: Path, columnPredicate: NullExpression<*>): Predicate { + return when (columnPredicate.operator) { + IS_NULL -> criteriaBuilder.isNull(column) + NOT_NULL -> criteriaBuilder.isNotNull(column) + } + } } class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: CriteriaBuilder, @@ -474,7 +520,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class).values.map { (it as LiteralExpression).literal }.toSet() if (existingTypes != contractStateTypes) { diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 62e1b43023..0d9cbe9f68 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -800,6 +800,139 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test + fun `logical operator case insensitive EQUAL`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal("gBp", false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive EQUAL does not affect numbers`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.equal(10000, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(3) + } + } + + @Test + fun `logical operator case insensitive NOT_EQUAL does not return results containing the same characters as the case insensitive string`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual("gBp", false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator case insensitive NOT_EQUAL does not affect numbers`() { + database.transaction { + listOf(USD, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.notEqual(10000, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive IN`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val currencies = listOf("cHf", "gBp") + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator case insensitive IN does not affect numbers`() { + database.transaction { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, USD), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(200, CHF), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.`in`(listOf(10000L, 20000L), false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + + @Test + fun `logical operator case insensitive NOT IN does not return results containing the same characters as the case insensitive strings`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val currencies = listOf("cHf", "gBp") + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies, false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive NOT_IN does not affect numbers`() { + database.transaction { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, USD), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(200, CHF), notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER) + val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.notIn(listOf(10000L, 20000L), false) } + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator case insensitive LIKE`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%bP", false) } // GPB + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(1) + } + } + + @Test + fun `logical operator NOT LIKE does not return results containing the same characters as the case insensitive string`() { + database.transaction { + listOf(USD, GBP, CHF).forEach { + vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER) + } + val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%bP", false) } // GPB + val criteria = VaultCustomQueryCriteria(logicalExpression) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(2) + } + } + @Test fun `aggregate functions without group clause`() { database.transaction { From 4f8a564104afd52d95fc516d5310296bef90ccb5 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 7 Sep 2018 14:26:35 +0100 Subject: [PATCH 4/4] CORDA-1964: Resolve some Gradle warnings which will soon become errors. (#3911) --- tools/shell-cli/build.gradle | 14 +++++++------- tools/shell/build.gradle | 13 +++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tools/shell-cli/build.gradle b/tools/shell-cli/build.gradle index 45513f5f89..d4d510e4cb 100644 --- a/tools/shell-cli/build.gradle +++ b/tools/shell-cli/build.gradle @@ -2,9 +2,7 @@ description 'Corda Shell CLI' buildscript { repositories { - maven { - url "https://plugins.gradle.org/m2/" - } + gradlePluginPortal() } dependencies { classpath "com.github.jengelman.gradle.plugins:shadow:$shadow_version" @@ -12,15 +10,16 @@ buildscript { } apply plugin: 'application' +// We need to set mainClassName before applying the shadow plugin. +mainClassName = 'net.corda.tools.shell.StandaloneShellKt' + apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'com.jfrog.artifactory' -mainClassName = 'net.corda.tools.shell.StandaloneShellKt' - dependencies { compile project(':tools:shell') - compile group: 'org.slf4j', name: 'slf4j-simple', version: slf4j_version + compile "org.slf4j:slf4j-simple:$slf4j_version" testCompile(project(':test-utils')) { exclude group: 'org.apache.logging.log4j', module: 'log4j-slf4j-impl' @@ -36,10 +35,11 @@ shadowJar { } task buildShellCli(dependsOn: shadowJar) +assemble.dependsOn buildShellCli artifacts { publish shadowJar { - classifier "" + classifier = "" } } diff --git a/tools/shell/build.gradle b/tools/shell/build.gradle index 219fdaa055..13338150fc 100644 --- a/tools/shell/build.gradle +++ b/tools/shell/build.gradle @@ -61,24 +61,25 @@ dependencies { compile "com.jcabi:jcabi-manifests:1.1" // For logging, required for ANSIProgressRenderer. - compile "org.apache.logging.log4j:log4j-core:${log4j_version}" + compile "org.apache.logging.log4j:log4j-core:$log4j_version" // Unit testing helpers. testCompile "junit:junit:$junit_version" - testCompile "org.assertj:assertj-core:${assertj_version}" + testCompile "org.assertj:assertj-core:$assertj_version" testCompile project(':test-utils') testCompile project(':finance') - // Integration test helpers. - integrationTestCompile "junit:junit:$junit_version" - integrationTestCompile "org.assertj:assertj-core:${assertj_version}" - // Jsh: Testing SSH server. integrationTestCompile "com.jcraft:jsch:$jsch_version" integrationTestCompile project(':node-driver') } +tasks.withType(JavaCompile) { + // Resolves a Gradle warning about not scanning for pre-processors. + options.compilerArgs << '-proc:none' +} + task integrationTest(type: Test) { testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath