Merge branch 'release/os/4.5' into nnagy-os-4.5-os-4.6-20200512

This commit is contained in:
nikinagy 2020-05-12 10:26:43 +01:00
commit 52e3030d60
24 changed files with 584 additions and 78 deletions

View File

@ -1,4 +1,4 @@
errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2} errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2}
shortDescription = A CorDapp has been installed multiple times on the same node. shortDescription = A CorDapp was installed multiple times on the same node. This is not permitted and causes the node to shut down.
actionsToFix = Investigate the logs to determine the files with duplicate content, and remove one of them from the cordapps directory. actionsToFix = Investigate the logs to determine the CorDapps with duplicate content, and remove one of them from the 'cordapps' directory. It does not matter which of the CorDapps you choose to remove as their content is identical.
aliases = iw8d4e aliases = iw8d4e

View File

@ -1,3 +1,4 @@
errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2} errorTemplate = The CorDapp (name: {0}, file: {1}) is installed multiple times on the node. The following files correspond to the exact same content: {2}
shortDescription = A CorDapp has been installed multiple times on the same node. shortDescription = A CorDapp was installed multiple times on the same node. This is not permitted and causes the node to shut down.
actionsToFix = Investigate the logs to determine the files with duplicate content, and remove one of them from the cordapps directory. actionsToFix = Investigate the logs to determine the CorDapps with duplicate content, and remove one of them from the 'cordapps' directory. It does not matter which of the CorDapps you choose to remove as their content is identical.
aliases = iw8d4e

View File

@ -1,4 +1,4 @@
errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1. errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1.
shortDescription = A version attribute was specified in the CorDapp manifest with an invalid value. The value must be a whole number, and it must be greater than or equal to 1. shortDescription = A version attribute with an invalid value was specified in the manifest of the CorDapp JAR. The version attribute value must be a whole number that is greater than or equal to 1.
actionsToFix = Investigate the logs to find the invalid attribute, and change the attribute value to be valid (a whole number greater than or equal to 1). actionsToFix = Investigate the logs to find the invalid version attribute, and change its value to a valid one (a whole number greater than or equal to 1).
aliases = aliases =

View File

@ -1,4 +1,4 @@
errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1. errorTemplate = Version identifier ({0}) for attribute {1} must be a whole number starting from 1.
shortDescription = A version attribute was specified in the CorDapp manifest with an invalid value. The value must be a whole number, and it must be greater than or equal to 1. shortDescription = A version attribute with an invalid value was specified in the manifest of the CorDapp JAR. The version attribute value must be a whole number that is greater than or equal to 1.
actionsToFix = Investigate the logs to find the invalid attribute, and change the attribute value to be valid (a whole number greater than or equal to 1). actionsToFix = Investigate the logs to find the invalid version attribute, and change its value to a valid one (a whole number greater than or equal to 1).
aliases = aliases =

View File

@ -1,4 +1,4 @@
errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1. errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1.
shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR. shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR.
actionsToFix = Investigate the logs to find out which version attribute has not been specified, and add that version attribute to the CorDapp manifest. actionsToFix = Investigate the logs to find out which version attribute was not specified, and add that version attribute to the CorDapp manifest.
aliases = aliases =

View File

@ -1,3 +1,4 @@
errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1. errorTemplate = Target versionId attribute {0} not specified. Please specify a whole number starting from 1.
shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR. shortDescription = A required version attribute was not specified in the manifest of the CorDapp JAR.
actionsToFix = Investigate the logs to find out which version attribute has not been specified, and add that version attribute to the CorDapp manifest. actionsToFix = Investigate the logs to find out which version attribute was not specified, and add that version attribute to the CorDapp manifest.
aliases =

View File

@ -1,4 +1,4 @@
errorTemplate = Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database. errorTemplate = Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database.
shortDescription = The node failed to connect to the database on node startup, preventing the node from starting correctly. shortDescription = The node failed to connect to the database on node startup and thus prevented the node from starting correctly.
actionsToFix = This happens either because the database connection has been misconfigured or the database is unreachable. Check that the JDBC URL is configured correctly in your node.conf. If this is correctly configured, then check your database connection. actionsToFix = This happened either because the database connection was misconfigured or because the database was unreachable. Check that the JDBC URL is configured correctly in your 'node.conf' file. If this is correctly configured, then check your database connection.
aliases = aliases =

View File

@ -1,3 +1,4 @@
errorTemplate = Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database. errorTemplate = Could not connect to the database. Please check your JDBC connection URL, or the connectivity to the database.
shortDescription = The node failed to connect to the database on node startup, preventing the node from starting correctly. shortDescription = The node failed to connect to the database on node startup and thus prevented the node from starting correctly.
actionsToFix = This happens either because the database connection has been misconfigured or the database is unreachable. Check that the JDBC URL is configured correctly in your node.conf. If this is correctly configured, then check your database connection. actionsToFix = This happened either because the database connection was misconfigured or because the database was unreachable. Check that the JDBC URL is configured correctly in your 'node.conf' file. If this is correctly configured, then check your database connection.
aliases =

View File

@ -1,4 +1,4 @@
errorTemplate = Could not find the database driver class. Please add it to the 'drivers' folder. errorTemplate = Could not find the database driver class. Please add it to the 'drivers' directory.
shortDescription = The node could not find the driver in the 'drivers' directory. shortDescription = The node could not find the driver in the 'drivers' directory.
actionsToFix = Please ensure that the correct database driver has been placed in the 'drivers' folder. The driver must contain the driver main class specified in 'node.conf'. actionsToFix = Ensure that the 'drivers' directory contains the correct database driver. The driver must contain the driver class as specified in 'node.conf'.
aliases = aliases =

View File

@ -1,3 +1,4 @@
errorTemplate = Could not find the database driver class. Please add it to the 'drivers' folder. errorTemplate = Could not find the database driver class. Please add it to the 'drivers' directory.
shortDescription = The node could not find the driver in the 'drivers' directory. shortDescription = The node could not find the driver in the 'drivers' directory.
actionsToFix = Please ensure that the correct database driver has been placed in the 'drivers' folder. The driver must contain the driver main class specified in 'node.conf'. actionsToFix = Ensure that the 'drivers' directory contains the correct database driver. The driver must contain the driver class as specified in 'node.conf'.
aliases =

View File

@ -1,4 +1,4 @@
errorTemplate = Database password is required for H2 server listening on {0} errorTemplate = A database password is required for H2 server listening on {0}
shortDescription = A password is required to access the H2 server the node is trying to access, and this password is missing. shortDescription = The node is trying to access an H2 server that requires a password, which is missing.
actionsToFix = Add the required password to the 'datasource.password' configuration in 'node.conf'. actionsToFix = Add the required password to the 'datasource.password' configuration section in the 'node.conf' file.
aliases = aliases =

View File

@ -1,3 +1,4 @@
errorTemplate = Database password is required for H2 server listening on {0} errorTemplate = A database password is required for H2 server listening on {0}
shortDescription = A password is required to access the H2 server the node is trying to access, and this password is missing. shortDescription = The node is trying to access an H2 server that requires a password, which is missing.
actionsToFix = Add the required password to the 'datasource.password' configuration in 'node.conf'. actionsToFix = Add the required password to the 'datasource.password' configuration section in the 'node.conf' file.
aliases =

View File

@ -71,6 +71,7 @@ def patchCore = tasks.register('patchCore', Zip) {
exclude 'net/corda/core/crypto/SHA256DigestSupplier.class' exclude 'net/corda/core/crypto/SHA256DigestSupplier.class'
exclude 'net/corda/core/internal/*ToggleField*.class' exclude 'net/corda/core/internal/*ToggleField*.class'
exclude 'net/corda/core/serialization/*SerializationFactory*.class' exclude 'net/corda/core/serialization/*SerializationFactory*.class'
exclude 'net/corda/core/serialization/internal/AttachmentsHolderImpl.class'
exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class' exclude 'net/corda/core/serialization/internal/CheckpointSerializationFactory*.class'
exclude 'net/corda/core/internal/rules/*.class' exclude 'net/corda/core/internal/rules/*.class'
} }

View File

@ -0,0 +1,23 @@
package net.corda.core.serialization.internal
import net.corda.core.contracts.Attachment
import java.net.URL
@Suppress("unused")
private class AttachmentsHolderImpl : AttachmentsHolder {
private val attachments = LinkedHashMap<URL, Pair<URL, Attachment>>()
override val size: Int get() = attachments.size
override fun getKey(key: URL): URL? {
return attachments[key]?.first
}
override fun get(key: URL): Attachment? {
return attachments[key]?.second
}
override fun set(key: URL, value: Attachment) {
attachments[key] = key to value
}
}

View File

@ -1,19 +1,37 @@
package net.corda.coretests.transactions package net.corda.coretests.transactions
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.contracts.CommandData
import net.corda.core.contracts.CommandWithParties
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.PrivacySalt
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.contracts.TransactionState
import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.AbstractAttachment
import net.corda.core.internal.AttachmentTrustCalculator import net.corda.core.internal.AttachmentTrustCalculator
import net.corda.core.internal.createLedgerTransaction
import net.corda.core.internal.declaredField import net.corda.core.internal.declaredField
import net.corda.core.internal.hash import net.corda.core.internal.hash
import net.corda.core.internal.inputStream import net.corda.core.internal.inputStream
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentsClassLoader import net.corda.core.serialization.internal.AttachmentsClassLoader
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.common.internal.testNetworkParameters
import net.corda.node.services.attachments.NodeAttachmentTrustCalculator
import net.corda.testing.contracts.DummyContract
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.internal.ContractJarTestUtils
import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar import net.corda.testing.core.internal.ContractJarTestUtils.signContractJar
import net.corda.testing.internal.TestingNamedCacheFactory import net.corda.testing.internal.TestingNamedCacheFactory
import net.corda.testing.internal.fakeAttachment import net.corda.testing.internal.fakeAttachment
@ -27,10 +45,14 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.net.URL import java.net.URL
import java.nio.file.Path
import java.security.PublicKey
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.fail import kotlin.test.fail
@ -47,8 +69,21 @@ class AttachmentsClassLoaderTests {
it.toByteArray() it.toByteArray()
} }
} }
val ALICE = TestIdentity(ALICE_NAME, 70).party
val BOB = TestIdentity(BOB_NAME, 80).party
val dummyNotary = TestIdentity(DUMMY_NOTARY_NAME, 20)
val DUMMY_NOTARY get() = dummyNotary.party
val PROGRAM_ID: String = "net.corda.testing.contracts.MyDummyContract"
} }
@Rule
@JvmField
val tempFolder = TemporaryFolder()
@Rule
@JvmField
val testSerialization = SerializationEnvironmentRule()
private lateinit var storage: MockAttachmentStorage private lateinit var storage: MockAttachmentStorage
private lateinit var internalStorage: InternalMockAttachmentStorage private lateinit var internalStorage: InternalMockAttachmentStorage
private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator private lateinit var attachmentTrustCalculator: AttachmentTrustCalculator
@ -469,4 +504,93 @@ class AttachmentsClassLoaderTests {
createClassloader(trustedAttachment).use {} createClassloader(trustedAttachment).use {}
} }
@Test(timeout=300_000)
fun `attachment still available in verify after forced gc in verify`() {
tempFolder.root.toPath().let { path ->
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), PROGRAM_ID, DUMMY_NOTARY, constraint = AlwaysAcceptAttachmentConstraint)
val inputs = emptyList<StateAndRef<*>>()
val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB))
val commands = emptyList<CommandWithParties<CommandData>>()
val content = createContractString(PROGRAM_ID)
val contractJarPath = ContractJarTestUtils.makeTestContractJar(path, PROGRAM_ID, content = content)
val attachments = createAttachments(contractJarPath)
val id = SecureHash.randomSHA256()
val timeWindow: TimeWindow? = null
val privacySalt = PrivacySalt()
val transaction = createLedgerTransaction(
inputs,
outputs,
commands,
attachments,
id,
null,
timeWindow,
privacySalt,
testNetworkParameters(),
emptyList(),
isAttachmentTrusted = { true }
)
transaction.verify()
}
}
private fun createContractString(contractName: String, versionSeed: Int = 0): String {
val pkgs = contractName.split(".")
val className = pkgs.last()
val packages = pkgs.subList(0, pkgs.size - 1)
val output = """package ${packages.joinToString(".")};
import net.corda.core.contracts.*;
import net.corda.core.transactions.*;
import java.net.URL;
import java.io.InputStream;
public class $className implements Contract {
private int seed = $versionSeed;
@Override
public void verify(LedgerTransaction tx) throws IllegalArgumentException {
System.gc();
InputStream str = this.getClass().getClassLoader().getResourceAsStream("importantDoc.pdf");
if (str == null) throw new IllegalStateException("Could not find importantDoc.pdf");
}
}
""".trimIndent()
System.out.println(output)
return output
}
private fun createAttachments(contractJarPath: Path) : List<Attachment> {
val attachment = object : AbstractAttachment({contractJarPath.inputStream().readBytes()}, uploader = "app") {
@Suppress("OverridingDeprecatedMember")
override val signers: List<Party> = emptyList()
override val signerKeys: List<PublicKey> = emptyList()
override val size: Int = 1234
override val id: SecureHash = SecureHash.sha256(attachmentData)
}
val contractAttachment = ContractAttachment(attachment, PROGRAM_ID)
return listOf(
object : AbstractAttachment({ISOLATED_CONTRACTS_JAR_PATH.openStream().readBytes()}, uploader = "app") {
@Suppress("OverridingDeprecatedMember")
override val signers: List<Party> = emptyList()
override val signerKeys: List<PublicKey> = emptyList()
override val size: Int = 1234
override val id: SecureHash = SecureHash.sha256(attachmentData)
},
object : AbstractAttachment({fakeAttachment("importantDoc.pdf", "I am a pdf!").inputStream().readBytes()
}, uploader = "app") {
@Suppress("OverridingDeprecatedMember")
override val signers: List<Party> = emptyList()
override val signerKeys: List<PublicKey> = emptyList()
override val size: Int = 1234
override val id: SecureHash = SecureHash.sha256(attachmentData)
},
contractAttachment)
}
} }

View File

@ -5,6 +5,5 @@ Constants for new features that can only be switched on at specific platform ver
The text constant describes the feature and the numeric specifies the platform version the feature is enabled at. The text constant describes the feature and the numeric specifies the platform version the feature is enabled at.
*/ */
object PlatformVersionSwitches { object PlatformVersionSwitches {
const val REMOVE_NO_OVERLAP_RULE_FOR_REFERENCE_DATA_ATTACHMENTS = 7
const val ENABLE_P2P_COMPRESSION = 7 const val ENABLE_P2P_COMPRESSION = 7
} }

View File

@ -302,7 +302,13 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
contractStateTypes: Set<Class<out ContractState>>? = null, contractStateTypes: Set<Class<out ContractState>>? = null,
relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, relevancyStatus) ) : this(participants,
linearId?.map { it.id }.takeIf { it != null && it.isNotEmpty() },
linearId?.mapNotNull { it.externalId }.takeIf { it != null && it.isNotEmpty() },
status,
contractStateTypes,
relevancyStatus
)
// V3 c'tor // V3 c'tor
@DeprecatedConstructorForDeserialization(version = 1) @DeprecatedConstructorForDeserialization(version = 1)

View File

@ -17,6 +17,7 @@ import net.corda.core.utilities.debug
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.lang.ref.WeakReference
import java.net.* import java.net.*
import java.security.Permission import java.security.Permission
import java.util.* import java.util.*
@ -53,14 +54,6 @@ class AttachmentsClassLoader(attachments: List<Attachment>,
private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/") private val ignoreDirectories = listOf("org/jolokia/", "org/json/simple/")
private val ignorePackages = ignoreDirectories.map { it.replace("/", ".") } private val ignorePackages = ignoreDirectories.map { it.replace("/", ".") }
@VisibleForTesting
private fun readAttachment(attachment: Attachment, filepath: String): ByteArray {
ByteArrayOutputStream().use {
attachment.extractFile(filepath, it)
return it.toByteArray()
}
}
/** /**
* Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet, * Apply our custom factory either directly, if `URL.setURLStreamHandlerFactory` has not been called yet,
* or use a decorator and reflection to bypass the single-call-per-JVM restriction otherwise. * or use a decorator and reflection to bypass the single-call-per-JVM restriction otherwise.
@ -359,8 +352,7 @@ object AttachmentsClassLoaderBuilder {
object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory { object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
internal const val attachmentScheme = "attachment" internal const val attachmentScheme = "attachment"
// TODO - what happens if this grows too large? private val loadedAttachments: AttachmentsHolder = AttachmentsHolderImpl()
private val loadedAttachments = mutableMapOf<String, Attachment>().toSynchronised()
override fun createURLStreamHandler(protocol: String): URLStreamHandler? { override fun createURLStreamHandler(protocol: String): URLStreamHandler? {
return if (attachmentScheme == protocol) { return if (attachmentScheme == protocol) {
@ -368,34 +360,79 @@ object AttachmentURLStreamHandlerFactory : URLStreamHandlerFactory {
} else null } else null
} }
@Synchronized
fun toUrl(attachment: Attachment): URL { fun toUrl(attachment: Attachment): URL {
val id = attachment.id.toString() val proposedURL = URL(attachmentScheme, "", -1, attachment.id.toString(), AttachmentURLStreamHandler)
loadedAttachments[id] = attachment val existingURL = loadedAttachments.getKey(proposedURL)
return URL(attachmentScheme, "", -1, id, AttachmentURLStreamHandler) return if (existingURL == null) {
loadedAttachments[proposedURL] = attachment
proposedURL
} else {
existingURL
}
} }
@VisibleForTesting
fun loadedAttachmentsSize(): Int = loadedAttachments.size
private object AttachmentURLStreamHandler : URLStreamHandler() { private object AttachmentURLStreamHandler : URLStreamHandler() {
override fun openConnection(url: URL): URLConnection { override fun openConnection(url: URL): URLConnection {
if (url.protocol != attachmentScheme) throw IOException("Cannot handle protocol: ${url.protocol}") if (url.protocol != attachmentScheme) throw IOException("Cannot handle protocol: ${url.protocol}")
val attachment = loadedAttachments[url.path] ?: throw IOException("Could not load url: $url .") val attachment = loadedAttachments[url] ?: throw IOException("Could not load url: $url .")
return AttachmentURLConnection(url, attachment) return AttachmentURLConnection(url, attachment)
} }
}
private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) { override fun equals(attachmentUrl: URL, otherURL: URL?): Boolean {
override fun getContentLengthLong(): Long = attachment.size.toLong() if (attachmentUrl.protocol != otherURL?.protocol) return false
override fun getInputStream(): InputStream = attachment.open() if (attachmentUrl.protocol != attachmentScheme) throw IllegalArgumentException("Cannot handle protocol: ${attachmentUrl.protocol}")
/** return attachmentUrl.file == otherURL?.file
* Define the permissions that [AttachmentsClassLoader] will need to }
* use this [URL]. The attachment is stored in memory, and so we
* don't need any extra permissions here. But if we don't override override fun hashCode(url: URL): Int {
* [getPermission] then [AttachmentsClassLoader] will assign the if (url.protocol != attachmentScheme) throw IllegalArgumentException("Cannot handle protocol: ${url.protocol}")
* default permission of ALL_PERMISSION to these classes' return url.file.hashCode()
* [java.security.ProtectionDomain]. This would be a security hole!
*/
override fun getPermission(): Permission? = null
override fun connect() {
connected = true
} }
} }
}
interface AttachmentsHolder {
val size: Int
fun getKey(key: URL): URL?
operator fun get(key: URL): Attachment?
operator fun set(key: URL, value: Attachment)
}
private class AttachmentsHolderImpl : AttachmentsHolder {
private val attachments = WeakHashMap<URL, Pair<WeakReference<URL>, Attachment>>().toSynchronised()
override val size: Int get() = attachments.size
override fun getKey(key: URL): URL? {
return attachments[key]?.first?.get()
}
override fun get(key: URL): Attachment? {
return attachments[key]?.second
}
override fun set(key: URL, value: Attachment) {
attachments[key] = WeakReference(key) to value
}
}
private class AttachmentURLConnection(url: URL, private val attachment: Attachment) : URLConnection(url) {
override fun getContentLengthLong(): Long = attachment.size.toLong()
override fun getInputStream(): InputStream = attachment.open()
/**
* Define the permissions that [AttachmentsClassLoader] will need to
* use this [URL]. The attachment is stored in memory, and so we
* don't need any extra permissions here. But if we don't override
* [getPermission] then [AttachmentsClassLoader] will assign the
* default permission of ALL_PERMISSION to these classes'
* [java.security.ProtectionDomain]. This would be a security hole!
*/
override fun getPermission(): Permission? = null
override fun connect() {
connected = true
}
} }

View File

@ -5,13 +5,20 @@ import net.corda.core.contracts.ContractAttachment
import net.corda.core.contracts.ContractClassName import net.corda.core.contracts.ContractClassName
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentId
import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory import net.corda.core.serialization.internal.AttachmentURLStreamHandlerFactory
import net.corda.core.serialization.internal.AttachmentsClassLoader import net.corda.core.serialization.internal.AttachmentsClassLoader
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.lang.ref.ReferenceQueue
import java.lang.ref.WeakReference
import java.net.URL
import java.net.URLClassLoader import java.net.URLClassLoader
import java.security.PublicKey import java.security.PublicKey
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
@ -120,9 +127,125 @@ class ClassLoadingUtilsTest {
} }
} }
private fun signedAttachment(data: ByteArray, vararg parties: Party) = ContractAttachment.create( @Ignore("Using System.gc in this test which has no guarantees when/if gc occurs.")
@Test(timeout=300_000)
@Suppress("ExplicitGarbageCollectionCall", "UNUSED_VALUE")
fun `test weak reference removed from map`() {
val jarData = with(ByteArrayOutputStream()) {
val internalName = STANDALONE_CLASS_NAME.asInternalName
JarOutputStream(this, Manifest()).use {
it.setLevel(NO_COMPRESSION)
it.setMethod(DEFLATED)
it.putNextEntry(directoryEntry("com"))
it.putNextEntry(directoryEntry("com/example"))
it.putNextEntry(classEntry(internalName))
it.write(TemplateClassWithEmptyConstructor::class.java.renameTo(internalName))
}
toByteArray()
}
val attachment = signedAttachment(jarData)
var url: URL? = AttachmentURLStreamHandlerFactory.toUrl(attachment)
val referenceQueue: ReferenceQueue<URL> = ReferenceQueue()
val weakReference = WeakReference<URL>(url, referenceQueue)
assertEquals(1, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
// Clear strong reference
url = null
System.gc()
val ref = referenceQueue.remove(100000)
assertSame(weakReference, ref)
assertEquals(0, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
}
@Ignore("Using System.gc in this test which has no guarantees when/if gc occurs.")
@Test(timeout=300_000)
@Suppress("ExplicitGarbageCollectionCall", "UNUSED_VALUE")
fun `test adding same attachment twice then removing`() {
val jarData = with(ByteArrayOutputStream()) {
val internalName = STANDALONE_CLASS_NAME.asInternalName
JarOutputStream(this, Manifest()).use {
it.setLevel(NO_COMPRESSION)
it.setMethod(DEFLATED)
it.putNextEntry(directoryEntry("com"))
it.putNextEntry(directoryEntry("com/example"))
it.putNextEntry(classEntry(internalName))
it.write(TemplateClassWithEmptyConstructor::class.java.renameTo(internalName))
}
toByteArray()
}
val attachment1 = signedAttachment(jarData)
val attachment2 = signedAttachment(jarData)
var url1: URL? = AttachmentURLStreamHandlerFactory.toUrl(attachment1)
var url2: URL? = AttachmentURLStreamHandlerFactory.toUrl(attachment2)
val referenceQueue1: ReferenceQueue<URL> = ReferenceQueue()
val weakReference1 = WeakReference<URL>(url1, referenceQueue1)
val referenceQueue2: ReferenceQueue<URL> = ReferenceQueue()
val weakReference2 = WeakReference<URL>(url2, referenceQueue2)
assertEquals(1, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
url1 = null
System.gc()
val ref1 = referenceQueue1.remove(500)
assertNull(ref1)
assertEquals(1, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
url2 = null
System.gc()
val ref2 = referenceQueue2.remove(100000)
assertSame(weakReference2, ref2)
assertSame(weakReference1, referenceQueue1.poll())
assertEquals(0, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
}
@Ignore("Using System.gc in this test which has no guarantees when/if gc occurs.")
@Test(timeout=300_000)
@Suppress("ExplicitGarbageCollectionCall", "UNUSED_VALUE")
fun `test adding two different attachments then removing`() {
val jarData1 = with(ByteArrayOutputStream()) {
val internalName = STANDALONE_CLASS_NAME.asInternalName
JarOutputStream(this, Manifest()).use {
it.setLevel(NO_COMPRESSION)
it.setMethod(DEFLATED)
it.putNextEntry(directoryEntry("com"))
it.putNextEntry(directoryEntry("com/example"))
it.putNextEntry(classEntry(internalName))
it.write(TemplateClassWithEmptyConstructor::class.java.renameTo(internalName))
}
toByteArray()
}
val attachment1 = signedAttachment(jarData1)
val attachment2 = signedAttachment(jarData1, id = SecureHash.randomSHA256())
var url1: URL? = AttachmentURLStreamHandlerFactory.toUrl(attachment1)
var url2: URL? = AttachmentURLStreamHandlerFactory.toUrl(attachment2)
val referenceQueue1: ReferenceQueue<URL> = ReferenceQueue()
val weakReference1 = WeakReference<URL>(url1, referenceQueue1)
val referenceQueue2: ReferenceQueue<URL> = ReferenceQueue()
val weakReference2 = WeakReference<URL>(url2, referenceQueue2)
assertEquals(2, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
url1 = null
System.gc()
val ref1 = referenceQueue1.remove(100000)
assertSame(weakReference1, ref1)
assertEquals(1, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
url2 = null
System.gc()
val ref2 = referenceQueue2.remove(100000)
assertSame(weakReference2, ref2)
assertEquals(0, AttachmentURLStreamHandlerFactory.loadedAttachmentsSize())
}
private fun signedAttachment(data: ByteArray, id: AttachmentId = contractAttachmentId,
vararg parties: Party) = ContractAttachment.create(
object : AbstractAttachment({ data }, "test") { object : AbstractAttachment({ data }, "test") {
override val id: SecureHash get() = contractAttachmentId override val id: SecureHash get() = id
override val signerKeys: List<PublicKey> get() = parties.map(Party::owningKey) override val signerKeys: List<PublicKey> get() = parties.map(Party::owningKey)
}, PROGRAM_ID, signerKeys = parties.map(Party::owningKey) }, PROGRAM_ID, signerKeys = parties.map(Party::owningKey)

View File

@ -153,6 +153,18 @@ abstract class AbstractQueryCriteriaParser<Q : GenericQueryCriteria<Q,P>, in P:
NOT_NULL -> criteriaBuilder.isNotNull(column) NOT_NULL -> criteriaBuilder.isNotNull(column)
} }
} }
/**
* Returns the given predicate if the provided `args` list is not empty
* If the list is empty it returns an always false predicate (1=0)
*/
protected fun checkIfListIsEmpty(args: List<Any>, criteriaBuilder: CriteriaBuilder, predicate: Predicate): Predicate {
return if (args.isEmpty()) {
criteriaBuilder.and(criteriaBuilder.equal(criteriaBuilder.literal(1), 0))
} else {
predicate
}
}
} }
class HibernateAttachmentQueryCriteriaParser<T,R>(override val criteriaBuilder: CriteriaBuilder, class HibernateAttachmentQueryCriteriaParser<T,R>(override val criteriaBuilder: CriteriaBuilder,
@ -215,7 +227,14 @@ class HibernateAttachmentQueryCriteriaParser<T,R>(override val criteriaBuilder:
(criteria.contractClassNamesCondition as EqualityComparison<List<ContractClassName>>).rightLiteral (criteria.contractClassNamesCondition as EqualityComparison<List<ContractClassName>>).rightLiteral
else emptyList() else emptyList()
val joinDBAttachmentToContractClassNames = root.joinList<NodeAttachmentService.DBAttachment, ContractClassName>("contractClassNames") val joinDBAttachmentToContractClassNames = root.joinList<NodeAttachmentService.DBAttachment, ContractClassName>("contractClassNames")
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToContractClassNames.`in`(contractClassNames)))
predicateSet.add(
checkIfListIsEmpty(
args = contractClassNames,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(joinDBAttachmentToContractClassNames.`in`(contractClassNames))
)
)
} }
criteria.signersCondition?.let { criteria.signersCondition?.let {
@ -224,7 +243,14 @@ class HibernateAttachmentQueryCriteriaParser<T,R>(override val criteriaBuilder:
(criteria.signersCondition as EqualityComparison<List<PublicKey>>).rightLiteral (criteria.signersCondition as EqualityComparison<List<PublicKey>>).rightLiteral
else emptyList() else emptyList()
val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers") val joinDBAttachmentToSigners = root.joinList<NodeAttachmentService.DBAttachment, PublicKey>("signers")
predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.`in`(signers)))
predicateSet.add(
checkIfListIsEmpty(
args = signers,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(joinDBAttachmentToSigners.`in`(signers))
)
)
} }
criteria.isSignedCondition?.let { isSigned -> criteria.isSignedCondition?.let { isSigned ->
@ -290,14 +316,27 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// notary names // notary names
criteria.notary?.let { criteria.notary?.let {
predicateSet.add(criteriaBuilder.and(vaultStates.get<AbstractParty>("notary").`in`(criteria.notary))) predicateSet.add(
checkIfListIsEmpty(
args = criteria.notary!!,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(vaultStates.get<AbstractParty>("notary").`in`(criteria.notary))
)
)
} }
// state references // state references
criteria.stateRefs?.let { criteria.stateRefs?.let {
val persistentStateRefs = (criteria.stateRefs as List<StateRef>).map(::PersistentStateRef) val persistentStateRefs = (criteria.stateRefs as List<StateRef>).map(::PersistentStateRef)
val compositeKey = vaultStates.get<PersistentStateRef>("stateRef") val compositeKey = vaultStates.get<PersistentStateRef>("stateRef")
predicateSet.add(criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)))
predicateSet.add(
checkIfListIsEmpty(
args = persistentStateRefs,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))
)
)
} }
// time constraints (recorded, consumed) // time constraints (recorded, consumed)
@ -447,7 +486,14 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// owner // owner
criteria.owner?.let { criteria.owner?.let {
val owners = criteria.owner as List<AbstractParty> val owners = criteria.owner as List<AbstractParty>
predicateSet.add(criteriaBuilder.and(vaultFungibleStatesRoot.get<AbstractParty>("owner").`in`(owners)))
predicateSet.add(
checkIfListIsEmpty(
args = owners,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(vaultFungibleStatesRoot.get<AbstractParty>("owner").`in`(owners))
)
)
} }
// quantity // quantity
@ -458,13 +504,27 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// issuer party // issuer party
criteria.issuer?.let { criteria.issuer?.let {
val issuerParties = criteria.issuer as List<AbstractParty> val issuerParties = criteria.issuer as List<AbstractParty>
predicateSet.add(criteriaBuilder.and(vaultFungibleStatesRoot.get<AbstractParty>("issuer").`in`(issuerParties)))
predicateSet.add(
checkIfListIsEmpty(
args = issuerParties,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(vaultFungibleStatesRoot.get<AbstractParty>("issuer").`in`(issuerParties))
)
)
} }
// issuer reference // issuer reference
criteria.issuerRef?.let { criteria.issuerRef?.let {
val issuerRefs = (criteria.issuerRef as List<OpaqueBytes>).map { it.bytes } val issuerRefs = (criteria.issuerRef as List<OpaqueBytes>).map { it.bytes }
predicateSet.add(criteriaBuilder.and(vaultFungibleStatesRoot.get<ByteArray>("issuerRef").`in`(issuerRefs)))
predicateSet.add(
checkIfListIsEmpty(
args = issuerRefs,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(vaultFungibleStatesRoot.get<ByteArray>("issuerRef").`in`(issuerRefs))
)
)
} }
if (criteria.participants != null && criteria.exactParticipants != null) if (criteria.participants != null && criteria.exactParticipants != null)
@ -498,14 +558,27 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// linear ids UUID // linear ids UUID
criteria.uuid?.let { criteria.uuid?.let {
val uuids = criteria.uuid as List<UUID> val uuids = criteria.uuid as List<UUID>
predicateSet.add(criteriaBuilder.and(vaultLinearStatesRoot.get<UUID>("uuid").`in`(uuids)))
predicateSet.add(
checkIfListIsEmpty(
args = uuids,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(vaultLinearStatesRoot.get<UUID>("uuid").`in`(uuids))
)
)
} }
// linear ids externalId // linear ids externalId
criteria.externalId?.let { criteria.externalId?.let {
val externalIds = criteria.externalId as List<String> val externalIds = criteria.externalId as List<String>
if (externalIds.isNotEmpty())
predicateSet.add(criteriaBuilder.and(vaultLinearStatesRoot.get<String>("externalId").`in`(externalIds))) predicateSet.add(
checkIfListIsEmpty(
args = externalIds,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(vaultLinearStatesRoot.get<String>("externalId").`in`(externalIds))
)
)
} }
if (criteria.participants != null && criteria.exactParticipants != null) if (criteria.participants != null && criteria.exactParticipants != null)
@ -694,8 +767,11 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
} }
else { else {
// Get the persistent party entity. // Get the persistent party entity.
commonPredicates[predicateID] = criteriaBuilder.and( commonPredicates[predicateID] = checkIfListIsEmpty(
getPersistentPartyRoot().get<VaultSchemaV1.PersistentParty>("x500Name").`in`(participants)) args = participants,
criteriaBuilder = criteriaBuilder,
predicate = criteriaBuilder.and(getPersistentPartyRoot().get<VaultSchemaV1.PersistentParty>("x500Name").`in`(participants))
)
} }
// Add the join for vault states to persistent entities (if this is not a Fungible nor Linear criteria query) // Add the join for vault states to persistent entities (if this is not a Fungible nor Linear criteria query)

View File

@ -258,6 +258,24 @@ class NodeAttachmentServiceTest {
) )
} }
@Test(timeout=300_000)
fun `AttachmentsQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() {
SelfCleaningDir().use { file ->
val contractJar = makeTestContractJar(file.path, "com.example.MyContract")
contractJar.read { storage.importAttachment(it, "uploaderB", "contract.jar") }
assertEquals(
1,
storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))).size
)
assertEquals(
0,
storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(emptyList()))).size
)
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `contract class, versioning and signing metadata can be used to search`() { fun `contract class, versioning and signing metadata can be used to search`() {
SelfCleaningDir().use { file -> SelfCleaningDir().use { file ->

View File

@ -267,6 +267,31 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
} }
} }
@Test(timeout=300_000)
fun `VaultQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() {
database.transaction {
val states = vaultFiller.fillWithSomeTestLinearStates(1, "TEST")
val stateRefs = states.states.map { it.ref }
val criteria = VaultQueryCriteria(notary = listOf(DUMMY_NOTARY))
val results = vaultService.queryBy<LinearState>(criteria)
assertThat(results.states).hasSize(1)
val emptyCriteria = VaultQueryCriteria(notary = emptyList())
val emptyResults = vaultService.queryBy<LinearState>(emptyCriteria)
assertThat(emptyResults.states).hasSize(0)
val stateCriteria = VaultQueryCriteria(stateRefs = stateRefs)
val stateResults = vaultService.queryBy<LinearState>(stateCriteria)
assertThat(stateResults.states).hasSize(1)
val emptyStateCriteria = VaultQueryCriteria(stateRefs = emptyList())
val emptyStateResults = vaultService.queryBy<LinearState>(emptyStateCriteria)
assertThat(emptyStateResults.states).hasSize(0)
}
}
/** Generic Query tests /** Generic Query tests
(combining both FungibleState and LinearState contract types) */ (combining both FungibleState and LinearState contract types) */
@ -1823,6 +1848,33 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
/** LinearState tests */ /** LinearState tests */
@Test(timeout=300_000)
fun `LinearStateQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() {
database.transaction {
val uid = UniqueIdentifier("999")
vaultFiller.fillWithSomeTestLinearStates(numberToCreate = 1, uniqueIdentifier = uid)
vaultFiller.fillWithSomeTestLinearStates(numberToCreate = 1, externalId = "1234")
val uuidCriteria = LinearStateQueryCriteria(uuid = listOf(uid.id))
val externalIdCriteria = LinearStateQueryCriteria(externalId = listOf("1234"))
val uuidResults = vaultService.queryBy<ContractState>(uuidCriteria)
val externalIdResults = vaultService.queryBy<ContractState>(externalIdCriteria)
assertThat(uuidResults.states).hasSize(1)
assertThat(externalIdResults.states).hasSize(1)
val uuidCriteriaEmpty = LinearStateQueryCriteria(uuid = emptyList())
val externalIdCriteriaEmpty = LinearStateQueryCriteria(externalId = emptyList())
val uuidResultsEmpty = vaultService.queryBy<ContractState>(uuidCriteriaEmpty)
val externalIdResultsEmpty = vaultService.queryBy<ContractState>(externalIdCriteriaEmpty)
assertThat(uuidResultsEmpty.states).hasSize(0)
assertThat(externalIdResultsEmpty.states).hasSize(0)
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `unconsumed linear heads for linearId without external Id`() { fun `unconsumed linear heads for linearId without external Id`() {
database.transaction { database.transaction {
@ -2030,6 +2082,46 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
/** FungibleAsset tests */ /** FungibleAsset tests */
@Test(timeout=300_000)
fun `FungibleAssetQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() {
database.transaction {
vaultFiller.fillWithSomeTestCash(100.DOLLARS, notaryServices, 1, MEGA_CORP.ref(0))
val ownerCriteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP))
val ownerResults = vaultService.queryBy<FungibleAsset<*>>(ownerCriteria)
assertThat(ownerResults.states).hasSize(1)
val emptyOwnerCriteria = FungibleAssetQueryCriteria(owner = emptyList())
val emptyOwnerResults = vaultService.queryBy<FungibleAsset<*>>(emptyOwnerCriteria)
assertThat(emptyOwnerResults.states).hasSize(0)
// Issuer field checks
val issuerCriteria = FungibleAssetQueryCriteria(issuer = listOf(MEGA_CORP))
val issuerResults = vaultService.queryBy<FungibleAsset<*>>(issuerCriteria)
assertThat(issuerResults.states).hasSize(1)
val emptyIssuerCriteria = FungibleAssetQueryCriteria(issuer = emptyList())
val emptyIssuerResults = vaultService.queryBy<FungibleAsset<*>>(emptyIssuerCriteria)
assertThat(emptyIssuerResults.states).hasSize(0)
// Issuer Ref field checks
val issuerRefCriteria = FungibleAssetQueryCriteria(issuerRef = listOf(MINI_CORP.ref(0).reference))
val issuerRefResults = vaultService.queryBy<FungibleAsset<*>>(issuerRefCriteria)
assertThat(issuerRefResults.states).hasSize(1)
val emptyIssuerRefCriteria = FungibleAssetQueryCriteria(issuerRef = emptyList())
val emptyIssuerRefResults = vaultService.queryBy<FungibleAsset<*>>(emptyIssuerRefCriteria)
assertThat(emptyIssuerRefResults.states).hasSize(0)
}
}
@Test(timeout=300_000) @Test(timeout=300_000)
fun `unconsumed fungible assets for specific issuer party and refs`() { fun `unconsumed fungible assets for specific issuer party and refs`() {
database.transaction { database.transaction {

View File

@ -59,12 +59,14 @@ object ContractJarTestUtils {
return workingDir.resolve(jarName) to signer return workingDir.resolve(jarName) to signer
} }
@Suppress("LongParameterList")
@JvmOverloads @JvmOverloads
fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: Int = 1, versionSeed: Int = 0): Path { fun makeTestContractJar(workingDir: Path, contractName: String, signed: Boolean = false, version: Int = 1, versionSeed: Int = 0,
content: String? = null): Path {
val packages = contractName.split(".") val packages = contractName.split(".")
val jarName = "attachment-${packages.last()}-$version-$versionSeed-${(if (signed) "signed" else "")}.jar" val jarName = "attachment-${packages.last()}-$version-$versionSeed-${(if (signed) "signed" else "")}.jar"
val className = packages.last() val className = packages.last()
createTestClass(workingDir, className, packages.subList(0, packages.size - 1), versionSeed) createTestClass(workingDir, className, packages.subList(0, packages.size - 1), versionSeed, content)
workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class") workingDir.createJar(jarName, "${contractName.replace(".", "/")}.class")
workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString())) workingDir.addManifest(jarName, Pair(Attributes.Name(CORDAPP_CONTRACT_VERSION), version.toString()))
return workingDir.resolve(jarName) return workingDir.resolve(jarName)
@ -87,8 +89,8 @@ object ContractJarTestUtils {
return workingDir.resolve(jarName) return workingDir.resolve(jarName)
} }
private fun createTestClass(workingDir: Path, className: String, packages: List<String>, versionSeed: Int = 0): Path { private fun createTestClass(workingDir: Path, className: String, packages: List<String>, versionSeed: Int = 0, content: String? = null): Path {
val newClass = """package ${packages.joinToString(".")}; val newClass = content ?: """package ${packages.joinToString(".")};
import net.corda.core.contracts.*; import net.corda.core.contracts.*;
import net.corda.core.transactions.*; import net.corda.core.transactions.*;

View File

@ -1,4 +1,4 @@
codeHeading = Error Code codeHeading = Error code
aliasesHeading = Aliases aliasesHeading = Aliases
descriptionHeading = Description descriptionHeading = Description
toFixHeading = Actions to Fix toFixHeading = Actions to fix