From 1a02c9a74f1398dfdfc23cb336f9480475bed1d8 Mon Sep 17 00:00:00 2001 From: Maksymilian Pawlak <120831+m4ksio@users.noreply.github.com> Date: Tue, 14 Nov 2017 10:22:02 +0000 Subject: [PATCH] AttachmentCriteriaQuery class and infrastructure (#2022) * Attachments metadata support --- .ci/api-current.txt | 117 ++++++++- .../net/corda/core/messaging/CordaRPCOps.kt | 12 +- .../core/node/services/AttachmentStorage.kt | 20 ++ .../core/node/services/vault/QueryCriteria.kt | 83 +++++-- .../node/services/vault/QueryCriteriaUtils.kt | 24 +- docs/source/changelog.rst | 3 + docs/source/tutorial-attachments.rst | 29 +++ .../corda/node/internal/CordaRPCOpsImpl.kt | 26 +- .../node/internal/RpcAuthorisationProxy.kt | 13 +- .../persistence/NodeAttachmentService.kt | 56 ++++- .../vault/HibernateQueryCriteriaParser.kt | 222 ++++++++++++------ .../persistence/NodeAttachmentStorageTest.kt | 96 +++++++- .../testing/node/MockAttachmentStorage.kt | 30 ++- 13 files changed, 593 insertions(+), 138 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index a725671072..f00c85996a 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -1364,12 +1364,14 @@ public final class net.corda.core.identity.IdentityUtils extends java.lang.Objec @org.jetbrains.annotations.NotNull public abstract java.io.InputStream openAttachment(net.corda.core.crypto.SecureHash) @org.jetbrains.annotations.NotNull public abstract Set partiesFromName(String, boolean) @org.jetbrains.annotations.Nullable public abstract net.corda.core.identity.Party partyFromKey(java.security.PublicKey) + @org.jetbrains.annotations.NotNull public abstract List queryAttachments(net.corda.core.node.services.vault.AttachmentQueryCriteria, net.corda.core.node.services.vault.AttachmentSort) @org.jetbrains.annotations.NotNull public abstract List registeredFlows() @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed stateMachineRecordedTransactionMappingFeed() @org.jetbrains.annotations.NotNull public abstract List stateMachineRecordedTransactionMappingSnapshot() @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.messaging.DataFeed stateMachinesFeed() @org.jetbrains.annotations.NotNull public abstract List stateMachinesSnapshot() @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash uploadAttachment(java.io.InputStream) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash uploadAttachmentWithMetadata(java.io.InputStream, String, String) @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQuery(Class) @net.corda.core.messaging.RPCReturnsObservables @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryBy(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.PageSpecification, net.corda.core.node.services.vault.Sort, Class) @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.Vault$Page vaultQueryByCriteria(net.corda.core.node.services.vault.QueryCriteria, Class) @@ -1548,7 +1550,9 @@ public @interface net.corda.core.messaging.RPCReturnsObservables ## @net.corda.core.DoNotImplement public interface net.corda.core.node.services.AttachmentStorage @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.crypto.SecureHash importAttachment(java.io.InputStream, String, String) @org.jetbrains.annotations.Nullable public abstract net.corda.core.contracts.Attachment openAttachment(net.corda.core.crypto.SecureHash) + @org.jetbrains.annotations.NotNull public abstract List queryAttachments(net.corda.core.node.services.vault.AttachmentQueryCriteria, net.corda.core.node.services.vault.AttachmentSort) ## public final class net.corda.core.node.services.AttachmentStorageKt extends java.lang.Object ## @@ -1837,6 +1841,76 @@ public final class net.corda.core.node.services.VaultServiceKt extends java.lang public static net.corda.core.node.services.vault.AggregateFunctionType valueOf(String) public static net.corda.core.node.services.vault.AggregateFunctionType[] values() ## +@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.node.services.vault.AttachmentQueryCriteria extends java.lang.Object implements net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria, net.corda.core.node.services.vault.GenericQueryCriteria + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.AttachmentQueryCriteria and(net.corda.core.node.services.vault.AttachmentQueryCriteria) + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.AttachmentQueryCriteria or(net.corda.core.node.services.vault.AttachmentQueryCriteria) +## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.AttachmentQueryCriteria$AndComposition extends net.corda.core.node.services.vault.AttachmentQueryCriteria implements net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$AndVisitor + public (net.corda.core.node.services.vault.AttachmentQueryCriteria, net.corda.core.node.services.vault.AttachmentQueryCriteria) + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.AttachmentQueryCriteria getA() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.AttachmentQueryCriteria getB() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.AttachmentsQueryCriteriaParser) +## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.AttachmentQueryCriteria$AttachmentsQueryCriteria extends net.corda.core.node.services.vault.AttachmentQueryCriteria + public () + public (net.corda.core.node.services.vault.ColumnPredicate) + public (net.corda.core.node.services.vault.ColumnPredicate, net.corda.core.node.services.vault.ColumnPredicate) + public (net.corda.core.node.services.vault.ColumnPredicate, net.corda.core.node.services.vault.ColumnPredicate, net.corda.core.node.services.vault.ColumnPredicate) + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate component1() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate component2() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate component3() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AttachmentQueryCriteria$AttachmentsQueryCriteria copy(net.corda.core.node.services.vault.ColumnPredicate, net.corda.core.node.services.vault.ColumnPredicate, net.corda.core.node.services.vault.ColumnPredicate) + public boolean equals(Object) + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate getFilenameCondition() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate getUploadDateCondition() + @org.jetbrains.annotations.Nullable public final net.corda.core.node.services.vault.ColumnPredicate getUploaderCondition() + public int hashCode() + public String toString() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.AttachmentsQueryCriteriaParser) +## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.AttachmentQueryCriteria$OrComposition extends net.corda.core.node.services.vault.AttachmentQueryCriteria implements net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$OrVisitor + public (net.corda.core.node.services.vault.AttachmentQueryCriteria, net.corda.core.node.services.vault.AttachmentQueryCriteria) + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.AttachmentQueryCriteria getA() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.AttachmentQueryCriteria getB() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.AttachmentsQueryCriteriaParser) +## +@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.services.vault.AttachmentSort extends net.corda.core.node.services.vault.BaseSort + public (Collection) + @org.jetbrains.annotations.NotNull public final Collection component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AttachmentSort copy(Collection) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final Collection getColumns() + public int hashCode() + public String toString() +## +public static final class net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute extends java.lang.Enum + protected (String, int, String) + @org.jetbrains.annotations.NotNull public final String getColumnName() + public static net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute valueOf(String) + public static net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute[] values() +## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.AttachmentSort$AttachmentSortColumn extends java.lang.Object + public (net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute, net.corda.core.node.services.vault.Sort$Direction) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute component1() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Direction component2() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AttachmentSort$AttachmentSortColumn copy(net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute, net.corda.core.node.services.vault.Sort$Direction) + public boolean equals(Object) + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort$Direction getDirection() + @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.AttachmentSort$AttachmentSortAttribute getSortAttribute() + public int hashCode() + public String toString() +## +public interface net.corda.core.node.services.vault.AttachmentsQueryCriteriaParser extends net.corda.core.node.services.vault.BaseQueryCriteriaParser + @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.AttachmentQueryCriteria$AttachmentsQueryCriteria) +## +public interface net.corda.core.node.services.vault.BaseQueryCriteriaParser + @org.jetbrains.annotations.NotNull public abstract Collection parse(net.corda.core.node.services.vault.GenericQueryCriteria, net.corda.core.node.services.vault.BaseSort) + @org.jetbrains.annotations.NotNull public abstract Collection parseAnd(net.corda.core.node.services.vault.GenericQueryCriteria, net.corda.core.node.services.vault.GenericQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract Collection parseOr(net.corda.core.node.services.vault.GenericQueryCriteria, net.corda.core.node.services.vault.GenericQueryCriteria) +## +public abstract class net.corda.core.node.services.vault.BaseSort extends java.lang.Object + public () +## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.node.services.vault.BinaryComparisonOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator protected (String, int) public static net.corda.core.node.services.vault.BinaryComparisonOperator valueOf(String) @@ -2052,15 +2126,29 @@ public final class net.corda.core.node.services.VaultServiceKt extends java.lang public static net.corda.core.node.services.vault.EqualityComparisonOperator valueOf(String) public static net.corda.core.node.services.vault.EqualityComparisonOperator[] values() ## -@net.corda.core.DoNotImplement public interface net.corda.core.node.services.vault.IQueryCriteriaParser - @org.jetbrains.annotations.NotNull public abstract Collection parse(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) - @org.jetbrains.annotations.NotNull public abstract Collection parseAnd(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) +public interface net.corda.core.node.services.vault.GenericQueryCriteria + @org.jetbrains.annotations.NotNull public abstract Collection visit(net.corda.core.node.services.vault.BaseQueryCriteriaParser) +## +public static interface net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.vault.GenericQueryCriteria and(net.corda.core.node.services.vault.GenericQueryCriteria) + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.vault.GenericQueryCriteria or(net.corda.core.node.services.vault.GenericQueryCriteria) +## +public static interface net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$AndVisitor extends net.corda.core.node.services.vault.GenericQueryCriteria + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.vault.GenericQueryCriteria getA() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.vault.GenericQueryCriteria getB() + @org.jetbrains.annotations.NotNull public abstract Collection visit(net.corda.core.node.services.vault.BaseQueryCriteriaParser) +## +public static interface net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$OrVisitor extends net.corda.core.node.services.vault.GenericQueryCriteria + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.vault.GenericQueryCriteria getA() + @org.jetbrains.annotations.NotNull public abstract net.corda.core.node.services.vault.GenericQueryCriteria getB() + @org.jetbrains.annotations.NotNull public abstract Collection visit(net.corda.core.node.services.vault.BaseQueryCriteriaParser) +## +@net.corda.core.DoNotImplement public interface net.corda.core.node.services.vault.IQueryCriteriaParser extends net.corda.core.node.services.vault.BaseQueryCriteriaParser @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria) @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$FungibleAssetQueryCriteria) @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$LinearStateQueryCriteria) @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$VaultCustomQueryCriteria) @org.jetbrains.annotations.NotNull public abstract Collection parseCriteria(net.corda.core.node.services.vault.QueryCriteria$VaultQueryCriteria) - @org.jetbrains.annotations.NotNull public abstract Collection parseOr(net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) ## @net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public final class net.corda.core.node.services.vault.LikenessOperator extends java.lang.Enum implements net.corda.core.node.services.vault.Operator protected (String, int) @@ -2087,10 +2175,15 @@ public final class net.corda.core.node.services.VaultServiceKt extends java.lang public final boolean isDefault() public String toString() ## -@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.node.services.vault.QueryCriteria extends java.lang.Object - @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria and(net.corda.core.node.services.vault.QueryCriteria) - @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria or(net.corda.core.node.services.vault.QueryCriteria) - @org.jetbrains.annotations.NotNull public abstract Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +@net.corda.core.serialization.CordaSerializable public abstract class net.corda.core.node.services.vault.QueryCriteria extends java.lang.Object implements net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria, net.corda.core.node.services.vault.GenericQueryCriteria + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.QueryCriteria and(net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.QueryCriteria or(net.corda.core.node.services.vault.QueryCriteria) +## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.QueryCriteria$AndComposition extends net.corda.core.node.services.vault.QueryCriteria implements net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$AndVisitor + public (net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.QueryCriteria getA() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.QueryCriteria getB() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) ## @net.corda.core.serialization.CordaSerializable public abstract static class net.corda.core.node.services.vault.QueryCriteria$CommonQueryCriteria extends net.corda.core.node.services.vault.QueryCriteria public () @@ -2151,6 +2244,12 @@ public final class net.corda.core.node.services.VaultServiceKt extends java.lang public String toString() @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) ## +@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.QueryCriteria$OrComposition extends net.corda.core.node.services.vault.QueryCriteria implements net.corda.core.node.services.vault.GenericQueryCriteria$ChainableQueryCriteria$OrVisitor + public (net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.QueryCriteria) + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.QueryCriteria getA() + @org.jetbrains.annotations.NotNull public net.corda.core.node.services.vault.QueryCriteria getB() + @org.jetbrains.annotations.NotNull public Collection visit(net.corda.core.node.services.vault.IQueryCriteriaParser) +## @net.corda.core.serialization.CordaSerializable public static final class net.corda.core.node.services.vault.QueryCriteria$SoftLockingCondition extends java.lang.Object public (net.corda.core.node.services.vault.QueryCriteria$SoftLockingType, List) @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.QueryCriteria$SoftLockingType component1() @@ -2234,7 +2333,7 @@ public final class net.corda.core.node.services.vault.QueryCriteriaUtils extends public static final int DEFAULT_PAGE_SIZE = 200 public static final int MAX_PAGE_SIZE = 2147483647 ## -@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.services.vault.Sort extends java.lang.Object +@net.corda.core.serialization.CordaSerializable public final class net.corda.core.node.services.vault.Sort extends net.corda.core.node.services.vault.BaseSort public (Collection) @org.jetbrains.annotations.NotNull public final Collection component1() @org.jetbrains.annotations.NotNull public final net.corda.core.node.services.vault.Sort copy(Collection) diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index a17b6dcfb7..6b3074564d 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -10,13 +10,11 @@ import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException -import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.* import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Try @@ -222,6 +220,12 @@ interface CordaRPCOps : RPCOps { /** Uploads a jar to the node, returns it's hash. */ fun uploadAttachment(jar: InputStream): SecureHash + /** Uploads a jar including metadata to the node, returns it's hash. */ + fun uploadAttachmentWithMetadata(jar: InputStream, uploader:String, filename:String): SecureHash + + /** Queries attachments metadata */ + fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?): List + /** Returns the node's current time. */ fun currentNodeTime(): Instant diff --git a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt index ed13d5edd5..e7da510f23 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/AttachmentStorage.kt @@ -3,6 +3,8 @@ package net.corda.core.node.services import net.corda.core.DoNotImplement import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash +import net.corda.core.node.services.vault.AttachmentQueryCriteria +import net.corda.core.node.services.vault.AttachmentSort import java.io.IOException import java.io.InputStream import java.nio.file.FileAlreadyExistsException @@ -33,5 +35,23 @@ interface AttachmentStorage { */ @Throws(FileAlreadyExistsException::class, IOException::class) fun importAttachment(jar: InputStream): AttachmentId + + /** + * Inserts the given attachment with additional metadata, see [importAttachment] for input stream handling + * Extra parameters: + * @param uploader Uploader name + * @param filename Name of the file + */ + @Throws(FileAlreadyExistsException::class, IOException::class) + fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId + + /** + * Searches attachment using given criteria and optional sort rules + * @param criteria Query criteria to use as a filter + * @param sorting Sorting definition, if not given, order is undefined + * + * @return List of AttachmentId of attachment matching criteria, sorted according to given sorting parameter + */ + fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort? = null): List } diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 208109f3b4..e1987112cd 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -15,13 +15,38 @@ import java.time.Instant import java.util.* import javax.persistence.criteria.Predicate +interface GenericQueryCriteria, in P : BaseQueryCriteriaParser> { + fun visit(parser: P): Collection + + interface ChainableQueryCriteria, in P : BaseQueryCriteriaParser> { + + interface AndVisitor, in P : BaseQueryCriteriaParser, in S : BaseSort> : GenericQueryCriteria { + val a:Q + val b:Q + override fun visit(parser: P): Collection { + return parser.parseAnd(this.a, this.b) + } + } + + interface OrVisitor, in P : BaseQueryCriteriaParser, in S : BaseSort> : GenericQueryCriteria { + val a:Q + val b:Q + override fun visit(parser: P): Collection { + return parser.parseOr(this.a, this.b) + } + } + + infix fun and(criteria: Q): Q + infix fun or(criteria: Q): Q + } +} + /** * Indexing assumptions: * QueryCriteria assumes underlying schema tables are correctly indexed for performance. */ @CordaSerializable -sealed class QueryCriteria { - abstract fun visit(parser: IQueryCriteriaParser): Collection +sealed class QueryCriteria : GenericQueryCriteria, GenericQueryCriteria.ChainableQueryCriteria { @CordaSerializable data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate) @@ -121,19 +146,6 @@ sealed class QueryCriteria { } } - // enable composition of [QueryCriteria] - private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria) : QueryCriteria() { - override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseAnd(this.a, this.b) - } - } - - private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria) : QueryCriteria() { - override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseOr(this.a, this.b) - } - } - // timestamps stored in the vault states table [VaultSchema.VaultStates] @CordaSerializable enum class TimeInstantType { @@ -141,18 +153,47 @@ sealed class QueryCriteria { CONSUMED } - infix fun and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) - infix fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) + class AndComposition(override val a: QueryCriteria, override val b: QueryCriteria): QueryCriteria(), GenericQueryCriteria.ChainableQueryCriteria.AndVisitor + class OrComposition(override val a: QueryCriteria, override val b: QueryCriteria): QueryCriteria(), GenericQueryCriteria.ChainableQueryCriteria.OrVisitor + + override fun and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria) + override fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria) +} +@CordaSerializable +sealed class AttachmentQueryCriteria : GenericQueryCriteria, GenericQueryCriteria.ChainableQueryCriteria { + /** + * AttachmentsQueryCriteria: + */ + data class AttachmentsQueryCriteria @JvmOverloads constructor (val uploaderCondition: ColumnPredicate? = null, + val filenameCondition: ColumnPredicate? = null, + val uploadDateCondition: ColumnPredicate? = null) : AttachmentQueryCriteria() { + override fun visit(parser: AttachmentsQueryCriteriaParser): Collection { + return parser.parseCriteria(this) + } + } + + class AndComposition(override val a: AttachmentQueryCriteria, override val b: AttachmentQueryCriteria): AttachmentQueryCriteria(), GenericQueryCriteria.ChainableQueryCriteria.AndVisitor + class OrComposition(override val a: AttachmentQueryCriteria, override val b: AttachmentQueryCriteria): AttachmentQueryCriteria(), GenericQueryCriteria.ChainableQueryCriteria.OrVisitor + + override fun and(criteria: AttachmentQueryCriteria): AttachmentQueryCriteria = AndComposition(this, criteria) + override fun or(criteria: AttachmentQueryCriteria): AttachmentQueryCriteria = OrComposition(this, criteria) +} + +interface BaseQueryCriteriaParser, in P: BaseQueryCriteriaParser, in S : BaseSort> { + fun parseOr(left: Q, right: Q): Collection + fun parseAnd(left: Q, right: Q): Collection + fun parse(criteria: Q, sorting: S? = null): Collection } @DoNotImplement -interface IQueryCriteriaParser { +interface IQueryCriteriaParser : BaseQueryCriteriaParser { fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection - fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection - fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection - fun parse(criteria: QueryCriteria, sorting: Sort? = null): Collection +} + +interface AttachmentsQueryCriteriaParser : BaseQueryCriteriaParser{ + fun parseCriteria(criteria: AttachmentQueryCriteria.AttachmentsQueryCriteria): Collection } 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 098a5a07e2..3824a1d55e 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 @@ -127,12 +127,14 @@ data class PageSpecification(val pageNumber: Int = -1, val pageSize: Int = DEFAU val isDefault = (pageSize == DEFAULT_PAGE_SIZE && pageNumber == -1) } +abstract class BaseSort + /** * Sort allows specification of a set of entity attribute names and their associated directionality * and null handling, to be applied upon processing a query specification. */ @CordaSerializable -data class Sort(val columns: Collection) { +data class Sort(val columns: Collection) : BaseSort() { @CordaSerializable enum class Direction { ASC, @@ -177,6 +179,21 @@ data class Sort(val columns: Collection) { val direction: Sort.Direction = Sort.Direction.ASC) } +@CordaSerializable +data class AttachmentSort(val columns: Collection) : BaseSort() { + + enum class AttachmentSortAttribute(val columnName: String) { + INSERTION_DATE("insertion_date"), + UPLOADER("uploader"), + FILENAME("filename") + } + + @CordaSerializable + data class AttachmentSortColumn( + val sortAttribute: AttachmentSortAttribute, + val direction: Sort.Direction = Sort.Direction.ASC) +} + @CordaSerializable sealed class SortAttribute { /** @@ -257,6 +274,11 @@ object Builder { 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) + fun isNull() = ColumnPredicate.NullExpression(NullOperator.IS_NULL) + fun isNotNull() = ColumnPredicate.NullExpression(NullOperator.NOT_NULL) + fun KProperty1.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) @JvmStatic diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index c08ba3e844..9c2e1a4fb5 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -6,6 +6,9 @@ from the previous milestone release. UNRELEASED ---------- +* ``AttachmentStorage`` now allows providing metadata on attachments upload - username and filename, currently as plain + strings. Those can be then used for querying, utilizing ``queryAttachments`` method of the same interface. + * ``CordaRPCOps`` implementation now checks permissions for any function invocation, rather than just when starting flows. * ``OpaqueBytes.bytes`` now returns a clone of its underlying ``ByteArray``, and has been redeclared as ``final``. diff --git a/docs/source/tutorial-attachments.rst b/docs/source/tutorial-attachments.rst index 7047cfb959..cbb88cf92b 100644 --- a/docs/source/tutorial-attachments.rst +++ b/docs/source/tutorial-attachments.rst @@ -25,6 +25,13 @@ is also available for interactive use via the shell. To **upload** run: ``>>> run uploadAttachment jar: /path/to/the/file.jar`` +or + +``>>> run uploadAttachmentWithMetadata jar: /path/to/the/file.jar, uploader: myself, filename: original_name.jar`` + +to include the metadata with the attachment which can be used to find it later on. Note, that currently both uploader +and filename are just plain strings (there is no connection between uploader and the RPC users for example). + The file is uploaded, checked and if successful the hash of the file is returned. This is how the attachment is identified inside the node. @@ -36,6 +43,28 @@ which will then ask you to provide a path to save the file to. To do the same th can pass a simple ``InputStream`` or ``SecureHash`` to the ``uploadAttachment``/``openAttachment`` RPCs from a JVM client. +Searching for attachments +------------------------- + +Attachments metadata can be used to query, in the similar manner as :doc:`api-vault-query`. + +``AttachmentQueryCriteria`` can be used to build a query, utilizing set of operations per column, namely: + +* Binary logical (AND, OR) +* Comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL) +* Equality (EQUAL, NOT_EQUAL) +* Likeness (LIKE, NOT_LIKE) +* Nullability (IS_NULL, NOT_NULL) +* Collection based (IN, NOT_IN) + +``And`` and ``or`` operators can be used to build queries of arbitrary complexity. Example of such query: + +.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt + :language: kotlin + :start-after: DOCSTART AttachmentQueryExample1 + :end-before: DOCEND AttachmentQueryExample1 + :dedent: 12 + Protocol -------- diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index c7330ffbbd..931a260e39 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -13,13 +13,13 @@ import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.messaging.* import net.corda.core.node.NodeInfo +import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.* import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.loggerFor import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.rpcContext @@ -177,6 +177,25 @@ internal class CordaRPCOpsImpl( } } + override fun uploadAttachmentWithMetadata(jar: InputStream, uploader:String, filename:String): SecureHash { + // TODO: this operation should not require an explicit transaction + return database.transaction { + services.attachments.importAttachment(jar, uploader, filename) + } + } + + override fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?): List { + try { + return database.transaction { + services.attachments.queryAttachments(query, sorting) + } + } catch (e: Exception) { + // log and rethrow exception so we keep a copy server side + log.error(e.message) + throw e.cause ?: e + } + } + override fun currentNodeTime(): Instant = Instant.now(services.clock) override fun waitUntilNetworkReady(): CordaFuture = services.networkMapCache.nodeReady @@ -264,5 +283,6 @@ internal class CordaRPCOpsImpl( is StateMachineManager.Change.Removed -> StateMachineUpdate.Removed(change.logic.runId, change.result) } } + private val log = loggerFor() } } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt b/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt index 3e434d3f3a..1f7c0ed011 100644 --- a/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt +++ b/node/src/main/kotlin/net/corda/node/internal/RpcAuthorisationProxy.kt @@ -10,11 +10,10 @@ import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.DataFeed import net.corda.core.messaging.NodeState import net.corda.core.node.NodeInfo +import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.Vault -import net.corda.core.node.services.vault.PageSpecification -import net.corda.core.node.services.vault.QueryCriteria -import net.corda.core.node.services.vault.Sort +import net.corda.core.node.services.vault.* import net.corda.node.services.messaging.RpcContext import net.corda.node.services.messaging.requireEitherPermission import rx.Observable @@ -24,6 +23,14 @@ import java.security.PublicKey // TODO change to KFunction reference after Kotlin fixes https://youtrack.jetbrains.com/issue/KT-12140 class RpcAuthorisationProxy(private val implementation: CordaRPCOps, private val context: () -> RpcContext, private val permissionsAllowing: (methodName: String, args: List) -> Set) : CordaRPCOps { + override fun uploadAttachmentWithMetadata(jar: InputStream, uploader: String, filename: String): SecureHash = guard("uploadAttachmentWithMetadata") { + implementation.uploadAttachmentWithMetadata(jar, uploader, filename) + } + + override fun queryAttachments(query: AttachmentQueryCriteria, sorting: AttachmentSort?): List = guard("queryAttachments") { + implementation.queryAttachments(query, sorting) + } + override fun stateMachinesSnapshot() = guard("stateMachinesSnapshot") { implementation.stateMachinesSnapshot() } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index c95fe2bf94..2a88cd87fe 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -7,19 +7,26 @@ import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream import net.corda.core.CordaRuntimeException +import net.corda.core.contracts.* import net.corda.core.internal.AbstractAttachment -import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash +import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.vault.* import net.corda.core.serialization.* import net.corda.core.utilities.loggerFor +import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser +import net.corda.node.utilities.DatabaseTransactionManager import net.corda.node.utilities.NODE_DATABASE_PREFIX import net.corda.node.utilities.currentDBSession import java.io.* +import java.lang.Exception import java.nio.file.Paths +import java.time.Instant import java.util.jar.JarInputStream import javax.annotation.concurrent.ThreadSafe import javax.persistence.* +import javax.persistence.Column /** * Stores attachments using Hibernate to database. @@ -37,7 +44,16 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single @Column(name = "content") @Lob - var content: ByteArray + var content: ByteArray, + + @Column(name = "insertion_date", nullable = false, updatable = false) + var insertionDate: Instant = Instant.now(), + + @Column(name = "uploader", updatable = false) + var uploader: String? = null, + + @Column(name = "filename", updatable = false) + var filename: String? = null ) : Serializable companion object { @@ -147,8 +163,16 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single return null } + override fun importAttachment(jar: InputStream): AttachmentId { + return import(jar, null, null) + } + + override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId { + return import(jar, uploader, filename) + } + // TODO: PLT-147: The attachment should be randomised to prevent brute force guessing and thus privacy leaks. - override fun importAttachment(jar: InputStream): SecureHash { + private fun import(jar: InputStream, uploader: String?, filename: String?): AttachmentId { require(jar !is JarInputStream) // Read the file into RAM, hashing it to find the ID as we go. The attachment must fit into memory. @@ -169,7 +193,7 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single criteriaQuery.where(criteriaBuilder.equal(attachments.get(DBAttachment::attId.name), id.toString())) val count = session.createQuery(criteriaQuery).singleResult if (count == 0L) { - val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes) + val attachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = bytes, uploader = uploader, filename = filename) session.save(attachment) attachmentCount.inc() @@ -179,6 +203,30 @@ class NodeAttachmentService(metrics: MetricRegistry) : AttachmentStorage, Single return id } + override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { + log.info("Attachment query criteria: $criteria, sorting: $sorting") + + val session = DatabaseTransactionManager.current().session + val criteriaBuilder = session.criteriaBuilder + + val criteriaQuery = criteriaBuilder.createQuery(DBAttachment::class.java) + val root = criteriaQuery.from(DBAttachment::class.java) + + val criteriaParser = HibernateAttachmentQueryCriteriaParser(criteriaBuilder, criteriaQuery, root) + + // parse criteria and build where predicates + criteriaParser.parse(criteria, sorting) + + // prepare query for execution + val query = session.createQuery(criteriaQuery) + + // execution + val results = query.resultList + + return results.map { AttachmentId.parse(it.attId) } + } + + private fun checkIsAValidJAR(stream: InputStream) { // Just iterate over the entries with verification enabled: should be good enough to catch mistakes. // Note that JarInputStream won't throw any kind of error at all if the file stream is in fact not 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 7b8816b361..8f26161b98 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 @@ -14,19 +14,161 @@ import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.loggerFor import net.corda.core.utilities.toHexString import net.corda.core.utilities.trace +import net.corda.node.services.persistence.NodeAttachmentService import org.hibernate.query.criteria.internal.expression.LiteralExpression import org.hibernate.query.criteria.internal.predicate.ComparisonPredicate import org.hibernate.query.criteria.internal.predicate.InPredicate +import java.time.Instant import java.util.* import javax.persistence.Tuple import javax.persistence.criteria.* +abstract class AbstractQueryCriteriaParser, in P: BaseQueryCriteriaParser, in S: BaseSort> : BaseQueryCriteriaParser { + + abstract val criteriaBuilder: CriteriaBuilder + + override fun parseOr(left: Q, right: Q): Collection { + val predicateSet = mutableSetOf() + val leftPredicates = parse(left) + val rightPredicates = parse(right) + + val orPredicate = criteriaBuilder.or(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) + predicateSet.add(orPredicate) + + return predicateSet + } + + override fun parseAnd(left: Q, right: Q): Collection { + val predicateSet = mutableSetOf() + val leftPredicates = parse(left) + val rightPredicates = parse(right) + + val andPredicate = criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) + predicateSet.add(andPredicate) + + return predicateSet + } + + 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) + } + } + else -> throw VaultQueryException("Not expecting $columnPredicate") + } + } +} + +class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: CriteriaBuilder, + private val criteriaQuery: CriteriaQuery, val root: Root) : + AbstractQueryCriteriaParser(), AttachmentsQueryCriteriaParser { + + private companion object { + val log = loggerFor() + } + + init { + criteriaQuery.select(root) + } + + override fun parse(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): Collection { + val predicateSet = criteria.visit(this) + + sorting?.let { + if (sorting.columns.isNotEmpty()) + parse(sorting) + } + + criteriaQuery.where(*predicateSet.toTypedArray()) + + return predicateSet + } + + private fun parse(sorting: AttachmentSort) { + log.trace { "Parsing sorting specification: $sorting" } + + val orderCriteria = mutableListOf() + + sorting.columns.map { (sortAttribute, direction) -> + when (direction) { + Sort.Direction.ASC -> orderCriteria.add(criteriaBuilder.asc(root.get(sortAttribute.columnName))) + Sort.Direction.DESC -> orderCriteria.add(criteriaBuilder.desc(root.get(sortAttribute.columnName))) + } + } + if (orderCriteria.isNotEmpty()) { + criteriaQuery.orderBy(orderCriteria) + } + } + + override fun parseCriteria(criteria: AttachmentQueryCriteria.AttachmentsQueryCriteria): Collection { + log.trace { "Parsing AttachmentsQueryCriteria: $criteria" } + + val predicateSet = mutableSetOf() + + criteria.filenameCondition?.let { + predicateSet.add(columnPredicateToPredicate(root.get("filename"), it)) + } + + criteria.uploaderCondition?.let { + predicateSet.add(columnPredicateToPredicate(root.get("uploader"), it)) + } + + criteria.uploadDateCondition?.let { + predicateSet.add(columnPredicateToPredicate(root.get("upload_date"), it)) + } + + return predicateSet + } +} + class HibernateQueryCriteriaParser(val contractStateType: Class, val contractStateTypeMappings: Map>, - val criteriaBuilder: CriteriaBuilder, + override val criteriaBuilder: CriteriaBuilder, val criteriaQuery: CriteriaQuery, - val vaultStates: Root) : IQueryCriteriaParser { + val vaultStates: Root) : AbstractQueryCriteriaParser(), IQueryCriteriaParser { private companion object { val log = loggerFor() } @@ -102,56 +244,6 @@ class HibernateQueryCriteriaParser(val contractStateType: Class, 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) - } - } - else -> throw VaultQueryException("Not expecting $columnPredicate") - } - } private fun parseExpression(entityRoot: Root, expression: CriteriaExpression, predicateSet: MutableSet) { if (expression is CriteriaExpression.AggregateFunctionExpression) { @@ -326,32 +418,6 @@ class HibernateQueryCriteriaParser(val contractStateType: Class { - log.trace { "Parsing OR QueryCriteria composition: $left OR $right" } - - val predicateSet = mutableSetOf() - val leftPredicates = parse(left) - val rightPredicates = parse(right) - - val orPredicate = criteriaBuilder.or(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) - predicateSet.add(orPredicate) - - return predicateSet - } - - override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection { - log.trace { "Parsing AND QueryCriteria composition: $left AND $right" } - - val predicateSet = mutableSetOf() - val leftPredicates = parse(left) - val rightPredicates = parse(right) - - val andPredicate = criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) - predicateSet.add(andPredicate) - - return predicateSet - } - override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection { val predicateSet = criteria.visit(this) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index a8a352cbf1..003ba0f33e 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -9,6 +9,10 @@ import net.corda.core.internal.read import net.corda.core.internal.readAll import net.corda.core.internal.write import net.corda.core.internal.writeLines +import net.corda.core.node.services.vault.AttachmentQueryCriteria +import net.corda.core.node.services.vault.AttachmentSort +import net.corda.core.node.services.vault.Builder +import net.corda.core.node.services.vault.Sort import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.CordaPersistence import net.corda.node.utilities.configureDatabase @@ -51,8 +55,7 @@ class NodeAttachmentStorageTest { @Test fun `insert and retrieve`() { - val testJar = makeTestJar() - val expectedHash = testJar.readAll().sha256() + val (testJar,expectedHash) = makeTestJar() database.transaction { val storage = NodeAttachmentService(MetricRegistry()) @@ -77,10 +80,87 @@ class NodeAttachmentStorageTest { } } + @Test + fun `metadata can be used to search`() { + val (jarA,hashA) = makeTestJar() + val (jarB,hashB) = makeTestJar(listOf(Pair("file","content"))) + val (jarC,hashC) = makeTestJar(listOf(Pair("magic_file","magic_content_puff"))) + + database.transaction { + val storage = NodeAttachmentService(MetricRegistry()) + + jarA.read { storage.importAttachment(it) } + jarB.read { storage.importAttachment(it, "uploaderB", "fileB.zip") } + jarC.read { storage.importAttachment(it, "uploaderC", "fileC.zip") } + + assertEquals( + listOf(hashB), + storage.queryAttachments( AttachmentQueryCriteria.AttachmentsQueryCriteria( Builder.equal("uploaderB"))) + ) + + assertEquals ( + listOf(hashB, hashC), + storage.queryAttachments( AttachmentQueryCriteria.AttachmentsQueryCriteria( Builder.like ("%uploader%"))) + ) + } + } + + @Test + fun `sorting and compound conditions work`() { + val (jarA,hashA) = makeTestJar(listOf(Pair("a","a"))) + val (jarB,hashB) = makeTestJar(listOf(Pair("b","b"))) + val (jarC,hashC) = makeTestJar(listOf(Pair("c","c"))) + + fun uploaderCondition(s:String) = AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal(s)) + fun filenamerCondition(s:String) = AttachmentQueryCriteria.AttachmentsQueryCriteria(filenameCondition = Builder.equal(s)) + + fun filenameSort(direction: Sort.Direction) = AttachmentSort(listOf(AttachmentSort.AttachmentSortColumn(AttachmentSort.AttachmentSortAttribute.FILENAME, direction))) + + database.transaction { + val storage = NodeAttachmentService(MetricRegistry()) + + jarA.read { storage.importAttachment(it, "complexA", "archiveA.zip") } + jarB.read { storage.importAttachment(it, "complexB", "archiveB.zip") } + jarC.read { storage.importAttachment(it, "complexC", "archiveC.zip") } + + // DOCSTART AttachmentQueryExample1 + + assertEquals( + emptyList(), + storage.queryAttachments( + AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA")) + .and(AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB")))) + ) + + assertEquals( + listOf(hashA, hashB), + storage.queryAttachments( + + AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexA")) + .or(AttachmentQueryCriteria.AttachmentsQueryCriteria(uploaderCondition = Builder.equal("complexB")))) + ) + + val complexCondition = + (uploaderCondition("complexB").and(filenamerCondition("archiveB.zip"))).or(filenamerCondition("archiveC.zip")) + + // DOCEND AttachmentQueryExample1 + + assertEquals ( + listOf(hashB, hashC), + storage.queryAttachments(complexCondition, sorting = filenameSort(Sort.Direction.ASC)) + ) + assertEquals ( + listOf(hashC, hashB), + storage.queryAttachments(complexCondition, sorting = filenameSort(Sort.Direction.DESC)) + ) + + } + } + @Ignore("We need to be able to restart nodes - make importing attachments idempotent?") @Test fun `duplicates not allowed`() { - val testJar = makeTestJar() + val (testJar,_) = makeTestJar() database.transaction { val storage = NodeAttachmentService(MetricRegistry()) testJar.read { @@ -96,7 +176,7 @@ class NodeAttachmentStorageTest { @Test fun `corrupt entry throws exception`() { - val testJar = makeTestJar() + val (testJar,_) = makeTestJar() val id = database.transaction { val storage = NodeAttachmentService(MetricRegistry()) val id = testJar.read { storage.importAttachment(it) } @@ -139,7 +219,7 @@ class NodeAttachmentStorageTest { } private var counter = 0 - private fun makeTestJar(): Path { + private fun makeTestJar(extraEntries: List> = emptyList()): Pair { counter++ val file = fs.getPath("$counter.jar") file.write { @@ -149,8 +229,12 @@ class NodeAttachmentStorageTest { jar.closeEntry() jar.putNextEntry(JarEntry("test2.txt")) jar.write("Some more useful content".toByteArray()) + extraEntries.forEach { + jar.putNextEntry(JarEntry(it.first)) + jar.write(it.second.toByteArray()) + } jar.closeEntry() } - return file + return Pair(file, file.readAll().sha256()) } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt index 2ca0313088..0550e1722e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/node/MockAttachmentStorage.kt @@ -4,7 +4,10 @@ import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.internal.AbstractAttachment +import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage +import net.corda.core.node.services.vault.AttachmentQueryCriteria +import net.corda.core.node.services.vault.AttachmentSort import net.corda.core.serialization.SingletonSerializeAsToken import java.io.ByteArrayOutputStream import java.io.InputStream @@ -12,16 +15,8 @@ import java.util.HashMap import java.util.jar.JarInputStream class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { - val files = HashMap() - override fun openAttachment(id: SecureHash): Attachment? { - val f = files[id] ?: return null - return object : AbstractAttachment({ f }) { - override val id = id - } - } - - override fun importAttachment(jar: InputStream): SecureHash { + override fun importAttachment(jar: InputStream): AttachmentId { // JIS makes read()/readBytes() return bytes of the current file, but we want to hash the entire container here. require(jar !is JarInputStream) @@ -37,4 +32,21 @@ class MockAttachmentStorage : AttachmentStorage, SingletonSerializeAsToken() { } return sha256 } + + override fun importAttachment(jar: InputStream, uploader: String, filename: String): AttachmentId { + return importAttachment(jar) + } + + val files = HashMap() + + override fun openAttachment(id: SecureHash): Attachment? { + val f = files[id] ?: return null + return object : AbstractAttachment({ f }) { + override val id = id + } + } + + override fun queryAttachments(criteria: AttachmentQueryCriteria, sorting: AttachmentSort?): List { + throw NotImplementedError("Querying for attachments not implemented") + } }