From 5423dd2a3dd7adc6d0f6ce2b14761a4e22044dfe Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Tue, 17 Sep 2019 08:54:30 +0300 Subject: [PATCH 1/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 79fa4cf1..b712501c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - +[![DOI](https://zenodo.org/badge/148831678.svg)](https://zenodo.org/badge/latestdoi/148831678) # Questions and Answers # From d10bd40763605d733e94ac35234e2e4403e5a108 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 13 Nov 2019 18:24:33 +0300 Subject: [PATCH 2/5] Remove use of direct Input to ByteArray conversions due to bugs in kotlinx.io. --- build.gradle.kts | 2 +- .../src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt | 4 +++- .../kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt | 6 +++--- .../kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7f190853..430fcda4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("scientifik.publish") version "0.2.2" apply false } -val dataforgeVersion by extra("0.1.5-dev-2") +val dataforgeVersion by extra("0.1.5-dev-3") val bintrayRepo by extra("dataforge") val githubProject by extra("dataforge-core") diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt index bd1e54f4..e5497365 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt @@ -79,8 +79,10 @@ inline fun buildPacketWithoutPool(headerSizeHint: Int = 0, block: BytePacketBuil } fun IOFormat.writePacket(obj: T): ByteReadPacket = buildPacket { writeObject(obj) } -//TODO Double buffer copy. fix all that with IO-2 + +@Deprecated("Not to be used outside tests due to double buffer write") fun IOFormat.writeBytes(obj: T): ByteArray = buildPacket { writeObject(obj) }.readBytes() +@Deprecated("Not to be used outside tests due to double buffer write") fun IOFormat.readBytes(array: ByteArray): T = buildPacket { writeFully(array) }.readObject() object DoubleIOFormat : IOFormat, IOFormatFactory { diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt index a95b7bfb..a461d257 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt @@ -38,15 +38,15 @@ class TaggedEnvelopeFormat( override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) { val metaFormat = metaFormatFactory.invoke(formatMeta, io.context) - val metaBytes = metaFormat.writeBytes(envelope.meta) + val metaBytes = metaFormat.writePacket(envelope.meta) val actualSize: ULong = if (envelope.data == null) { 0u } else { envelope.data?.size ?: ULong.MAX_VALUE } - val tag = Tag(metaFormatFactory.key, metaBytes.size.toUInt() + 2u, actualSize) + val tag = Tag(metaFormatFactory.key, metaBytes.remaining.toUInt() + 2u, actualSize) writePacket(tag.toBytes()) - writeFully(metaBytes) + writePacket(metaBytes) writeText("\r\n") envelope.data?.read { copyTo(this@writeEnvelope) } flush() diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt index d3953a0c..5c4b7dae 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt @@ -37,10 +37,10 @@ class TaglessEnvelopeFormat( //Printing meta if (!envelope.meta.isEmpty()) { - val metaBytes = metaFormat.writeBytes(envelope.meta) - writeProperty(META_LENGTH_PROPERTY, metaBytes.size) + val metaBytes = metaFormat.writePacket(envelope.meta) + writeProperty(META_LENGTH_PROPERTY, metaBytes.remaining) writeText(metaStart + "\r\n") - writeFully(metaBytes) + writePacket(metaBytes) writeText("\r\n") } From 4370a66164d728a2293f2345afedf6bf304cd920 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Wed, 13 Nov 2019 21:12:19 +0300 Subject: [PATCH 3/5] Fixed TaglessEnvelopeFormat resolution --- .../kotlin/hep/dataforge/io/EnvelopeParts.kt | 35 ++++++++++++++- .../kotlin/hep/dataforge/io/IOPlugin.kt | 2 +- .../hep/dataforge/io/TaglessEnvelopeFormat.kt | 3 +- .../jvmMain/kotlin/hep/dataforge/io/fileIO.kt | 45 ++++++++++++------- .../hep/dataforge/io/FileEnvelopeTest.kt | 23 +++++++--- 5 files changed, 82 insertions(+), 26 deletions(-) diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt index eb1dd696..d1b86195 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt @@ -3,6 +3,7 @@ package hep.dataforge.io import hep.dataforge.context.Global import hep.dataforge.io.EnvelopeParts.FORMAT_META_KEY import hep.dataforge.io.EnvelopeParts.FORMAT_NAME_KEY +import hep.dataforge.io.EnvelopeParts.INDEX_KEY import hep.dataforge.io.EnvelopeParts.MULTIPART_DATA_TYPE import hep.dataforge.io.EnvelopeParts.SIZE_KEY import hep.dataforge.meta.* @@ -13,6 +14,7 @@ import hep.dataforge.names.toName object EnvelopeParts { val MULTIPART_KEY = "multipart".asName() val SIZE_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "size" + val INDEX_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "index" val FORMAT_NAME_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "format" val FORMAT_META_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "meta" @@ -37,8 +39,37 @@ fun EnvelopeBuilder.multipart(format: EnvelopeFormatFactory, envelopes: Collecti } } -fun EnvelopeBuilder.multipart(formatFactory: EnvelopeFormatFactory, builder: suspend SequenceScope.() -> Unit) = - multipart(formatFactory, sequence(builder).toList()) +/** + * Create a multipart partition in the envelope adding additional name-index mapping in meta + */ +@DFExperimental +fun EnvelopeBuilder.multipart(format: EnvelopeFormatFactory, envelopes: Map) { + dataType = MULTIPART_DATA_TYPE + meta { + SIZE_KEY put envelopes.size + FORMAT_NAME_KEY put format.name.toString() + } + data { + format.run { + var counter = 0 + envelopes.forEach {(key, envelope)-> + writeObject(envelope) + meta{ + append(INDEX_KEY, buildMeta { + "key" put key + "index" put counter + }) + } + counter++ + } + } + } +} + +fun EnvelopeBuilder.multipart( + formatFactory: EnvelopeFormatFactory, + builder: suspend SequenceScope.() -> Unit +) = multipart(formatFactory, sequence(builder).toList()) /** * If given envelope supports multipart data, return a sequence of those parts (could be empty). Otherwise return null. diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt index ae88e4f4..a463a053 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt @@ -52,7 +52,7 @@ class IOPlugin(meta: Meta) : AbstractPlugin(meta) { companion object : PluginFactory { val defaultMetaFormats: List = listOf(JsonMetaFormat, BinaryMetaFormat) - val defaultEnvelopeFormats = listOf(TaggedEnvelopeFormat) + val defaultEnvelopeFormats = listOf(TaggedEnvelopeFormat, TaglessEnvelopeFormat) override val tag: PluginTag = PluginTag("io", group = PluginTag.DATAFORGE_GROUP) diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt index 5c4b7dae..1cc62a2b 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt @@ -49,6 +49,7 @@ class TaglessEnvelopeFormat( writeText(dataStart + "\r\n") writeFully(data.toBytes()) } + flush() } override fun Input.readObject(): Envelope { @@ -191,7 +192,7 @@ class TaglessEnvelopeFormat( return try { val buffer = ByteArray(TAGLESS_ENVELOPE_HEADER.length) input.readFully(buffer) - return if (buffer.toString() == TAGLESS_ENVELOPE_HEADER) { + return if (String(buffer) == TAGLESS_ENVELOPE_HEADER) { TaglessEnvelopeFormat(io) } else { null diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt index 14e4c077..eff9e705 100644 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt @@ -58,6 +58,24 @@ fun IOPlugin.writeMetaFile( } } +/** + * Return inferred [EnvelopeFormat] if only one format could read given file. If no format accepts file, return null. If + * multiple formats accepts file, throw an error. + */ +fun IOPlugin.peekBinaryFormat(binary: Binary): EnvelopeFormat? { + val formats = envelopeFormatFactories.mapNotNull { factory -> + binary.read { + factory.peekFormat(this@peekBinaryFormat, this@read) + } + } + + return when (formats.size) { + 0 -> null + 1 -> formats.first() + else -> error("Envelope format binary recognition clash") + } +} + /** * Read and envelope from file if the file exists, return null if file does not exist. * @@ -72,7 +90,11 @@ fun IOPlugin.writeMetaFile( * Return null otherwise. */ @DFExperimental -fun IOPlugin.readEnvelopeFile(path: Path, readNonEnvelopes: Boolean = false): Envelope? { +fun IOPlugin.readEnvelopeFile( + path: Path, + readNonEnvelopes: Boolean = false, + formatPeeker: IOPlugin.(Binary) -> EnvelopeFormat? = IOPlugin::peekBinaryFormat +): Envelope? { if (!Files.exists(path)) return null //read two-files directory @@ -99,24 +121,13 @@ fun IOPlugin.readEnvelopeFile(path: Path, readNonEnvelopes: Boolean = false): En val binary = path.asBinary() - val formats = envelopeFormatFactories.mapNotNull { factory -> + return formatPeeker(binary)?.run { binary.read { - factory.peekFormat(this@readEnvelopeFile, this@read) + readObject() } - } - return when (formats.size) { - 0 -> if (readNonEnvelopes) { - SimpleEnvelope(Meta.empty, binary) - } else { - null - } - 1 -> formats.first().run { - binary.read { - readObject() - } - } - else -> error("Envelope format file recognition clash") - } + } ?: if (readNonEnvelopes) { // if no format accepts file, read it as binary + SimpleEnvelope(Meta.empty, binary) + } else null } fun IOPlugin.writeEnvelopeFile( diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt index f4847cfd..b8bfafaa 100644 --- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt +++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt @@ -22,10 +22,23 @@ class FileEnvelopeTest { @Test fun testFileWriteRead() { - val tmpPath = Files.createTempFile("dataforge_test", ".df") - Global.io.writeEnvelopeFile(tmpPath,envelope) - println(tmpPath.toUri()) - val restored: Envelope = Global.io.readEnvelopeFile(tmpPath)!! - assertTrue { envelope.contentEquals(restored) } + Global.io.run { + val tmpPath = Files.createTempFile("dataforge_test", ".df") + writeEnvelopeFile(tmpPath, envelope) + println(tmpPath.toUri()) + val restored: Envelope = readEnvelopeFile(tmpPath)!! + assertTrue { envelope.contentEquals(restored) } + } + } + + @Test + fun testFileWriteReadTagless() { + Global.io.run { + val tmpPath = Files.createTempFile("dataforge_test_tagless", ".df") + writeEnvelopeFile(tmpPath, envelope, format = TaglessEnvelopeFormat) + println(tmpPath.toUri()) + val restored: Envelope = readEnvelopeFile(tmpPath)!! + assertTrue { envelope.contentEquals(restored) } + } } } \ No newline at end of file From 56334896b818955ec780049277509c4f100294cd Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Fri, 15 Nov 2019 21:25:57 +0300 Subject: [PATCH 4/5] Create gradle.yml --- .github/workflows/gradle.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/gradle.yml diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..85ea102d --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,17 @@ +name: Java CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Build with Gradle + run: ./gradlew build From 45a5b6fe28318b951dfe598219d2750e27702c1e Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 17 Nov 2019 22:15:29 +0300 Subject: [PATCH 5/5] Working on zip and directory storage for data. Update to build 0.2.4 --- .github/workflows/gradle.yml | 2 +- build.gradle.kts | 6 +- .../kotlin/hep/dataforge/data/DataNode.kt | 38 ++-- .../kotlin/hep/dataforge/data/dataCast.kt | 4 +- .../dataforge/data/TypeFilteredDataNode.kt | 4 +- .../kotlin/hep/dataforge/data/dataJVM.kt | 4 +- .../dataforge-io-yaml/build.gradle.kts | 2 - .../dataforge/io/yaml/YamlMetaFormatTest.kt | 2 +- .../kotlin/hep/dataforge/io/Binary.kt | 8 +- .../kotlin/hep/dataforge/io/Envelope.kt | 1 - .../kotlin/hep/dataforge/io/EnvelopeFormat.kt | 8 +- .../kotlin/hep/dataforge/io/IOPlugin.kt | 2 +- .../jvmMain/kotlin/hep/dataforge/io/fileIO.kt | 101 ++++++++--- .../hep/dataforge/io/FileEnvelopeTest.kt | 2 +- .../dataforge/io/tcp/EnvelopeServerTest.kt | 10 +- dataforge-scripting/build.gradle.kts | 2 - .../hep/dataforge/scripting/BuildersKtTest.kt | 2 +- .../hep/dataforge/workspace/dataUtils.kt | 14 -- .../hep/dataforge/workspace/envelopeData.kt | 33 ++++ .../hep/dataforge/workspace/fileData.kt | 162 ++++++++++++++---- .../workspace/DataPropagationTest.kt | 2 +- .../hep/dataforge/workspace/FileDataTest.kt | 72 ++++++++ .../workspace/SimpleWorkspaceTest.kt | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 58702 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 29 ++-- 26 files changed, 383 insertions(+), 131 deletions(-) delete mode 100644 dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt create mode 100644 dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt create mode 100644 dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt mode change 100644 => 100755 gradlew diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 85ea102d..adc74adf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,4 +1,4 @@ -name: Java CI +name: Gradle build on: [push] diff --git a/build.gradle.kts b/build.gradle.kts index 430fcda4..cf2f4387 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,9 +1,9 @@ import scientifik.ScientifikExtension plugins { - id("scientifik.mpp") version "0.2.2" apply false - id("scientifik.jvm") version "0.2.2" apply false - id("scientifik.publish") version "0.2.2" apply false + id("scientifik.mpp") version "0.2.4" apply false + id("scientifik.jvm") version "0.2.4" apply false + id("scientifik.publish") version "0.2.4" apply false } val dataforgeVersion by extra("0.1.5-dev-3") diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt index 65c07676..a673f0b7 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt @@ -15,20 +15,20 @@ sealed class DataItem : MetaRepr { abstract val meta: Meta - class Node(val value: DataNode) : DataItem() { - override val type: KClass get() = value.type + class Node(val node: DataNode) : DataItem() { + override val type: KClass get() = node.type - override fun toMeta(): Meta = value.toMeta() + override fun toMeta(): Meta = node.toMeta() - override val meta: Meta get() = value.meta + override val meta: Meta get() = node.meta } - class Leaf(val value: Data) : DataItem() { - override val type: KClass get() = value.type + class Leaf(val data: Data) : DataItem() { + override val type: KClass get() = data.type - override fun toMeta(): Meta = value.toMeta() + override fun toMeta(): Meta = data.toMeta() - override val meta: Meta get() = value.meta + override val meta: Meta get() = data.meta } } @@ -68,8 +68,8 @@ interface DataNode : MetaRepr { } } -val DataItem?.node: DataNode? get() = (this as? DataItem.Node)?.value -val DataItem?.data: Data? get() = (this as? DataItem.Leaf)?.value +val DataItem?.node: DataNode? get() = (this as? DataItem.Node)?.node +val DataItem?.data: Data? get() = (this as? DataItem.Leaf)?.data /** * Start computation for all goals in data node and return a job for the whole node @@ -77,8 +77,8 @@ val DataItem?.data: Data? get() = (this as? DataItem.Leaf)?.v fun DataNode<*>.launchAll(scope: CoroutineScope): Job = scope.launch { items.values.forEach { when (it) { - is DataItem.Node<*> -> it.value.launchAll(scope) - is DataItem.Leaf<*> -> it.value.start(scope) + is DataItem.Node<*> -> it.node.launchAll(scope) + is DataItem.Leaf<*> -> it.data.start(scope) } } } @@ -98,7 +98,7 @@ fun DataNode.asSequence(): Sequence>> = sequ items.forEach { (head, item) -> yield(head.asName() to item) if (item is DataItem.Node) { - val subSequence = item.value.asSequence() + val subSequence = item.node.asSequence() .map { (name, data) -> (head.asName() + name) to data } yieldAll(subSequence) } @@ -111,9 +111,9 @@ fun DataNode.asSequence(): Sequence>> = sequ fun DataNode.dataSequence(): Sequence>> = sequence { items.forEach { (head, item) -> when (item) { - is DataItem.Leaf -> yield(head.asName() to item.value) + is DataItem.Leaf -> yield(head.asName() to item.data) is DataItem.Node -> { - val subSequence = item.value.dataSequence() + val subSequence = item.node.dataSequence() .map { (name, data) -> (head.asName() + name) to data } yieldAll(subSequence) } @@ -188,8 +188,8 @@ class DataTreeBuilder(val type: KClass) { operator fun set(name: Name, node: DataNode) = set(name, node.builder()) operator fun set(name: Name, item: DataItem) = when (item) { - is DataItem.Node -> set(name, item.value.builder()) - is DataItem.Leaf -> set(name, item.value) + is DataItem.Node -> set(name, item.node.builder()) + is DataItem.Leaf -> set(name, item.data) } /** @@ -223,6 +223,10 @@ class DataTreeBuilder(val type: KClass) { fun meta(block: MetaBuilder.() -> Unit) = meta.apply(block) + fun meta(meta: Meta) { + this.meta = meta.builder() + } + fun build(): DataTree { val resMap = map.mapValues { (_, value) -> when (value) { diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt index 0b9a4910..2bf8adde 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt @@ -28,8 +28,8 @@ expect fun DataNode<*>.canCast(type: KClass): Boolean expect fun Data<*>.canCast(type: KClass): Boolean fun DataItem<*>.canCast(type: KClass): Boolean = when (this) { - is DataItem.Node -> value.canCast(type) - is DataItem.Leaf -> value.canCast(type) + is DataItem.Node -> node.canCast(type) + is DataItem.Leaf -> data.canCast(type) } /** diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt index 331f3b0e..3590679c 100644 --- a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt @@ -14,12 +14,12 @@ class TypeFilteredDataNode(val origin: DataNode<*>, override val ty origin.items.mapNotNull { (key, item) -> when (item) { is DataItem.Leaf -> { - (item.value.filterIsInstance(type))?.let { + (item.data.filterIsInstance(type))?.let { key to DataItem.Leaf(it) } } is DataItem.Node -> { - key to DataItem.Node(item.value.filterIsInstance(type)) + key to DataItem.Node(item.node.filterIsInstance(type)) } } }.associate { it } diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt index 29d048ed..f354c2f7 100644 --- a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt @@ -42,8 +42,8 @@ fun DataNode<*>.filterIsInstance(type: KClass): DataNode { */ fun DataItem<*>?.filterIsInstance(type: KClass): DataItem? = when (this) { null -> null - is DataItem.Node -> DataItem.Node(this.value.filterIsInstance(type)) - is DataItem.Leaf -> this.value.filterIsInstance(type)?.let { DataItem.Leaf(it) } + is DataItem.Node -> DataItem.Node(this.node.filterIsInstance(type)) + is DataItem.Leaf -> this.data.filterIsInstance(type)?.let { DataItem.Leaf(it) } } inline fun DataItem<*>?.filterIsInstance(): DataItem? = this@filterIsInstance.filterIsInstance(R::class) \ No newline at end of file diff --git a/dataforge-io/dataforge-io-yaml/build.gradle.kts b/dataforge-io/dataforge-io-yaml/build.gradle.kts index 74ba43cf..d287d9ac 100644 --- a/dataforge-io/dataforge-io-yaml/build.gradle.kts +++ b/dataforge-io/dataforge-io-yaml/build.gradle.kts @@ -7,6 +7,4 @@ description = "YAML meta IO" dependencies { api(project(":dataforge-io")) api("org.yaml:snakeyaml:1.25") - testImplementation(kotlin("test")) - testImplementation(kotlin("test-junit")) } diff --git a/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt b/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt index 414162f7..5c5b3c18 100644 --- a/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt +++ b/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt @@ -6,7 +6,7 @@ import hep.dataforge.meta.Meta import hep.dataforge.meta.buildMeta import hep.dataforge.meta.get import hep.dataforge.meta.seal -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.test.assertEquals diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt index bd1f2249..b671928b 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt @@ -10,7 +10,7 @@ interface Binary { /** * The size of binary in bytes. [ULong.MAX_VALUE] if size is not defined and input should be read until its end is reached */ - val size: ULong + val size: ULong get() = ULong.MAX_VALUE /** * Read continuous [Input] from this binary stating from the beginning. @@ -41,7 +41,11 @@ interface RandomAccessBinary : Binary { } fun Binary.toBytes(): ByteArray = read { - this.readBytes() + readBytes() +} + +fun Binary.contentToString(): String = read { + readText() } @ExperimentalUnsignedTypes diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt index 7cb918df..abf8a504 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt @@ -83,4 +83,3 @@ fun Envelope.withMetaLayers(vararg layers: Meta): Envelope { else -> ProxyEnvelope(this, *layers) } } - diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt index 4f747ea2..49a25919 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt @@ -23,11 +23,15 @@ interface EnvelopeFormat : IOFormat { fun Input.readPartial(): PartialEnvelope - fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta = EmptyMeta) + fun Output.writeEnvelope( + envelope: Envelope, + metaFormatFactory: MetaFormatFactory = defaultMetaFormat, + formatMeta: Meta = EmptyMeta + ) override fun Input.readObject(): Envelope - override fun Output.writeObject(obj: Envelope): Unit = writeEnvelope(obj, defaultMetaFormat) + override fun Output.writeObject(obj: Envelope): Unit = writeEnvelope(obj) } @Type(ENVELOPE_FORMAT_TYPE) diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt index a463a053..eb975029 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt @@ -20,7 +20,7 @@ class IOPlugin(meta: Meta) : AbstractPlugin(meta) { metaFormatFactories.find { it.key == key }?.invoke(meta) fun metaFormat(name: String, meta: Meta = EmptyMeta): MetaFormat? = - metaFormatFactories.find { it.name.toString() == name }?.invoke(meta) + metaFormatFactories.find { it.name.last().toString() == name }?.invoke(meta) val envelopeFormatFactories by lazy { context.content(ENVELOPE_FORMAT_TYPE).values diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt index eff9e705..9203d306 100644 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt @@ -4,6 +4,9 @@ import hep.dataforge.descriptors.NodeDescriptor import hep.dataforge.meta.DFExperimental import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta +import hep.dataforge.meta.isEmpty +import kotlinx.io.core.Output +import kotlinx.io.core.copyTo import kotlinx.io.nio.asInput import kotlinx.io.nio.asOutput import java.nio.file.Files @@ -12,10 +15,15 @@ import java.nio.file.StandardOpenOption import kotlin.reflect.full.isSuperclassOf import kotlin.streams.asSequence +/** + * Resolve IOFormat based on type + */ +@DFExperimental inline fun IOPlugin.resolveIOFormat(): IOFormat? { return ioFormats.values.find { it.type.isSuperclassOf(T::class) } as IOFormat? } + /** * Read file containing meta using given [formatOverride] or file extension to infer meta type. * If [path] is a directory search for file starting with `meta` in it @@ -43,11 +51,12 @@ fun IOPlugin.readMetaFile(path: Path, formatOverride: MetaFormat? = null, descri */ fun IOPlugin.writeMetaFile( path: Path, + meta: Meta, metaFormat: MetaFormatFactory = JsonMetaFormat, descriptor: NodeDescriptor? = null ) { val actualPath = if (Files.isDirectory(path)) { - path.resolve(metaFormat.name.toString()) + path.resolve("@" + metaFormat.name.toString()) } else { path } @@ -62,7 +71,8 @@ fun IOPlugin.writeMetaFile( * Return inferred [EnvelopeFormat] if only one format could read given file. If no format accepts file, return null. If * multiple formats accepts file, throw an error. */ -fun IOPlugin.peekBinaryFormat(binary: Binary): EnvelopeFormat? { +fun IOPlugin.peekBinaryFormat(path: Path): EnvelopeFormat? { + val binary = path.asBinary() val formats = envelopeFormatFactories.mapNotNull { factory -> binary.read { factory.peekFormat(this@peekBinaryFormat, this@read) @@ -76,6 +86,9 @@ fun IOPlugin.peekBinaryFormat(binary: Binary): EnvelopeFormat? { } } +val IOPlugin.Companion.META_FILE_NAME: String get() = "@meta" +val IOPlugin.Companion.DATA_FILE_NAME: String get() = "@data" + /** * Read and envelope from file if the file exists, return null if file does not exist. * @@ -93,14 +106,14 @@ fun IOPlugin.peekBinaryFormat(binary: Binary): EnvelopeFormat? { fun IOPlugin.readEnvelopeFile( path: Path, readNonEnvelopes: Boolean = false, - formatPeeker: IOPlugin.(Binary) -> EnvelopeFormat? = IOPlugin::peekBinaryFormat + formatPeeker: IOPlugin.(Path) -> EnvelopeFormat? = IOPlugin::peekBinaryFormat ): Envelope? { if (!Files.exists(path)) return null //read two-files directory if (Files.isDirectory(path)) { val metaFile = Files.list(path).asSequence() - .singleOrNull { it.fileName.toString().startsWith("meta") } + .singleOrNull { it.fileName.toString().startsWith(IOPlugin.META_FILE_NAME) } val meta = if (metaFile == null) { EmptyMeta @@ -108,7 +121,7 @@ fun IOPlugin.readEnvelopeFile( readMetaFile(metaFile) } - val dataFile = path.resolve("data") + val dataFile = path.resolve(IOPlugin.DATA_FILE_NAME) val data: Binary? = if (Files.exists(dataFile)) { dataFile.asBinary() @@ -119,30 +132,76 @@ fun IOPlugin.readEnvelopeFile( return SimpleEnvelope(meta, data) } - val binary = path.asBinary() - - return formatPeeker(binary)?.run { - binary.read { - readObject() - } + return formatPeeker(path)?.let { format -> + FileEnvelope(path, format) } ?: if (readNonEnvelopes) { // if no format accepts file, read it as binary - SimpleEnvelope(Meta.empty, binary) + SimpleEnvelope(Meta.empty, path.asBinary()) } else null } +private fun Path.useOutput(consumer: Output.() -> Unit) { + //TODO forbid rewrite? + Files.newByteChannel( + this, + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING + ).asOutput().use { + it.consumer() + it.flush() + } +} + +/** + * Write a binary into file. Throws an error if file already exists + */ +fun IOFormat.writeToFile(path: Path, obj: T) { + path.useOutput { + writeObject(obj) + flush() + } +} + +/** + * Write envelope file to given [path] using [envelopeFormat] and optional [metaFormat] + */ +@DFExperimental fun IOPlugin.writeEnvelopeFile( path: Path, envelope: Envelope, - format: EnvelopeFormat = TaggedEnvelopeFormat + envelopeFormat: EnvelopeFormat = TaggedEnvelopeFormat, + metaFormat: MetaFormatFactory? = null ) { - val output = Files.newByteChannel( - path, - StandardOpenOption.WRITE, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING - ).asOutput() + path.useOutput { + with(envelopeFormat) { + writeEnvelope(envelope, metaFormat ?: envelopeFormat.defaultMetaFormat) + } + } +} - with(format) { - output.writeObject(envelope) +/** + * Write separate meta and data files to given directory [path] + */ +@DFExperimental +fun IOPlugin.writeEnvelopeDirectory( + path: Path, + envelope: Envelope, + metaFormat: MetaFormatFactory = JsonMetaFormat +) { + if (!Files.exists(path)) { + Files.createDirectories(path) + } + if (!Files.isDirectory(path)) { + error("Can't write envelope directory to file") + } + if (!envelope.meta.isEmpty()) { + writeMetaFile(path, envelope.meta, metaFormat) + } + val dataFile = path.resolve(IOPlugin.DATA_FILE_NAME) + dataFile.useOutput { + envelope.data?.read { + val copied = copyTo(this@useOutput) + if (envelope.data?.size != ULong.MAX_VALUE && copied != envelope.data?.size?.toLong()) { + error("The number of copied bytes does not equal data size") + } + } } } \ No newline at end of file diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt index b8bfafaa..585aced9 100644 --- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt +++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt @@ -35,7 +35,7 @@ class FileEnvelopeTest { fun testFileWriteReadTagless() { Global.io.run { val tmpPath = Files.createTempFile("dataforge_test_tagless", ".df") - writeEnvelopeFile(tmpPath, envelope, format = TaglessEnvelopeFormat) + writeEnvelopeFile(tmpPath, envelope, envelopeFormat = TaglessEnvelopeFormat) println(tmpPath.toUri()) val restored: Envelope = readEnvelopeFile(tmpPath)!! assertTrue { envelope.contentEquals(restored) } diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt index 37c35efc..64067dec 100644 --- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt +++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt @@ -7,9 +7,9 @@ import hep.dataforge.io.TaggedEnvelopeFormat import hep.dataforge.io.writeBytes import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.runBlocking -import org.junit.AfterClass -import org.junit.BeforeClass -import org.junit.Test +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.ExperimentalTime @@ -30,13 +30,13 @@ class EnvelopeServerTest { @JvmStatic val echoEnvelopeServer = EnvelopeServer(Global, 7778, EchoResponder, GlobalScope) - @BeforeClass + @BeforeAll @JvmStatic fun start() { echoEnvelopeServer.start() } - @AfterClass + @AfterAll @JvmStatic fun close() { echoEnvelopeServer.stop() diff --git a/dataforge-scripting/build.gradle.kts b/dataforge-scripting/build.gradle.kts index 757f0c33..c848c1b1 100644 --- a/dataforge-scripting/build.gradle.kts +++ b/dataforge-scripting/build.gradle.kts @@ -19,8 +19,6 @@ kotlin { } val jvmTest by getting { dependencies { - implementation(kotlin("test")) - implementation(kotlin("test-junit")) implementation("ch.qos.logback:logback-classic:1.2.3") } } diff --git a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt index 5a9ba56d..6dd61105 100644 --- a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt +++ b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt @@ -6,7 +6,7 @@ import hep.dataforge.meta.int import hep.dataforge.workspace.SimpleWorkspaceBuilder import hep.dataforge.workspace.context import hep.dataforge.workspace.target -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt deleted file mode 100644 index f6d27774..00000000 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt +++ /dev/null @@ -1,14 +0,0 @@ -package hep.dataforge.workspace - -import hep.dataforge.data.Data -import hep.dataforge.io.Envelope -import hep.dataforge.io.IOFormat -import hep.dataforge.io.readWith -import kotlin.reflect.KClass - -/** - * Convert an [Envelope] to a data via given format. The actual parsing is done lazily. - */ -fun Envelope.toData(type: KClass, format: IOFormat): Data = Data(type, meta) { - data?.readWith(format) ?: error("Can't convert envelope without data to Data") -} \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt new file mode 100644 index 00000000..111bba76 --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt @@ -0,0 +1,33 @@ +package hep.dataforge.workspace + +import hep.dataforge.data.Data +import hep.dataforge.data.await +import hep.dataforge.io.* +import kotlinx.coroutines.coroutineScope +import kotlinx.io.core.Input +import kotlinx.io.core.buildPacket +import kotlin.reflect.KClass + +/** + * Convert an [Envelope] to a data via given format. The actual parsing is done lazily. + */ +fun Envelope.toData(type: KClass, format: IOFormat): Data = Data(type, meta) { + data?.readWith(format) ?: error("Can't convert envelope without data to Data") +} + +suspend fun Data.toEnvelope(format: IOFormat): Envelope { + val obj = coroutineScope { + await(this) + } + val binary = object : Binary { + override fun read(block: Input.() -> R): R { + //TODO optimize away additional copy by creating inputs that reads directly from output + val packet = buildPacket { + format.run { writeObject(obj) } + } + return packet.block() + } + + } + return SimpleEnvelope(meta, binary) +} \ No newline at end of file diff --git a/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt index b5ecb519..2b6d5454 100644 --- a/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt +++ b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt @@ -1,17 +1,27 @@ package hep.dataforge.workspace +//import jdk.nio.zipfs.ZipFileSystemProvider import hep.dataforge.data.* -import hep.dataforge.io.Envelope -import hep.dataforge.io.IOFormat -import hep.dataforge.io.IOPlugin -import hep.dataforge.io.readEnvelopeFile -import hep.dataforge.meta.Meta -import hep.dataforge.meta.get -import hep.dataforge.meta.string +import hep.dataforge.io.* +import hep.dataforge.meta.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.nio.file.FileSystem import java.nio.file.Files import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.nio.file.spi.FileSystemProvider import kotlin.reflect.KClass +typealias FileFormatResolver = (Path, Meta) -> IOFormat + +//private val zipFSProvider = ZipFileSystemProvider() + +private fun newZFS(path: Path): FileSystem { + val fsProvider = FileSystemProvider.installedProviders().find { it.scheme == "jar" } + ?: error("Zip file system provider not found") + return fsProvider.newFileSystem(path, mapOf("create" to "true")) +} /** * Read data with supported envelope format and binary format. If envelope format is null, then read binary directly from file. @@ -22,62 +32,152 @@ import kotlin.reflect.KClass * @param metaFile the relative file for optional meta override * @param metaFileFormat the meta format for override */ +@DFExperimental fun IOPlugin.readDataFile( path: Path, type: KClass, - formatResolver: (Meta) -> IOFormat + formatResolver: FileFormatResolver ): Data { val envelope = readEnvelopeFile(path, true) ?: error("Can't read data from $path") - val format = formatResolver(envelope.meta) + val format = formatResolver(path, envelope.meta) return envelope.toData(type, format) } -//TODO wants multi-receiver +@DFExperimental +inline fun IOPlugin.readDataFile(path: Path): Data = + readDataFile(path, T::class) { _, _ -> + resolveIOFormat() ?: error("Can't resolve IO format for ${T::class}") + } + +/** + * Add file/directory-based data tree item + */ +@DFExperimental fun DataTreeBuilder.file( plugin: IOPlugin, path: Path, - formatResolver: (Meta) -> IOFormat + formatResolver: FileFormatResolver ) { - plugin.run { - val data = readDataFile(path, type, formatResolver) - val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string - ?: path.fileName.toString().replace(".df", "") - datum(name, data) + //If path is a single file or a special directory, read it as single datum + if (!Files.isDirectory(path) || Files.list(path).allMatch { it.fileName.toString().startsWith("@") }) { + plugin.run { + val data = readDataFile(path, type, formatResolver) + val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string + ?: path.fileName.toString().replace(".df", "") + datum(name, data) + } + } else { + //otherwise, read as directory + plugin.run { + val data = readDataDirectory(path, type, formatResolver) + val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string + ?: path.fileName.toString().replace(".df", "") + node(name, data) + } } } /** - * Read the directory as a data node + * Read the directory as a data node. If [path] is a zip archive, read it as directory */ +@DFExperimental fun IOPlugin.readDataDirectory( path: Path, type: KClass, - formatResolver: (Meta) -> IOFormat + formatResolver: FileFormatResolver ): DataNode { - if (!Files.isDirectory(path)) error("Provided path $this is not a directory") + //read zipped data node + if (path.fileName != null && path.fileName.toString().endsWith(".zip")) { + //Using explicit Zip file system to avoid bizarre compatibility bugs + val fs = newZFS(path) + return readDataDirectory(fs.rootDirectories.first(), type, formatResolver) + } + if (!Files.isDirectory(path)) error("Provided path $path is not a directory") return DataNode(type) { Files.list(path).forEach { path -> - if (!path.fileName.toString().endsWith(".meta")) { + val fileName = path.fileName.toString() + if (fileName.startsWith(IOPlugin.META_FILE_NAME)) { + meta(readMetaFile(path)) + } else if (!fileName.startsWith("@")) { file(this@readDataDirectory, path, formatResolver) } } } } -fun DataTreeBuilder.directory( - plugin: IOPlugin, +@DFExperimental +inline fun IOPlugin.readDataDirectory(path: Path): DataNode = + readDataDirectory(path, T::class) { _, _ -> + resolveIOFormat() ?: error("Can't resolve IO format for ${T::class}") + } + +/** + * Write data tree to existing directory or create a new one using default [java.nio.file.FileSystem] provider + */ +@DFExperimental +suspend fun IOPlugin.writeDataDirectory( path: Path, - formatResolver: (Meta) -> IOFormat + node: DataNode, + format: IOFormat, + envelopeFormat: EnvelopeFormat? = null, + metaFormat: MetaFormatFactory? = null ) { - plugin.run { - val data = readDataDirectory(path, type, formatResolver) - val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string - ?: path.fileName.toString().replace(".df", "") - node(name, data) + withContext(Dispatchers.IO) { + if (!Files.exists(path)) { + Files.createDirectories(path) + } else if (!Files.isDirectory(path)) { + error("Can't write a node into file") + } + node.items.forEach { (token, item) -> + val childPath = path.resolve(token.toString()) + when (item) { + is DataItem.Node -> { + writeDataDirectory(childPath, item.node, format, envelopeFormat) + } + is DataItem.Leaf -> { + val envelope = item.data.toEnvelope(format) + if (envelopeFormat != null) { + writeEnvelopeFile(childPath, envelope, envelopeFormat, metaFormat) + } else { + writeEnvelopeDirectory(childPath, envelope, metaFormat ?: JsonMetaFormat) + } + } + } + } + if (!node.meta.isEmpty()) { + writeMetaFile(path, node.meta, metaFormat ?: JsonMetaFormat) + } } } - - - +suspend fun IOPlugin.writeZip( + path: Path, + node: DataNode, + format: IOFormat, + envelopeFormat: EnvelopeFormat? = null, + metaFormat: MetaFormatFactory? = null +) { + withContext(Dispatchers.IO) { + val actualFile = if (path.toString().endsWith(".zip")) { + path + } else { + path.resolveSibling(path.fileName.toString() + ".zip") + } + if (Files.exists(actualFile) && Files.size(path) == 0.toLong()) { + Files.delete(path) + } + //Files.createFile(actualFile) + newZFS(actualFile).use { zipfs -> + val internalTargetPath = zipfs.getPath("/") + Files.createDirectories(internalTargetPath) + val tmp = Files.createTempDirectory("df_zip") + writeDataDirectory(tmp, node, format, envelopeFormat, metaFormat) + Files.list(tmp).forEach { sourcePath -> + val targetPath = sourcePath.fileName.toString() + val internalTargetPath = internalTargetPath.resolve(targetPath) + Files.copy(sourcePath, internalTargetPath, StandardCopyOption.REPLACE_EXISTING) + } + } + } +} diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt index c449ffc3..083d3f57 100644 --- a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt @@ -6,8 +6,8 @@ import hep.dataforge.context.PluginTag import hep.dataforge.data.* import hep.dataforge.meta.Meta import hep.dataforge.names.asName -import org.junit.Test import kotlin.reflect.KClass +import kotlin.test.Test import kotlin.test.assertEquals diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt new file mode 100644 index 00000000..b73a4d59 --- /dev/null +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt @@ -0,0 +1,72 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Global +import hep.dataforge.data.* +import hep.dataforge.io.IOFormat +import hep.dataforge.io.io +import hep.dataforge.meta.DFExperimental +import kotlinx.coroutines.runBlocking +import kotlinx.io.core.Input +import kotlinx.io.core.Output +import kotlinx.io.core.readText +import kotlinx.io.core.writeText +import java.nio.file.Files +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +class FileDataTest { + val dataNode = DataNode { + node("dir") { + static("a", "Some string") { + "content" put "Some string" + } + } + static("b", "root data") + meta { + "content" put "This is root meta node" + } + } + + object StringIOFormat : IOFormat { + override fun Output.writeObject(obj: String) { + writeText(obj) + } + + override fun Input.readObject(): String { + return readText() + } + + } + + @Test + @DFExperimental + fun testDataWriteRead() { + Global.io.run { + val dir = Files.createTempDirectory("df_data_node") + runBlocking { + writeDataDirectory(dir, dataNode, StringIOFormat) + } + println(dir.toUri().toString()) + val reconstructed = readDataDirectory(dir, String::class) { _, _ -> StringIOFormat } + assertEquals(dataNode["dir.a"]?.meta, reconstructed["dir.a"]?.meta) + assertEquals(dataNode["b"]?.data?.get(), reconstructed["b"]?.data?.get()) + } + } + + + @Test + @Ignore + fun testZipWriteRead() { + Global.io.run { + val zip = Files.createTempFile("df_data_node", ".zip") + runBlocking { + writeZip(zip, dataNode, StringIOFormat) + } + println(zip.toUri().toString()) + val reconstructed = readDataDirectory(zip, String::class) { _, _ -> StringIOFormat } + assertEquals(dataNode["dir.a"]?.meta, reconstructed["dir.a"]?.meta) + assertEquals(dataNode["b"]?.data?.get(), reconstructed["b"]?.data?.get()) + } + } +} \ No newline at end of file diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt index 3a40e783..a4df6a4b 100644 --- a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt @@ -7,7 +7,7 @@ import hep.dataforge.meta.builder import hep.dataforge.meta.get import hep.dataforge.meta.int import hep.dataforge.names.plus -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..cc4fdc293d0e50b0ad9b65c16e7ddd1db2f6025b 100644 GIT binary patch delta 16535 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