diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index 0b43e1c03..e3aa4aba3 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { tasks { val examples = listOf( + "ZBytes", "ZDelete", "ZGet", "ZPub", diff --git a/examples/src/main/kotlin/io.zenoh/Config.kt b/examples/src/main/kotlin/io.zenoh/Config.kt index b25ebe614..2bc2903a5 100644 --- a/examples/src/main/kotlin/io.zenoh/Config.kt +++ b/examples/src/main/kotlin/io.zenoh/Config.kt @@ -14,7 +14,6 @@ package io.zenoh -import io.zenoh.sample.Attachment import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName import kotlinx.serialization.json.Json @@ -57,10 +56,12 @@ internal fun loadConfig( noMulticastScouting: Boolean, mode: String? ): Config { - val config = if (emptyArgs) { + return if (emptyArgs) { Config.default() } else { - configFile?.let { Config.from(Path(it)) } ?: run { + configFile?.let { + Config.from(path = Path(it)) + } ?: run { val connect = Connect(connectEndpoints) val listen = Listen(listenEndpoints) val scouting = Scouting(Multicast(!noMulticastScouting)) @@ -69,10 +70,4 @@ internal fun loadConfig( Config.from(jsonConfig) } } - return config -} - -internal fun decodeAttachment(attachment: String): Attachment { - val pairs = attachment.split("&").map { it.split("=").let { (k, v) -> k.toByteArray() to v.toByteArray() } } - return Attachment.Builder().addAll(pairs).res() } diff --git a/examples/src/main/kotlin/io.zenoh/ZBytes.kt b/examples/src/main/kotlin/io.zenoh/ZBytes.kt new file mode 100644 index 000000000..b5c844e17 --- /dev/null +++ b/examples/src/main/kotlin/io.zenoh/ZBytes.kt @@ -0,0 +1,237 @@ +package io.zenoh + +import io.zenoh.protocol.* +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.reflect.typeOf + +fun main() { + + /*********************************************** + * Standard serialization and deserialization. * + ***********************************************/ + + /** Numeric: byte, short, int, float, double */ + val intInput = 1234 + var payload = ZBytes.from(intInput) + var intOutput = payload.deserialize().getOrThrow() + check(intInput == intOutput) + + // Alternatively you can serialize into the type. + payload = ZBytes.serialize(intInput).getOrThrow() + intOutput = payload.deserialize().getOrThrow() + check(intInput == intOutput) + + // Alternatively, `Numeric.into()`: ZBytes can be used + payload = intInput.into() + intOutput = payload.deserialize().getOrThrow() + check(intInput == intOutput) + + // Another example with float + val floatInput = 3.1415f + payload = ZBytes.from(floatInput) + val floatOutput = payload.deserialize().getOrThrow() + check(floatInput == floatOutput) + + /** String serialization and deserialization. */ + val stringInput = "example" + payload = ZBytes.from(stringInput) + // Alternatively, you can also call `String.into()` to convert + // a string into a ZBytes object: + // payload = stringInput.into() + var stringOutput = payload.deserialize().getOrThrow() + check(stringInput == stringOutput) + + // For the case of strings, ZBytes::toString() is equivalent: + stringOutput = payload.toString() + check(stringInput == stringOutput) + + /** ByteArray serialization and deserialization. */ + val byteArrayInput = "example".toByteArray() + payload = ZBytes.from(byteArrayInput) // Equivalent to `byteArrayInput.into()` + var byteArrayOutput = payload.deserialize().getOrThrow() + check(byteArrayInput.contentEquals(byteArrayOutput)) + // Alternatively, we can directly access the bytes of property of ZBytes: + byteArrayOutput = payload.toByteArray() + check(byteArrayInput.contentEquals(byteArrayOutput)) + + /** List serialization and deserialization. + * + * Supported types: String, ByteArray, ZBytes, Byte, Short, Int, Long, Float and Double. + */ + val inputList = listOf("sample1", "sample2", "sample3") + payload = ZBytes.serialize(inputList).getOrThrow() + val outputList = payload.deserialize>().getOrThrow() + check(inputList == outputList) + + val inputListZBytes = inputList.map { value -> value.into() } + payload = ZBytes.serialize(inputListZBytes).getOrThrow() + val outputListZBytes = payload.deserialize>().getOrThrow() + check(inputListZBytes == outputListZBytes) + + val inputListByteArray = inputList.map { value -> value.toByteArray() } + payload = ZBytes.serialize(inputListByteArray).getOrThrow() + val outputListByteArray = payload.deserialize>().getOrThrow() + check(compareByteArrayLists(inputListByteArray, outputListByteArray)) + + /** + * Map serialization and deserialization. + * + * Maps with the following Type combinations are supported: String, ByteArray, ZBytes, Byte, Short, Int, Long, Float and Double. + */ + val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") + payload = ZBytes.serialize(inputMap).getOrThrow() + val outputMap = payload.deserialize>().getOrThrow() + check(inputMap == outputMap) + + val combinedInputMap = mapOf("key1" to ZBytes.from("zbytes1"), "key2" to ZBytes.from("zbytes2")) + payload = ZBytes.serialize(combinedInputMap).getOrThrow() + val combinedOutputMap = payload.deserialize>().getOrThrow() + check(combinedInputMap == combinedOutputMap) + + /********************************************* + * Custom serialization and deserialization. * + *********************************************/ + + /** + * The examples below use [MyZBytes], an example class consisting that implements the [Serializable] interface. + * + * In order for the serialization and deserialization to be successful on a custom class, + * the class itself must override the `into(): ZBytes` function, but also the companion + * object must implement the [Deserializable.From] interface. + * + * @see MyZBytes + */ + val inputMyZBytes = MyZBytes("example") + payload = ZBytes.serialize(inputMyZBytes).getOrThrow() + val outputMyZBytes = payload.deserialize().getOrThrow() + check(inputMyZBytes == outputMyZBytes) + + /** List of MyZBytes. */ + val inputListMyZBytes = inputList.map { value -> MyZBytes(value) } + payload = ZBytes.serialize>(inputListMyZBytes).getOrThrow() + val outputListMyZBytes = payload.deserialize>().getOrThrow() + check(inputListMyZBytes == outputListMyZBytes) + + /** Map of MyZBytes. */ + val inputMapMyZBytes = inputMap.map { (k, v) -> MyZBytes(k) to MyZBytes(v)}.toMap() + payload = ZBytes.serialize>(inputMapMyZBytes).getOrThrow() + val outputMapMyZBytes = payload.deserialize>().getOrThrow() + check(inputMapMyZBytes == outputMapMyZBytes) + + val combinedMap = mapOf(MyZBytes("foo") to 1, MyZBytes("bar") to 2) + payload = ZBytes.serialize>(combinedMap).getOrThrow() + val combinedOutput = payload.deserialize>().getOrThrow() + check(combinedMap == combinedOutput) + + /** + * Providing a map of deserializers. + * + * Alternatively, [ZBytes.deserialize] also accepts a deserializers parameter of type + * `Map>`. That is, a map of types that is associated + * to a function receiving a ByteArray, that returns Any. This way, you can provide a series + * of deserializer functions that extend the deserialization mechanisms we provide by default. + * + * For example, let's say we have a custom map serializer, with its own deserializer: + */ + val fooMap = mapOf(Foo("foo1") to Foo("bar1"), Foo("foo2") to Foo("bar2")) + val fooMapSerialized = ZBytes.from(serializeFooMap(fooMap)) + val deserializersMap = mapOf(typeOf>() to ::deserializeFooMap) + val deserializedFooMap = fooMapSerialized.deserialize>(deserializersMap).getOrThrow() + check(fooMap == deserializedFooMap) +} + +class MyZBytes(val content: String) : Serializable, Deserializable { + + override fun into(): ZBytes = content.into() + + companion object : Deserializable.From { + override fun from(zbytes: ZBytes): MyZBytes { + return MyZBytes(zbytes.toString()) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MyZBytes + + return content == other.content + } + + override fun hashCode(): Int { + return content.hashCode() + } +} + +/** Example class for the deserialization map examples. */ +class Foo(val content: String) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Foo + + return content == other.content + } + + override fun hashCode(): Int { + return content.hashCode() + } +} + +/** Example serializer and deserializer. */ +private fun serializeFooMap(testMap: Map): ByteArray { + return testMap.map { + val key = it.key.content.toByteArray() + val keyLength = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(key.size).array() + val value = it.value.content.toByteArray() + val valueLength = + ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(value.size).array() + keyLength + key + valueLength + value + }.reduce { acc, bytes -> acc + bytes } +} + +private fun deserializeFooMap(serializedMap: ZBytes): Map { + var idx = 0 + var sliceSize: Int + val bytes = serializedMap.toByteArray() + val decodedMap = mutableMapOf() + while (idx < bytes.size) { + sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) + .order(ByteOrder.LITTLE_ENDIAN).int + idx += Int.SIZE_BYTES + + val key = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))).order( + ByteOrder.LITTLE_ENDIAN + ).int + idx += Int.SIZE_BYTES + + val value = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + decodedMap[Foo(key.decodeToString())] = Foo(value.decodeToString()) + } + return decodedMap +} + +/** Utils for this example. */ + +private fun compareByteArrayLists(list1: List, list2: List): Boolean { + if (list1.size != list2.size) { + return false + } + + for (i in list1.indices) { + if (!list1[i].contentEquals(list2[i])) { + return false + } + } + + return true +} diff --git a/examples/src/main/kotlin/io.zenoh/ZDelete.kt b/examples/src/main/kotlin/io.zenoh/ZDelete.kt index 214cdbe1c..384820b39 100644 --- a/examples/src/main/kotlin/io.zenoh/ZDelete.kt +++ b/examples/src/main/kotlin/io.zenoh/ZDelete.kt @@ -21,6 +21,19 @@ import io.zenoh.keyexpr.intoKeyExpr class ZDelete(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Delete example" ) { + override fun run() { + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + println("Opening session...") + Session.open(config).onSuccess { session -> + session.use { + key.intoKeyExpr().onSuccess { keyExpr -> + println("Deleting resources matching '$keyExpr'...") + session.delete(keyExpr) + } + } + } + } private val connect: List by option( "-e", "--connect", help = "Endpoints to connect to.", metavar = "connect" @@ -41,22 +54,6 @@ class ZDelete(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) - - println("Opening session...") - Session.open(config).onSuccess { session -> - session.use { - key.intoKeyExpr().onSuccess { keyExpr -> - keyExpr.use { - println("Deleting resources matching '$keyExpr'...") - session.delete(keyExpr).res() - } - } - } - } - } } fun main(args: Array) = ZDelete(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZGet.kt b/examples/src/main/kotlin/io.zenoh/ZGet.kt index b5c148037..91c26f4f1 100644 --- a/examples/src/main/kotlin/io.zenoh/ZGet.kt +++ b/examples/src/main/kotlin/io.zenoh/ZGet.kt @@ -17,10 +17,12 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.long -import io.zenoh.query.ConsolidationMode +import io.zenoh.protocol.into import io.zenoh.query.QueryTarget import io.zenoh.query.Reply import io.zenoh.selector.intoSelector +import io.zenoh.value.Value +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import java.time.Duration @@ -28,14 +30,42 @@ class ZGet(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Get example" ) { + override fun run() { + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + Session.open(config).onSuccess { session -> + session.use { + selector.intoSelector().onSuccess { selector -> + session.get(selector, + channel = Channel(), + value = payload?.let { Value(it) }, + target = target?.let { QueryTarget.valueOf(it.uppercase()) } ?: QueryTarget.BEST_MATCHING, + attachment = attachment?.into(), + timeout = Duration.ofMillis(timeout)) + .onSuccess { channelReceiver -> + runBlocking { + for (reply in channelReceiver) { + when (reply) { + is Reply.Success -> println("Received ('${reply.sample.keyExpr}': '${reply.sample.value}')") + is Reply.Error -> println("Received (ERROR: '${reply.error}')") + is Reply.Delete -> println("Received (DELETE '${reply.keyExpr}')") + } + } + } + } + } + } + } + } + private val selector by option( "-s", "--selector", help = "The selection of resources to query [default: demo/example/**]", metavar = "selector" ).default("demo/example/**") - private val value by option( - "-v", "--value", help = "An optional value to put in the query.", metavar = "value" + private val payload by option( + "-p", "--payload", help = "An optional payload to put in the query.", metavar = "payload" ) private val target by option( "-t", @@ -68,42 +98,6 @@ class ZGet(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - - override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) - - Session.open(config).onSuccess { session -> - session.use { - selector.intoSelector().onSuccess { selector -> - selector.use { - session.get(selector) - .consolidation(ConsolidationMode.NONE) - .timeout(Duration.ofMillis(timeout)) - .apply { - target?.let { - target(QueryTarget.valueOf(it.uppercase())) - } - attachment?.let { - withAttachment(decodeAttachment(it)) - } - } - .res() - .onSuccess { receiver -> - runBlocking { - for (reply in receiver!!) { - when (reply) { - is Reply.Success -> {println("Received ('${reply.sample.keyExpr}': '${reply.sample.value}')")} - is Reply.Error -> println("Received (ERROR: '${reply.error}')") - } - } - } - } - } - } - } - } - } } fun main(args: Array) = ZGet(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZPub.kt b/examples/src/main/kotlin/io.zenoh/ZPub.kt index ac0dc0092..ba62d6611 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPub.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPub.kt @@ -17,10 +17,42 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import io.zenoh.keyexpr.intoKeyExpr +import io.zenoh.protocol.into class ZPub(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Pub example" ) { + override fun run() { + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + println("Opening session...") + Session.open(config).onSuccess { session -> + session.use { + key.intoKeyExpr().onSuccess { keyExpr -> + println("Declaring publisher on '$keyExpr'...") + session.declarePublisher(keyExpr).onSuccess { pub -> + println("Press CTRL-C to quit...") + val attachment = attachment?.toByteArray() + var idx = 0 + while (true) { + Thread.sleep(1000) + val payload = "[${ + idx.toString().padStart(4, ' ') + }] $value" + println( + "Putting Data ('$keyExpr': '$payload')..." + ) + attachment?.let { + pub.put(payload, attachment = it.into()) + } ?: let { pub.put(payload) } + idx++ + } + } + } + } + }.onFailure { exception -> println(exception.message) } + } + private val key by option( "-k", "--key", help = "The key expression to write to [default: demo/example/zenoh-kotlin-pub]", metavar = "key" @@ -50,41 +82,6 @@ class ZPub(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) - - println("Opening session...") - Session.open(config).onSuccess { session -> - session.use { - key.intoKeyExpr().onSuccess { keyExpr -> - keyExpr.use { - println("Declaring publisher on '$keyExpr'...") - session.declarePublisher(keyExpr).res().onSuccess { pub -> - pub.use { - println("Press CTRL-C to quit...") - val attachment = attachment?.let { decodeAttachment(it) } - var idx = 0 - while (true) { - Thread.sleep(1000) - val payload = "[${ - idx.toString().padStart(4, ' ') - }] $value" - println( - "Putting Data ('$keyExpr': '$payload')..." - ) - attachment?.let { - pub.put(payload).withAttachment(attachment).res() - } ?: let { pub.put(payload).res() } - idx++ - } - } - } - } - } - } - }.onFailure { exception -> println(exception.message) } - } } fun main(args: Array) = ZPub(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZPubThr.kt b/examples/src/main/kotlin/io.zenoh/ZPubThr.kt index 24dd87919..81ef45956 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPubThr.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPubThr.kt @@ -21,17 +21,57 @@ import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.boolean import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.ulong -import io.zenoh.prelude.KnownEncoding import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.CongestionControl import io.zenoh.prelude.Encoding import io.zenoh.prelude.Priority +import io.zenoh.prelude.QoS import io.zenoh.value.Value class ZPubThr(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Throughput example" ) { + override fun run() { + val data = ByteArray(payloadSize) + for (i in 0.. + session.declarePublisher("test/thr".intoKeyExpr().getOrThrow(), qos = qos).onSuccess { pub -> + println("Publisher declared on test/thr.") + var count: Long = 0 + var start = System.currentTimeMillis() + val number = number.toLong() + println("Press CTRL-C to quit...") + while (true) { + pub.put(value).getOrThrow() + if (statsPrint) { + if (count < number) { + count++ + } else { + val throughput = count * 1000 / (System.currentTimeMillis() - start) + println("$throughput msgs/s") + count = 0 + start = System.currentTimeMillis() + } + } + } + } + } + } + } + private val payloadSize by argument( "payload_size", help = "Sets the size of the payload to publish [Default: 8]" @@ -66,46 +106,6 @@ class ZPubThr(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - override fun run() { - val data = ByteArray(payloadSize) - for (i in 0.. - session.declarePublisher("test/thr".intoKeyExpr().getOrThrow()) - .congestionControl(CongestionControl.BLOCK).apply { - priorityInput?.let { priority(Priority.entries[it]) } - }.res().onSuccess { pub -> - pub.use { - println("Publisher declared on test/thr.") - var count: Long = 0 - var start = System.currentTimeMillis() - val number = number.toLong() - println("Press CTRL-C to quit...") - while (true) { - pub.put(value).res().getOrThrow() - if (statsPrint) { - if (count < number) { - count++ - } else { - val throughput = count * 1000 / (System.currentTimeMillis() - start) - println("$throughput msgs/s") - count = 0 - start = System.currentTimeMillis() - } - } - } - } - } - } - } - } } fun main(args: Array) = ZPubThr(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZPut.kt b/examples/src/main/kotlin/io.zenoh/ZPut.kt index 18af771bb..ae5492bf8 100644 --- a/examples/src/main/kotlin/io.zenoh/ZPut.kt +++ b/examples/src/main/kotlin/io.zenoh/ZPut.kt @@ -17,14 +17,28 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority +import io.zenoh.protocol.into class ZPut(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Put example" ) { + override fun run() { + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + println("Opening Session...") + Session.open(config).onSuccess { session -> + session.use { + key.intoKeyExpr().onSuccess { keyExpr -> + keyExpr.use { + session.put(keyExpr, value, attachment = attachment?.into()) + .onSuccess { println("Putting Data ('$keyExpr': '$value')...") } + } + } + } + }.onFailure { println(it.message) } + } + private val configFile by option("-c", "--config", help = "A configuration file.", metavar = "config") private val key by option( "-k", "--key", help = "The key expression to write to [default: demo/example/zenoh-kotlin-put]", metavar = "key" @@ -53,31 +67,6 @@ class ZPut(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) - - println("Opening Session...") - Session.open(config).onSuccess { session -> - session.use { - key.intoKeyExpr().onSuccess { keyExpr -> - keyExpr.use { - session.put(keyExpr, value) - .congestionControl(CongestionControl.BLOCK) - .priority(Priority.REALTIME) - .kind(SampleKind.PUT) - .apply { - attachment?.let { - withAttachment(decodeAttachment(it)) - } - } - .res() - .onSuccess { println("Putting Data ('$keyExpr': '$value')...") } - } - } - } - }.onFailure { println(it.message) } - } } fun main(args: Array) = ZPut(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZQueryable.kt b/examples/src/main/kotlin/io.zenoh/ZQueryable.kt index fda233973..aabd85192 100644 --- a/examples/src/main/kotlin/io.zenoh/ZQueryable.kt +++ b/examples/src/main/kotlin/io.zenoh/ZQueryable.kt @@ -16,10 +16,8 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* -import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.prelude.SampleKind -import io.zenoh.queryable.Query +import io.zenoh.value.Value import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking import org.apache.commons.net.ntp.TimeStamp @@ -28,6 +26,31 @@ class ZQueryable(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Queryable example" ) { + override fun run() { + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + Session.open(config).onSuccess { session -> + session.use { + key.intoKeyExpr().onSuccess { keyExpr -> + println("Declaring Queryable on $key...") + session.declareQueryable(keyExpr, Channel()).onSuccess { queryable -> + runBlocking { + for (query in queryable.receiver) { + val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" + println(">> [Queryable] Received Query '${query.selector}' $valueInfo") + query.replySuccess( + keyExpr, + value = Value(value), + timestamp = TimeStamp.getCurrentTime() + ).onFailure { println(">> [Queryable ] Error sending reply: $it") } + } + } + } + } + } + } + } + private val key by option( "-k", "--key", @@ -53,41 +76,6 @@ class ZQueryable(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) - - Session.open(config).onSuccess { session -> - session.use { - key.intoKeyExpr().onSuccess { keyExpr -> - keyExpr.use { - println("Declaring Queryable on " + key + "...") - session.declareQueryable(keyExpr).res().onSuccess { queryable -> - queryable.use { - println("Press CTRL-C to quit...") - queryable.receiver?.let { receiverChannel -> // The default receiver is a Channel we can process on a coroutine. - runBlocking { - handleRequests(receiverChannel, keyExpr) - } - } - } - } - } - } - } - } - } - - private suspend fun handleRequests( - receiverChannel: Channel, keyExpr: KeyExpr - ) { - for (query in receiverChannel) { - val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" - println(">> [Queryable] Received Query '${query.selector}' $valueInfo") - query.reply(keyExpr).success(value).withKind(SampleKind.PUT).withTimeStamp(TimeStamp.getCurrentTime()) - .res().onFailure { println(">> [Queryable ] Error sending reply: $it") } - } - } } fun main(args: Array) = ZQueryable(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZSub.kt b/examples/src/main/kotlin/io.zenoh/ZSub.kt index b7b606094..5a7fd15d7 100644 --- a/examples/src/main/kotlin/io.zenoh/ZSub.kt +++ b/examples/src/main/kotlin/io.zenoh/ZSub.kt @@ -17,12 +17,40 @@ package io.zenoh import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import io.zenoh.keyexpr.intoKeyExpr +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking class ZSub(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Sub example" ) { + override fun run() { + val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting, mode) + + println("Opening session...") + Session.open(config).onSuccess { session -> + session.use { + key.intoKeyExpr().onSuccess { keyExpr -> + keyExpr.use { + println("Declaring Subscriber on '$keyExpr'...") + + session.declareSubscriber(keyExpr, Channel()).onSuccess { subscriber -> + runBlocking { + for (sample in subscriber.receiver) { + println(">> [Subscriber] Received ${sample.kind} ('${sample.keyExpr}': '${sample.value}'" + "${ + sample.attachment?.let { + ", with attachment: $it" + } ?: "" + })") + } + } + } + } + } + } + }.onFailure { exception -> println(exception.message) } + } + private val configFile by option("-c", "--config", help = "A configuration file.", metavar = "config") private val key by option( "-k", "--key", help = "The key expression to subscribe to [default: demo/example/**]", metavar = "key" @@ -42,37 +70,6 @@ class ZSub(private val emptyArgs: Boolean) : CliktCommand( private val noMulticastScouting: Boolean by option( "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." ).flag(default = false) - - override fun run() { - val config = loadConfig(emptyArgs, configFile, connect, listen, noMulticastScouting,mode) - - println("Opening session...") - Session.open(config).onSuccess { session -> - session.use { - key.intoKeyExpr().onSuccess { keyExpr -> - keyExpr.use { - println("Declaring Subscriber on '$keyExpr'...") - session.declareSubscriber(keyExpr).bestEffort().res().onSuccess { subscriber -> - subscriber.use { - println("Press CTRL-C to quit...") - runBlocking { - for (sample in subscriber.receiver!!) { - println(">> [Subscriber] Received ${sample.kind} ('${sample.keyExpr}': '${sample.value}'" + "${ - sample.attachment?.let { - ", with attachment: " + "${ - it.values.map { it.first.decodeToString() to it.second.decodeToString() } - }" - } ?: "" - })") - } - } - } - } - } - } - } - }.onFailure { exception -> println(exception.message) } - } } fun main(args: Array) = ZSub(args.isEmpty()).main(args) diff --git a/examples/src/main/kotlin/io.zenoh/ZSubThr.kt b/examples/src/main/kotlin/io.zenoh/ZSubThr.kt index f0f22bf85..f413d7333 100644 --- a/examples/src/main/kotlin/io.zenoh/ZSubThr.kt +++ b/examples/src/main/kotlin/io.zenoh/ZSubThr.kt @@ -18,39 +18,13 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.ulong import io.zenoh.keyexpr.intoKeyExpr +import io.zenoh.subscriber.Reliability import io.zenoh.subscriber.Subscriber import kotlin.system.exitProcess class ZSubThr(private val emptyArgs: Boolean) : CliktCommand( help = "Zenoh Subscriber Throughput test" ) { - - private val samples by option( - "-s", "--samples", help = "Number of throughput measurements [default: 10]", metavar = "number" - ).ulong().default(10u) - private val number by option( - "-n", - "--number", - help = "Number of messages in each throughput measurements [default: 100000]", - metavar = "number" - ).ulong().default(10000u) - private val configFile by option("-c", "--config", help = "A configuration file.", metavar = "config") - private val connect: List by option( - "-e", "--connect", help = "Endpoints to connect to.", metavar = "connect" - ).multiple() - private val listen: List by option( - "-l", "--listen", help = "Endpoints to listen on.", metavar = "listen" - ).multiple() - private val mode by option( - "-m", - "--mode", - help = "The session mode. Default: peer. Possible values: [peer, client, router]", - metavar = "mode" - ).default("peer") - private val noMulticastScouting: Boolean by option( - "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." - ).flag(default = false) - companion object { private const val NANOS_TO_SEC = 1_000_000_000L } @@ -106,7 +80,11 @@ class ZSubThr(private val emptyArgs: Boolean) : CliktCommand( session.use { println("Press CTRL-C to quit...") subscriber = - session.declareSubscriber(keyExpr).reliable().with { listener(number) }.res().getOrThrow() + session.declareSubscriber( + keyExpr, + callback = { listener(number) }, + reliability = Reliability.RELIABLE + ).getOrThrow() while (subscriber.isValid()) {/* Keep alive the subscriber until the test is done. */ Thread.sleep(1000) } @@ -115,6 +93,33 @@ class ZSubThr(private val emptyArgs: Boolean) : CliktCommand( } } } + + private val samples by option( + "-s", "--samples", help = "Number of throughput measurements [default: 10]", metavar = "number" + ).ulong().default(10u) + private val number by option( + "-n", + "--number", + help = "Number of messages in each throughput measurements [default: 100000]", + metavar = "number" + ).ulong().default(10000u) + private val configFile by option("-c", "--config", help = "A configuration file.", metavar = "config") + private val connect: List by option( + "-e", "--connect", help = "Endpoints to connect to.", metavar = "connect" + ).multiple() + private val listen: List by option( + "-l", "--listen", help = "Endpoints to listen on.", metavar = "listen" + ).multiple() + private val mode by option( + "-m", + "--mode", + help = "The session mode. Default: peer. Possible values: [peer, client, router]", + metavar = "mode" + ).default("peer") + private val noMulticastScouting: Boolean by option( + "--no-multicast-scouting", help = "Disable the multicast-based scouting mechanism." + ).flag(default = false) + } fun main(args: Array) = ZSubThr(args.isEmpty()).main(args) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 743f7cd99..7897a24d1 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.72.0" +channel = "1.75.0" diff --git a/version.txt b/version.txt index 19a5fa437..835b79670 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.11.0-dev \ No newline at end of file +1.0.0-dev \ No newline at end of file diff --git a/zenoh-jni/Cargo.lock b/zenoh-jni/Cargo.lock index 55e17e567..d78c53595 100644 --- a/zenoh-jni/Cargo.lock +++ b/zenoh-jni/Cargo.lock @@ -86,6 +86,45 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" +[[package]] +name = "asn1-rs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ad1373757efa0f70ec53939aabc7152e1591cb485208052993070ac8d2429d" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -156,50 +195,11 @@ dependencies = [ "event-listener 2.5.3", ] -[[package]] -name = "async-process" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9d28b1d97e08915212e2e45310d47854eafa69600756fc735fb788f75199c9" -dependencies = [ - "async-io", - "async-lock", - "autocfg", - "blocking", - "cfg-if", - "event-listener 2.5.3", - "futures-lite", - "rustix", - "signal-hook", - "windows-sys 0.48.0", -] - [[package]] name = "async-std" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" -dependencies = [ - "async-channel", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] [[package]] name = "async-task" @@ -215,7 +215,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -262,6 +262,12 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -330,9 +336,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cache-padded" @@ -407,9 +413,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.2.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -501,6 +507,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.11" @@ -543,12 +563,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "dyn-clone" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "env_logger" version = "0.10.2" @@ -583,9 +620,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", @@ -640,9 +677,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -718,7 +755,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -803,16 +840,10 @@ dependencies = [ ] [[package]] -name = "gloo-timers" -version = "0.2.6" +name = "half" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" [[package]] name = "hashbrown" @@ -822,18 +853,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" -dependencies = [ - "ahash", -] - -[[package]] -name = "hashbrown" -version = "0.14.0" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -895,14 +917,134 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "idna" -version = "0.4.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] @@ -922,7 +1064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.5", ] [[package]] @@ -963,6 +1105,21 @@ dependencies = [ "serde", ] +[[package]] +name = "iter-read" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c397ca3ea05ad509c4ec451fea28b4771236a376ca1c69fd5143aae0cf8f93c4" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1036,20 +1193,11 @@ dependencies = [ [[package]] name = "keyed-set" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79e110283e09081809ca488cf3a9709270c6d4d4c4a32674c39cc438366615a" -dependencies = [ - "hashbrown 0.13.2", -] - -[[package]] -name = "kv-log-macro" -version = "1.0.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +checksum = "0a3ec39d2dc17953a1540d63906a112088f79b2e46833b4ed65bc9de3904ae34" dependencies = [ - "log", + "hashbrown 0.14.5", ] [[package]] @@ -1063,9 +1211,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1089,6 +1237,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.10" @@ -1104,9 +1258,6 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -dependencies = [ - "value-bag", -] [[package]] name = "lz4_flex" @@ -1128,9 +1279,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.3" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" @@ -1178,6 +1335,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1224,19 +1391,18 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1245,9 +1411,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -1272,6 +1438,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c958dd45046245b9c3c2547369bb634eb461670b2e7e0de552905801a648d1d" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1357,9 +1532,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" @@ -1392,7 +1567,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -1416,6 +1591,48 @@ dependencies = [ "indexmap 2.0.0", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -1433,7 +1650,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -1471,18 +1688,18 @@ dependencies = [ [[package]] name = "pnet_base" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" dependencies = [ "no-std-net", ] [[package]] name = "pnet_datalink" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad5854abf0067ebbd3967f7d45ebc8976ff577ff0c7bd101c4973ae3c70f98fe" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" dependencies = [ "ipnetwork", "libc", @@ -1493,9 +1710,9 @@ dependencies = [ [[package]] name = "pnet_sys" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417c0becd1b573f6d544f73671070b039051e5ad819cc64aa96377b536128d00" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" dependencies = [ "libc", "winapi", @@ -1537,18 +1754,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "quinn" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904e3d3ba178131798c6d9375db2b13b34337d489b089fc5ba0825a2ff1bee73" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" dependencies = [ "bytes", "pin-project-lite", @@ -1563,9 +1780,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e974563a4b1c2206bbc61191ca4da9c22e4308b4c455e8906751cc7828393f08" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" dependencies = [ "bytes", "rand", @@ -1581,9 +1798,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4f0def2590301f4f667db5a77f9694fb004f82796dc1a8b1508fafa3d0e8b72" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" dependencies = [ "libc", "once_cell", @@ -1594,9 +1811,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1671,14 +1888,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", - "regex-syntax 0.7.5", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1692,13 +1909,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.4", ] [[package]] @@ -1709,9 +1926,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "ring" @@ -1743,7 +1960,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.4", "bitflags 2.5.0", "serde", "serde_derive", @@ -1792,6 +2009,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.37.25" @@ -1808,9 +2034,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.9" +version = "0.23.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" dependencies = [ "log", "once_cell", @@ -1840,7 +2066,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e4980fa29e4c4b212ffb3db068a564cbf560e51d3944b7c88bd8bf5bec64f4" dependencies = [ - "base64", + "base64 0.21.4", "rustls-pki-types", ] @@ -1919,6 +2145,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" dependencies = [ "dyn-clone", + "either", "schemars_derive", "serde", "serde_json", @@ -1984,22 +2211,45 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-pickle" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762ad136a26407c6a80825813600ceeab5e613660d93d79a41f0ec877171e71" +dependencies = [ + "byteorder", + "iter-read", + "num-bigint", + "num-traits", + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -2015,9 +2265,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -2087,25 +2337,6 @@ dependencies = [ "dirs", ] -[[package]] -name = "signal-hook" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.1.0" @@ -2116,6 +2347,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -2127,9 +2364,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -2176,6 +2413,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2219,15 +2462,26 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.33" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2260,7 +2514,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -2304,6 +2558,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2353,7 +2617,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -2369,9 +2633,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", @@ -2389,7 +2653,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "hashbrown 0.14.0", + "hashbrown 0.14.5", "pin-project-lite", "tokio", ] @@ -2415,7 +2679,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", ] [[package]] @@ -2472,9 +2736,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", @@ -2485,7 +2749,6 @@ dependencies = [ "rand", "sha1", "thiserror", - "url", "utf-8", ] @@ -2513,9 +2776,9 @@ checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" [[package]] name = "uhlc" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b6df3f3e948b40e20c38a6d1fd6d8f91b3573922fc164e068ad3331560487e" +checksum = "79ac3c37bd9506595768f0387bd39d644525728b4a1d783218acabfb56356db7" dependencies = [ "humantime", "lazy_static", @@ -2525,27 +2788,12 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-xid" version = "0.2.4" @@ -2564,6 +2812,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unwrap-infallible" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151ac09978d3c2862c4e39b557f4eceee2cc72150bc4cb4f16abf061b6e381fb" + [[package]] name = "unzip-n" version = "0.1.2" @@ -2577,9 +2831,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", "idna", @@ -2592,6 +2846,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.4.1" @@ -2631,12 +2897,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "value-bag" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" - [[package]] name = "vec_map" version = "0.8.2" @@ -2692,22 +2952,10 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -2726,7 +2974,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2737,16 +2985,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "web-sys" -version = "0.3.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.26.0" @@ -2985,35 +3223,95 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", + "synstructure", +] + [[package]] name = "zenoh" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "ahash", "async-trait", - "base64", - "const_format", - "event-listener 4.0.0", + "base64 0.22.1", + "bytes", + "event-listener 5.3.1", "flume 0.11.0", "form_urlencoded", "futures", "git-version", + "itertools", "lazy_static", + "once_cell", "ordered-float", "paste", "petgraph", + "phf", "rand", "regex", "rustc_version", "serde", + "serde-pickle", + "serde_cbor", "serde_json", + "serde_yaml", "socket2 0.5.6", "stop-token", "tokio", "tokio-util", "tracing", "uhlc", + "unwrap-infallible", "uuid", "vec_map", "zenoh-buffers", @@ -3037,16 +3335,16 @@ dependencies = [ [[package]] name = "zenoh-buffers" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "zenoh-collections", ] [[package]] name = "zenoh-codec" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "serde", "tracing", @@ -3057,13 +3355,13 @@ dependencies = [ [[package]] name = "zenoh-collections" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" [[package]] name = "zenoh-config" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "flume 0.11.0", "json5", @@ -3073,8 +3371,10 @@ dependencies = [ "serde_json", "serde_yaml", "tracing", + "uhlc", "validated_struct", "zenoh-core", + "zenoh-macros", "zenoh-protocol", "zenoh-result", "zenoh-util", @@ -3082,8 +3382,8 @@ dependencies = [ [[package]] name = "zenoh-core" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-global-executor", "lazy_static", @@ -3094,8 +3394,8 @@ dependencies = [ [[package]] name = "zenoh-crypto" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "aes", "hmac", @@ -3107,31 +3407,29 @@ dependencies = [ [[package]] name = "zenoh-ext" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "bincode", "flume 0.11.0", "futures", + "phf", "serde", + "serde_cbor", + "serde_json", "tokio", "tracing", "zenoh", - "zenoh-core", "zenoh-macros", - "zenoh-result", - "zenoh-runtime", - "zenoh-sync", - "zenoh-task", "zenoh-util", ] [[package]] name = "zenoh-keyexpr" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.5", "keyed-set", "rand", "schemars", @@ -3142,8 +3440,8 @@ dependencies = [ [[package]] name = "zenoh-link" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", "zenoh-config", @@ -3160,8 +3458,8 @@ dependencies = [ [[package]] name = "zenoh-link-commons" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", "flume 0.11.0", @@ -3172,6 +3470,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "webpki-roots", "zenoh-buffers", "zenoh-codec", "zenoh-config", @@ -3184,11 +3483,11 @@ dependencies = [ [[package]] name = "zenoh-link-quic" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "futures", "quinn", "rustls", @@ -3201,6 +3500,8 @@ dependencies = [ "tokio-util", "tracing", "webpki-roots", + "x509-parser", + "zenoh-collections", "zenoh-config", "zenoh-core", "zenoh-link-commons", @@ -3213,10 +3514,11 @@ dependencies = [ [[package]] name = "zenoh-link-tcp" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", + "socket2 0.5.6", "tokio", "tokio-util", "tracing", @@ -3231,22 +3533,25 @@ dependencies = [ [[package]] name = "zenoh-link-tls" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "futures", "rustls", "rustls-pemfile", "rustls-pki-types", "rustls-webpki", "secrecy", + "socket2 0.5.6", "tokio", "tokio-rustls", "tokio-util", "tracing", "webpki-roots", + "x509-parser", + "zenoh-collections", "zenoh-config", "zenoh-core", "zenoh-link-commons", @@ -3259,8 +3564,8 @@ dependencies = [ [[package]] name = "zenoh-link-udp" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", "socket2 0.5.6", @@ -3280,8 +3585,8 @@ dependencies = [ [[package]] name = "zenoh-link-unixsock_stream" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", "futures", @@ -3300,8 +3605,8 @@ dependencies = [ [[package]] name = "zenoh-link-ws" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", "futures-util", @@ -3321,21 +3626,20 @@ dependencies = [ [[package]] name = "zenoh-macros" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", "zenoh-keyexpr", ] [[package]] name = "zenoh-plugin-trait" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ - "const_format", "libloading", "serde", "serde_json", @@ -3348,30 +3652,31 @@ dependencies = [ [[package]] name = "zenoh-protocol" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "const_format", "rand", "serde", "uhlc", "zenoh-buffers", + "zenoh-collections", "zenoh-keyexpr", "zenoh-result", ] [[package]] name = "zenoh-result" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "anyhow", ] [[package]] name = "zenoh-runtime" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "futures", "lazy_static", @@ -3379,17 +3684,16 @@ dependencies = [ "ron", "serde", "tokio", - "zenoh-collections", "zenoh-macros", "zenoh-result", ] [[package]] name = "zenoh-sync" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ - "event-listener 4.0.0", + "event-listener 5.3.1", "futures", "tokio", "zenoh-buffers", @@ -3400,8 +3704,8 @@ dependencies = [ [[package]] name = "zenoh-task" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "futures", "tokio", @@ -3413,11 +3717,13 @@ dependencies = [ [[package]] name = "zenoh-transport" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ "async-trait", + "crossbeam-utils", "flume 0.11.0", + "lazy_static", "lz4_flex", "paste", "rand", @@ -3445,11 +3751,11 @@ dependencies = [ [[package]] name = "zenoh-util" -version = "0.11.0-dev" -source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=main#e66745ebdf70f01bf01ba60f69bc902b917b2d24" +version = "1.0.0-dev" +source = "git+https://github.com/eclipse-zenoh/zenoh.git?branch=dev/1.0.0#7f7d648e76c46f0584d56405c5f162892278d610" dependencies = [ - "async-std", "async-trait", + "const_format", "flume 0.11.0", "home", "humantime", @@ -3457,6 +3763,8 @@ dependencies = [ "libc", "libloading", "pnet_datalink", + "serde", + "serde_json", "shellexpand", "tokio", "tracing", @@ -3468,7 +3776,7 @@ dependencies = [ [[package]] name = "zenoh_jni" -version = "0.11.0-dev" +version = "1.0.0-dev" dependencies = [ "android-logd-logger", "async-std", @@ -3477,11 +3785,12 @@ dependencies = [ "jni 0.21.1", "json5", "rustc_version", + "serde_yaml", "tracing", "uhlc", "zenoh", "zenoh-ext", - "zenoh-util", + "zenoh-protocol", ] [[package]] @@ -3501,7 +3810,28 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.33", + "syn 2.0.52", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", + "synstructure", ] [[package]] @@ -3509,3 +3839,25 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] diff --git a/zenoh-jni/Cargo.toml b/zenoh-jni/Cargo.toml index 097e1bf5c..a9b467fef 100644 --- a/zenoh-jni/Cargo.toml +++ b/zenoh-jni/Cargo.toml @@ -13,8 +13,8 @@ # [package] -rust-version = "1.66.1" -version = "0.11.0-dev" # Zenoh version +rust-version = "1.75.0" +version = "1.0.0-dev" repository = "https://github.com/eclipse-zenoh/zenoh" homepage = "http://zenoh.io" edition = "2021" @@ -33,11 +33,12 @@ async-std = { version = "=1.12.0", default-features = false } clap = "3.2.23" jni = "0.21.1" flume = "0.10.14" -uhlc = "0.7.0" +uhlc = "0.8.0" json5 = "0.4.1" -zenoh = { version = "0.11.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false } -zenoh-ext = { version = "0.11.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false } -zenoh-util = { version = "0.11.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main" } +serde_yaml = "0.9.19" +zenoh = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "dev/1.0.0", default-features = false } +zenoh-ext = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "dev/1.0.0", default-features = false } +zenoh-protocol = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "dev/1.0.0", default-features = false } tracing = "0.1" [lib] name = "zenoh_jni" diff --git a/zenoh-jni/src/errors.rs b/zenoh-jni/src/errors.rs index 337a3bab1..a254c67f5 100644 --- a/zenoh-jni/src/errors.rs +++ b/zenoh-jni/src/errors.rs @@ -16,6 +16,46 @@ use std::fmt; use jni::JNIEnv; +#[macro_export] +macro_rules! throw_exception { + ($env:expr, $err:expr) => {{ + let _ = $err.throw_on_jvm(&mut $env).map_err(|err| { + tracing::error!("Unable to throw exception: {}", err); + }); + }}; +} + +#[macro_export] +macro_rules! jni_error { + ($arg:expr) => { + $crate::errors::Error::Jni($arg.to_string()) + }; + ($fmt:expr, $($arg:tt)*) => { + Error::Jni(format!($fmt, $($arg)*)) + }; +} + +#[macro_export] +macro_rules! session_error { + ($arg:expr) => { + $crate::errors::Error::Session($arg.to_string()) + }; + ($fmt:expr, $($arg:tt)*) => { + Error::Session(format!($fmt, $($arg)*)) + }; + +} + +#[macro_export] +macro_rules! key_expr_error { + ($arg:expr) => { + Error::KeyExpr($arg.to_string()) + }; + ($fmt:expr, $($arg:tt)*) => { + Error::KeyExpr(format!($fmt, $($arg)*)) + }; +} + pub(crate) type Result = core::result::Result; #[derive(Debug)] @@ -49,8 +89,8 @@ impl Error { let exception_name = self.get_associated_kotlin_exception(); let exception_class = env .find_class(&exception_name) - .map_err(|err| Error::Jni(format!("Failed to retrieve exception class: {}", err)))?; + .map_err(|err| jni_error!("Failed to retrieve exception class: {}", err))?; env.throw_new(exception_class, self.to_string()) - .map_err(|err| Error::Jni(format!("Failed to throw exception: {}", err))) + .map_err(|err| jni_error!("Failed to throw exception: {}", err)) } } diff --git a/zenoh-jni/src/key_expr.rs b/zenoh-jni/src/key_expr.rs index e2d5d1038..ae1756546 100644 --- a/zenoh-jni/src/key_expr.rs +++ b/zenoh-jni/src/key_expr.rs @@ -18,34 +18,47 @@ use std::sync::Arc; use jni::objects::JClass; use jni::sys::{jboolean, jstring}; use jni::{objects::JString, JNIEnv}; -use zenoh::prelude::KeyExpr; +use zenoh::key_expr::KeyExpr; use crate::errors::Error; use crate::errors::Result; use crate::utils::decode_string; +use crate::{jni_error, key_expr_error, throw_exception}; +/// Validates the provided `key_expr` to be a valid key expression, returning it back +/// in case of success or throwing an exception in case of failure. +/// +/// # Parameters: +/// `env`: The JNI environment. +/// `_class`: the Java class (unused). +/// `key_expr`: Java string representation of the intended key expression. +/// #[no_mangle] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_tryFromViaJNI( +pub extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_tryFromViaJNI( mut env: JNIEnv, _class: JClass, key_expr: JString, ) -> jstring { - decode_key_expr(&mut env, &key_expr) - .and_then(|key_expr| { - env.new_string(key_expr.to_string()) - .map(|kexp| kexp.as_raw()) - .map_err(|err| Error::KeyExpr(err.to_string())) - }) + validate_key_expr(&mut env, &key_expr) + .map(|_| **key_expr) .unwrap_or_else(|err| { - let _ = err.throw_on_jvm(&mut env); + throw_exception!(env, err); JString::default().as_raw() }) } +/// Returns a java string representation of the autocanonized version of the provided `key_expr`. +/// In case of failure and exception will be thrown. +/// +/// # Parameters: +/// `env`: The JNI environment. +/// `_class`: the Java class (unused). +/// `key_expr`: Java string representation of the intended key expression. +/// #[no_mangle] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_autocanonizeViaJNI( +pub extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_autocanonizeViaJNI( mut env: JNIEnv, _class: JClass, key_expr: JString, @@ -54,105 +67,120 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_auto .and_then(|key_expr| { env.new_string(key_expr.to_string()) .map(|kexp| kexp.as_raw()) - .map_err(|err| Error::KeyExpr(err.to_string())) + .map_err(|err| jni_error!(err)) }) .unwrap_or_else(|err| { - let _ = err.throw_on_jvm(&mut env); + throw_exception!(env, err); JString::default().as_raw() }) } +/// Returns true in case key_expr_1 intersects key_expr_2. +/// +/// # Params: +/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. +/// - `key_expr_ptr_1`: String representation of the key expression 1. +/// - `key_expr_ptr_2`: Pointer to the key expression 2, differs from null only if it's a declared key expr. +/// - `key_expr_ptr_2`: String representation of the key expression 2. +/// +/// # Safety +/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing +/// key expressions that were declared from a session (in that case the key expression has a pointer associated). +/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers +/// remain valid after the call to this function. +/// #[no_mangle] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_intersectsViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_intersectsViaJNI( mut env: JNIEnv, _: JClass, - key_expr_ptr_1: *const KeyExpr<'static>, + key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, key_expr_str_1: JString, - key_expr_ptr_2: *const KeyExpr<'static>, + key_expr_ptr_2: /*nullable*/ *const KeyExpr<'static>, key_expr_str_2: JString, ) -> jboolean { - let key_expr_1 = - match process_kotlin_key_expr_or_throw(&mut env, &key_expr_str_1, key_expr_ptr_1) { - Ok(key_expr) => key_expr, - Err(_) => return false as jboolean, - }; - let key_expr_2 = - match process_kotlin_key_expr_or_throw(&mut env, &key_expr_str_2, key_expr_ptr_2) { - Ok(key_expr) => key_expr, - Err(_) => return false as jboolean, - }; - let intersects = key_expr_1.intersects(&key_expr_2); - intersects as jboolean + || -> Result { + let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; + let key_expr_2 = process_kotlin_key_expr(&mut env, &key_expr_str_2, key_expr_ptr_2)?; + Ok(key_expr_1.intersects(&key_expr_2) as jboolean) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + false as jboolean + }) } +/// Returns true in case key_expr_1 includes key_expr_2. +/// +/// # Params: +/// - `key_expr_ptr_1`: Pointer to the key expression 1, differs from null only if it's a declared key expr. +/// - `key_expr_ptr_1`: String representation of the key expression 1. +/// - `key_expr_ptr_2`: Pointer to the key expression 2, differs from null only if it's a declared key expr. +/// - `key_expr_ptr_2`: String representation of the key expression 2. +/// +/// # Safety +/// - This function is marked as unsafe due to raw pointer manipulation, which happens only when providing +/// key expressions that were declared from a session (in that case the key expression has a pointer associated). +/// In that case, this function assumes the pointers are valid pointers to key expressions and those pointers +/// remain valid after the call to this function. +/// #[no_mangle] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_includesViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_00024Companion_includesViaJNI( mut env: JNIEnv, _: JClass, - key_expr_ptr_1: *const KeyExpr<'static>, + key_expr_ptr_1: /*nullable*/ *const KeyExpr<'static>, key_expr_str_1: JString, - key_expr_ptr_2: *const KeyExpr<'static>, + key_expr_ptr_2: /*nullable*/ *const KeyExpr<'static>, key_expr_str_2: JString, ) -> jboolean { - let key_expr_1 = - match process_kotlin_key_expr_or_throw(&mut env, &key_expr_str_1, key_expr_ptr_1) { - Ok(key_expr) => key_expr, - Err(_) => return false as jboolean, - }; - let key_expr_2 = - match process_kotlin_key_expr_or_throw(&mut env, &key_expr_str_2, key_expr_ptr_2) { - Ok(key_expr) => key_expr, - Err(_) => return false as jboolean, - }; - key_expr_1.includes(&key_expr_2) as jboolean + || -> Result { + let key_expr_1 = process_kotlin_key_expr(&mut env, &key_expr_str_1, key_expr_ptr_1)?; + let key_expr_2 = process_kotlin_key_expr(&mut env, &key_expr_str_2, key_expr_ptr_2)?; + Ok(key_expr_1.includes(&key_expr_2) as jboolean) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + false as jboolean + }) } +/// Frees a declared key expression. +/// +/// # Parameters +/// - `_env`: Unused. The JNI environment. +/// - `_class`: Unused. The java class from which the function was called. +/// - `key_expr_ptr`: the pointer to the key expression. +/// +/// # Safety +/// - This function assumes the provided pointer is valid and points to a native key expression. +/// - The memory associated to the pointer is freed after returning from this call, turning the +/// pointer invalid after that. +/// #[no_mangle] #[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_freePtrViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIKeyExpr_freePtrViaJNI( _env: JNIEnv, _: JClass, - ptr: *const KeyExpr<'static>, + key_expr_ptr: *const KeyExpr<'static>, ) { - Arc::from_raw(ptr); + Arc::from_raw(key_expr_ptr); } -pub(crate) fn decode_key_expr(env: &mut JNIEnv, key_expr: &JString) -> Result> { - let key_expr_str = decode_string(env, key_expr).map_err(|err| { - Error::Jni(format!( - "Unable to get key expression string value: {}", - err - )) - })?; +fn validate_key_expr(env: &mut JNIEnv, key_expr: &JString) -> Result> { + let key_expr_str = decode_string(env, key_expr) + .map_err(|err| jni_error!("Unable to get key expression string value: '{}'.", err))?; - KeyExpr::try_from(key_expr_str).map_err(|err| { - Error::Jni(format!( - "Unable to create key expression from string: {}", - err - )) - }) + KeyExpr::try_from(key_expr_str) + .map_err(|err| key_expr_error!("Unable to create key expression: '{}'.", err)) } -pub(crate) fn autocanonize_key_expr( - env: &mut JNIEnv, - key_expr: &JString, -) -> Result> { +fn autocanonize_key_expr(env: &mut JNIEnv, key_expr: &JString) -> Result> { decode_string(env, key_expr) - .map_err(|err| { - Error::Jni(format!( - "Unable to get key expression string value: {}", - err - )) - }) + .map_err(|err| jni_error!("Unable to get key expression string value: '{}'.", err)) .and_then(|key_expr_str| { - KeyExpr::autocanonize(key_expr_str).map_err(|err| { - Error::Jni(format!( - "Unable to create key expression from string: {}", - err - )) - }) + KeyExpr::autocanonize(key_expr_str) + .map_err(|err| key_expr_error!("Unable to create key expression: '{}'", err)) }) } @@ -164,14 +192,20 @@ pub(crate) fn autocanonize_key_expr( /// If the pointer is valid, the key expression returned is the key expression the pointer pointed to. /// Otherwise, a key expression created from the string representation of the key expression is returned. /// +/// # Safety: +/// +/// The key_expr_str argument provided should already have been validated upon creation of the +/// KeyExpr instance on Kotlin. +/// pub(crate) unsafe fn process_kotlin_key_expr( env: &mut JNIEnv, key_expr_str: &JString, key_expr_ptr: *const KeyExpr<'static>, ) -> Result> { if key_expr_ptr.is_null() { - decode_key_expr(env, key_expr_str) - .map_err(|err| Error::Jni(format!("Unable to process key expression: {}", err))) + let key_expr = decode_string(env, key_expr_str) + .map_err(|err| jni_error!("Unable to get key expression string value: '{}'.", err))?; + Ok(KeyExpr::from_string_unchecked(key_expr)) } else { let key_expr = Arc::from_raw(key_expr_ptr); let key_expr_clone = key_expr.deref().clone(); @@ -179,20 +213,3 @@ pub(crate) unsafe fn process_kotlin_key_expr( Ok(key_expr_clone) } } - -/// Internal helper function that wraps [process_kotlin_key_expr] and throws an Exception through the JVM in -/// case of error. -unsafe fn process_kotlin_key_expr_or_throw( - env: &mut JNIEnv, - key_expr_str: &JString, - key_expr_ptr: *const KeyExpr<'static>, -) -> core::result::Result, ()> { - process_kotlin_key_expr(env, key_expr_str, key_expr_ptr).map_err(|err| { - let _ = err.throw_on_jvm(env).map_err(|err| { - tracing::error!( - "Unable to throw exception while processing key expression: '{}'.", - err - ) - }); - }) -} diff --git a/zenoh-jni/src/lib.rs b/zenoh-jni/src/lib.rs index 521d2c46d..a982babe4 100644 --- a/zenoh-jni/src/lib.rs +++ b/zenoh-jni/src/lib.rs @@ -16,15 +16,12 @@ mod errors; mod key_expr; mod logger; mod publisher; -mod put; mod query; mod queryable; -mod reply; -mod sample; mod session; mod subscriber; mod utils; -mod value; +mod zbytes; // Test should be runned with `cargo test --no-default-features` #[test] diff --git a/zenoh-jni/src/logger.rs b/zenoh-jni/src/logger.rs index ffdc0be4b..85362ac47 100644 --- a/zenoh-jni/src/logger.rs +++ b/zenoh-jni/src/logger.rs @@ -17,7 +17,7 @@ use jni::{ JNIEnv, }; -use crate::errors::{Error, Result}; +use crate::{errors::Result, jni_error, throw_exception}; /// Redirects the Rust logs either to logcat for Android systems or to the standard output (for non-Android systems). /// @@ -25,12 +25,12 @@ use crate::errors::{Error, Result}; /// indicating the desired log level, which must be one of the following: "info", "debug", "warn", /// "trace", or "error". /// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. /// - `log_level`: The log level java string indicating the desired log level. /// -/// Errors: +/// # Errors: /// - If there is an error parsing the log level string, a `JNIException` is thrown on the JVM. /// #[no_mangle] @@ -40,45 +40,22 @@ pub extern "C" fn Java_io_zenoh_Logger_00024Companion_start( _class: JClass, log_level: JString, ) { - let log_level = match parse_log_level(&mut env, log_level) { - Ok(level) => level, - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!("Error throwing exception on log start failure! {}", err) - }); - return; - } - }; - android_logd_logger::builder() - .parse_filters(log_level.as_str()) - .tag_target_strip() - .prepend_module(true) - .init(); + || -> Result<()> { + let log_level = parse_log_level(&mut env, log_level)?; + android_logd_logger::builder() + .parse_filters(log_level.as_str()) + .tag_target_strip() + .prepend_module(true) + .init(); + Ok(()) + }() + .unwrap_or_else(|err| throw_exception!(env, err)) } -/// Parses the log level string from the JNI environment into a Rust String. -/// -/// This function takes a mutable reference to the JNI environment (`env`) and a `log_level` -/// indicating the desired log level as a JString. It retrieves the log level string from the JNI -/// environment and converts it into a Rust String. -/// -/// Parameters: -/// - `env`: A mutable reference to the JNI environment. -/// - `log_level`: The log level as a JString. -/// -/// Returns: -/// - A [Result] containing the parsed log level string as a [String]. -/// -/// Errors: -/// - If there is an error retrieving or converting the log level string, a [Error::Jni] is returned -/// with the error message. -/// fn parse_log_level(env: &mut JNIEnv, log_level: JString) -> Result { - let log_level = env - .get_string(&log_level) - .map_err(|err| Error::Jni(err.to_string()))?; + let log_level = env.get_string(&log_level).map_err(|err| jni_error!(err))?; log_level .to_str() .map(|level| Ok(level.to_string())) - .map_err(|err| Error::Jni(err.to_string()))? + .map_err(|err| jni_error!(err))? } diff --git a/zenoh-jni/src/publisher.rs b/zenoh-jni/src/publisher.rs index 995cbca60..15e59c2f1 100644 --- a/zenoh-jni/src/publisher.rs +++ b/zenoh-jni/src/publisher.rs @@ -19,40 +19,30 @@ use jni::{ sys::jint, JNIEnv, }; -use zenoh::{ - prelude::{sync::SyncResolve, KeyExpr}, - publication::Publisher, - Session, SessionDeclarations, -}; +use zenoh::{bytes::EncodingBuilderTrait, pubsub::Publisher, sample::SampleBuilderTrait, Wait}; use crate::{ - errors::{Error, Result}, - key_expr::process_kotlin_key_expr, - utils::{decode_byte_array, vec_to_attachment}, -}; -use crate::{ - put::{decode_congestion_control, decode_priority}, - value::decode_value, + errors::Result, + utils::{decode_byte_array, decode_encoding}, }; +use crate::{session_error, throw_exception}; -/// Performs a put operation on a Zenoh publisher via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `payload`: The payload to be published, represented as a Java byte array (`JByteArray`). -/// - `encoding`: The encoding type of the payload. -/// - `encoded_attachment`: Optional encoded attachment. May be null. -/// - `ptr`: The raw pointer to the Zenoh publisher ([Publisher]). -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided publisher pointer is valid and has not been modified or freed. -/// - The ownership of the publisher is not transferred, and it is safe to continue using the publisher -/// after this function call. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// Performs a PUT operation on a Zenoh publisher via JNI. +/// +/// # Parameters +/// - `env`: The JNI environment pointer. +/// - `_class`: The Java class reference (unused). +/// - `payload`: The byte array to be published. +/// - `encoding_id`: The encoding ID of the payload. +/// - `encoding_schema`: Nullable encoding schema string of the payload. +/// - `attachment`: Nullble byte array for the attachment. +/// - `publisher_ptr`: The raw pointer to the Zenoh publisher ([Publisher]). +/// +/// # Safety +/// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - Assumes that the provided publisher pointer is valid and has not been modified or freed. +/// - The publisher pointer remains valid after this function call. +/// - May throw an exception in case of failure, which must be handled by the caller. /// #[no_mangle] #[allow(non_snake_case)] @@ -60,273 +50,80 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_putViaJNI( mut env: JNIEnv, _class: JClass, payload: JByteArray, - encoding: jint, - encoded_attachment: JByteArray, - ptr: *const Publisher<'static>, -) { - let publisher = Arc::from_raw(ptr); - match perform_put( - &env, - payload, - encoding, - encoded_attachment, - publisher.clone(), - ) { - Ok(_) => {} - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on PUT operation failure: {}", - err - ) - }); - } - }; - std::mem::forget(publisher) -} - -/// Frees the memory associated with a Zenoh publisher raw pointer via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `_env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `ptr`: The raw pointer to the Zenoh publisher ([Publisher]). -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation. -/// - It assumes that the provided publisher pointer is valid and has not been modified or freed. -/// - The function takes ownership of the raw pointer and releases the associated memory. -/// - After calling this function, the publisher pointer becomes invalid and should not be used anymore. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI( - _env: JNIEnv, - _: JClass, - ptr: *const Publisher, -) { - Arc::from_raw(ptr); -} - -/// Declares a Zenoh publisher via JNI. -/// -/// Parameters: -/// - `env`: A mutable reference to the JNI environment. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher. -/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the publisher. -/// It is only considered when the key_expr_ptr parameter is null, meaning the function is -/// receiving a key expression that was not declared. -/// - `session_ptr`: Raw pointer to the Zenoh [Session] to be used for the publisher. -/// - `congestion_control`: The [zenoh::CongestionControl] configuration as an ordinal. -/// - `priority`: The [zenoh::Priority] configuration as an ordinal. -/// -/// Returns: -/// - A [Result] containing a raw pointer to the declared Zenoh publisher ([Publisher]) in case of success, -/// or an [Error] variant in case of failure. -/// -/// Safety: -/// - The returned raw pointer should be stored appropriately and later freed using [Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI]. -/// -pub(crate) unsafe fn declare_publisher( - env: &mut JNIEnv, - key_expr_ptr: *const KeyExpr<'static>, - key_expr_str: JString, - session_ptr: *const Session, - congestion_control: jint, - priority: jint, -) -> Result<*const Publisher<'static>> { - let session = Arc::from_raw(session_ptr); - let key_expr = process_kotlin_key_expr(env, &key_expr_str, key_expr_ptr)?; - let congestion_control = decode_congestion_control(congestion_control)?; - let priority = decode_priority(priority)?; - let result = session - .declare_publisher(key_expr) - .congestion_control(congestion_control) - .priority(priority) - .res(); - std::mem::forget(session); - match result { - Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), - Err(err) => Err(Error::Session(err.to_string())), - } -} - -/// Performs a PUT operation via JNI using the specified Zenoh publisher. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `payload`: The payload as a `JByteArray`. -/// - `encoding`: The encoding of the payload. -/// - `encoded_attachment`: Optional encoded attachment. May be null. -/// - `publisher`: The Zenoh publisher. -/// -/// Returns: -/// - A [Result] indicating the success or failure of the operation. -/// -fn perform_put( - env: &JNIEnv, - payload: JByteArray, - encoding: jint, - encoded_attachment: JByteArray, - publisher: Arc, -) -> Result<()> { - let value = decode_value(env, payload, encoding)?; - let mut publication = publisher.put(value); - if !encoded_attachment.is_null() { - let aux = decode_byte_array(env, encoded_attachment)?; - publication = publication.with_attachment(vec_to_attachment(aux)) - }; - publication - .res_sync() - .map_err(|err| Error::Session(err.to_string())) -} - -/// Modifies the congestion control policy of a running Publisher through JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The Publisher class (unused but required). -/// - `congestion_control`: The [zenoh::CongestionControl] value to be set. -/// - `ptr`: Pointer to the publisher. -/// -/// Safety: -/// - This function is maked as unsafe due to raw pointer manipulation. -/// - This function is NOT thread safe; if there were to be multiple threads calling this function -/// concurrently while providing the same Publisher pointer, the result will be non deterministic. -/// -/// Throws: -/// - An exception in case the congestion control fails to be decoded. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_setCongestionControlViaJNI( - env: &mut JNIEnv, - _class: JClass, - congestion_control: jint, - ptr: *const Publisher<'static>, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, + attachment: /*nullable*/ JByteArray, + publisher_ptr: *const Publisher<'static>, ) { - let congestion_control = match decode_congestion_control(congestion_control) { - Ok(congestion_control) => congestion_control, - Err(err) => { - _ = err.throw_on_jvm(env); - return; - } - }; - tracing::debug!("Setting publisher congestion control with '{congestion_control:?}'."); - unsafe { - let publisher = core::ptr::read(ptr); - core::ptr::write( - ptr as *mut _, - publisher.congestion_control(congestion_control), - ); - } + let publisher = Arc::from_raw(publisher_ptr); + let _ = || -> Result<()> { + let payload = decode_byte_array(&env, payload)?; + let mut publication = publisher.put(payload); + let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + publication = publication.encoding(encoding); + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + publication = publication.attachment::>(attachment) + }; + publication.wait().map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); + std::mem::forget(publisher); } -/// Modifies the priority policy of a running Publisher through JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The Publisher class (unused but required). -/// - `priority`: The [zenoh::Priority] value to be set. -/// - `ptr`: Pointer to the publisher. +/// Performs a DELETE operation on a Zenoh publisher via JNI. /// -/// Safety: -/// - This function is maked as unsafe due to raw pointer manipulation. -/// - This function is NOT thread safe; if there were to be multiple threads calling this function -/// concurrently while providing the same Publisher pointer, the result will be non deterministic. +/// # Parameters +/// - `env`: The JNI environment pointer. +/// - `_class`: The Java class reference (unused). +/// - `attachment`: Nullble byte array for the attachment. +/// - `publisher_ptr`: The raw pointer to the Zenoh publisher ([Publisher]). /// -/// Throws: -/// - An exception in case the priority fails to be decoded. +/// # Safety +/// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - Assumes that the provided publisher pointer is valid and has not been modified or freed. +/// - The publisher pointer remains valid after this function call. +/// - May throw an exception in case of failure, which must be handled by the caller. /// #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_setPriorityViaJNI( - env: &mut JNIEnv, +pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( + mut env: JNIEnv, _class: JClass, - priority: jint, - ptr: *const Publisher<'static>, + attachment: /*nullable*/ JByteArray, + publisher_ptr: *const Publisher<'static>, ) { - let priority = match decode_priority(priority) { - Ok(priority) => priority, - Err(err) => { - _ = err.throw_on_jvm(env); - return; - } - }; - tracing::debug!("Setting publisher priority with '{priority:?}'."); - unsafe { - let publisher = core::ptr::read(ptr); - core::ptr::write(ptr as *mut _, publisher.priority(priority)); - } -} - -/// Performs a DELETE operation via JNI using the specified Zenoh publisher. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `encoded_attachment`: Optional encoded attachment. May be null. -/// - `publisher`: The Zenoh [Publisher]. -/// -/// Returns: -/// - A [Result] indicating the success or failure of the operation. -/// -fn perform_delete( - env: &JNIEnv, - encoded_attachment: JByteArray, - publisher: Arc, -) -> Result<()> { - let mut delete = publisher.delete(); - if !encoded_attachment.is_null() { - let aux = decode_byte_array(env, encoded_attachment)?; - delete = delete.with_attachment(vec_to_attachment(aux)) - }; - delete - .res() - .map_err(|err| Error::Session(format!("{}", err))) + let publisher = Arc::from_raw(publisher_ptr); + let _ = || -> Result<()> { + let mut delete = publisher.delete(); + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + delete = delete.attachment::>(attachment) + }; + delete.wait().map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); + std::mem::forget(publisher) } -/// Performs a DELETE operation on a Zenoh publisher via JNI. +/// Frees the publisher. /// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. +/// # Parameters: +/// - `_env`: The JNI environment. /// - `_class`: The JNI class. -/// - `encoded_attachment`: Optional encoded attachment. May be null. -/// - `ptr`: The raw pointer to the [Zenoh publisher](Publisher). +/// - `publisher_ptr`: The raw pointer to the Zenoh publisher ([Publisher]). /// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// # Safety: +/// - The function is marked as unsafe due to raw pointer manipulation. /// - It assumes that the provided publisher pointer is valid and has not been modified or freed. -/// - The ownership of the publisher is not transferred, and it is safe to continue using the publisher -/// after this function call. -/// - The function may throw an exception in case of failure, which should be handled by the caller. +/// - After calling this function, the publisher pointer becomes invalid and should not be used anymore. /// #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_deleteViaJNI( - mut env: JNIEnv, - _class: JClass, - encoded_attachment: JByteArray, - ptr: *const Publisher<'static>, +pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI( + _env: JNIEnv, + _: JClass, + publisher_ptr: *const Publisher, ) { - let publisher = Arc::from_raw(ptr); - match perform_delete(&env, encoded_attachment, publisher.clone()) { - Ok(_) => {} - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on WRITE operation failure: {}", - err - ) - }); - } - }; - std::mem::forget(publisher) + Arc::from_raw(publisher_ptr); } diff --git a/zenoh-jni/src/put.rs b/zenoh-jni/src/put.rs deleted file mode 100644 index bef96b4de..000000000 --- a/zenoh-jni/src/put.rs +++ /dev/null @@ -1,116 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use crate::errors::{Error, Result}; -use crate::key_expr::process_kotlin_key_expr; -use crate::sample::decode_sample_kind; -use crate::utils::{decode_byte_array, vec_to_attachment}; -use crate::value::decode_value; - -use jni::objects::{JByteArray, JString}; -use jni::sys::jint; -use jni::JNIEnv; -use std::sync::Arc; -use zenoh::prelude::r#sync::*; - -/// Performs a `put` operation in the Zenoh session, propagating eventual errors. -/// -/// Parameters: -/// - `env`: A mutable reference to the JNI environment. -/// - `key_expr_ptr`: Raw pointer of a declared [KeyExpr] to use for the put operation. If it is null, then -/// the key_expr_str parameter is used. -/// - `key_expr_str`: String representation of the key expression to be used for the put operation. If the key_expr_ptr -/// is valid, then this parameter won't be considered. -/// - `session`: An [Session] to use for the put operation. -/// - `payload`: The payload to send through the network. -/// - `encoding`: The [Encoding] of the put operation. -/// - `congestion_control`: The [CongestionControl] mechanism specified. -/// - `priority`: The [Priority] mechanism specified. -/// - `sample_kind`: The [SampleKind] of the put operation. -/// - `attachment`: An optional attachment, encoded into a byte array. May be null. -/// -/// Returns: -/// - A `Result` indicating the result of the `get` operation, with an [Error] in case of failure. -/// -#[allow(clippy::too_many_arguments)] -pub(crate) fn on_put( - env: &mut JNIEnv, - key_expr_ptr: *const KeyExpr<'static>, - key_expr_str: JString, - session: &Arc, - payload: JByteArray, - encoding: jint, - congestion_control: jint, - priority: jint, - sample_kind: jint, - attachment: JByteArray, -) -> Result<()> { - let key_expr = unsafe { process_kotlin_key_expr(env, &key_expr_str, key_expr_ptr) }?; - let value = decode_value(env, payload, encoding)?; - let sample_kind = decode_sample_kind(sample_kind)?; - let congestion_control = match decode_congestion_control(congestion_control) { - Ok(congestion_control) => congestion_control, - Err(err) => { - tracing::warn!( - "Error decoding congestion control: '{}'. Using default...", - err - ); - CongestionControl::default() - } - }; - - let priority = match decode_priority(priority) { - Ok(priority) => priority, - Err(err) => { - tracing::warn!("Error decoding priority: '{}'. Using default...", err); - Priority::default() - } - }; - - let mut put_builder = session - .put(&key_expr, value.to_owned()) - .kind(sample_kind) - .congestion_control(congestion_control) - .priority(priority); - - if !attachment.is_null() { - let attachment = decode_byte_array(env, attachment)?; - put_builder = put_builder.with_attachment(vec_to_attachment(attachment)) - } - - match put_builder.res() { - Ok(_) => { - tracing::trace!("Put on '{key_expr}' with value '{value}' and encoding '{}'. Kind: '{sample_kind}', Congestion control: '{congestion_control:?}', Priority: '{priority:?}'", value.encoding); - Ok(()) - } - Err(err) => Err(Error::Session(format!("{}", err))), - } -} - -pub(crate) fn decode_priority(priority: jint) -> Result { - match Priority::try_from(priority as u8) { - Ok(priority) => Ok(priority), - Err(err) => Err(Error::Session(format!("Error retrieving priority: {err}."))), - } -} - -pub(crate) fn decode_congestion_control(congestion_control: jint) -> Result { - match congestion_control { - 1 => Ok(CongestionControl::Block), - 0 => Ok(CongestionControl::Drop), - _value => Err(Error::Session(format!( - "Unknown congestion control '{_value}'." - ))), - } -} diff --git a/zenoh-jni/src/query.rs b/zenoh-jni/src/query.rs index 5f690e262..fae0dd519 100644 --- a/zenoh-jni/src/query.rs +++ b/zenoh-jni/src/query.rs @@ -12,55 +12,51 @@ // ZettaScale Zenoh Team, // -use std::{mem, sync::Arc}; +use std::sync::Arc; +use crate::{errors::Result, key_expr::process_kotlin_key_expr, throw_exception}; +use crate::{ + session_error, + utils::{decode_byte_array, decode_encoding}, +}; use jni::{ - objects::{GlobalRef, JByteArray, JClass, JPrimitiveArray, JString, JValue}, - sys::{jboolean, jbyte, jint, jlong}, + objects::{JByteArray, JClass, JString}, + sys::{jboolean, jint, jlong}, JNIEnv, }; +use uhlc::ID; use zenoh::{ - prelude::{sync::SyncResolve, KeyExpr, SplitBuffer}, - query::{ConsolidationMode, QueryTarget}, - queryable::Query, - sample::{Attachment, Sample}, - value::Value, + key_expr::KeyExpr, + prelude::{EncodingBuilderTrait, Wait}, + qos::{CongestionControl, Priority, QoSBuilderTrait}, + query::Query, + sample::{SampleBuilderTrait, TimestampBuilderTrait}, + time::{Timestamp, NTP64}, }; -use crate::{ - errors::{Error, Result}, - key_expr::process_kotlin_key_expr, - utils::attachment_to_vec, - value::decode_value, -}; -use crate::{ - sample::decode_sample, - utils::{decode_byte_array, vec_to_attachment}, -}; - -/// Replies with success to a Zenoh query via JNI. +/// Replies with `success` to a Zenoh [Query] via JNI, freeing the query in the process. /// -/// This function is meant to be called from Java/Kotlin through JNI. -/// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. -/// - `ptr`: The raw pointer to the Zenoh query. -/// - `key_expr_ptr`: The key expression pointer associated with the query result. This parameter +/// - `query_ptr`: The raw pointer to the Zenoh query. +/// - `key_expr_ptr`: Nullable key expression pointer associated with the query result. This parameter /// is meant to be used with declared key expressions, which have a pointer associated to them. /// In case of it being null, then the `key_expr_string` will be used to perform the reply. -/// - `key_expr_string`: The string representation of the key expression associated with the query result. -/// - `payload`: The payload as a `JByteArray`. -/// - `encoding`: The encoding of the payload. -/// - `sample_kind`: The kind of sample. +/// - `key_expr_str`: The string representation of the key expression associated with the query result. +/// - `payload`: The payload for the reply. +/// - `encoding_id`: The encoding id of the payload. +/// - `encoding_schema`: Nullable encoding schema. /// - `timestamp_enabled`: A boolean indicating whether the timestamp is enabled. /// - `timestamp_ntp_64`: The NTP64 timestamp value. -/// - `attachment`: Optional user attachment encoded as a byte array. May be null. +/// - `attachment`: Nullable user attachment encoded as a byte array. +/// - `qos_*`: QoS parameters for the reply. /// -/// Safety: +/// # Safety: /// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. /// - It assumes that the provided raw pointer to the Zenoh query is valid and has not been modified or freed. -/// - The ownership of the Zenoh query is not transferred, and it remains valid after this call. +/// - The query pointer is freed after calling this function (queries shouldn't be replied more than once), +/// therefore the query isn't valid anymore after that. /// - May throw a JNI exception in case of failure, which should be handled by the caller. /// #[no_mangle] @@ -68,130 +64,145 @@ use crate::{ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replySuccessViaJNI( mut env: JNIEnv, _class: JClass, - query_ptr: *const zenoh::queryable::Query, - key_expr_ptr: *const KeyExpr<'static>, + query_ptr: *const Query, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, payload: JByteArray, - encoding: jint, - sample_kind: jint, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, timestamp_enabled: jboolean, timestamp_ntp_64: jlong, - qos: jbyte, - attachment: JByteArray, + attachment: /*nullable*/ JByteArray, + qos_express: jboolean, + qos_priority: jint, + qos_congestion_control: jint, ) { - let key_expr = match process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr) { - Ok(key_expr) => key_expr, - Err(err) => { - if let Err(err) = err.throw_on_jvm(&mut env) { - tracing::error!("{}", err); - } - return; + let _ = || -> Result<()> { + let query = Arc::from_raw(query_ptr); + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let payload = decode_byte_array(&env, payload)?; + let mut reply_builder = query.reply(key_expr, payload); + let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + reply_builder = reply_builder.encoding(encoding); + if timestamp_enabled != 0 { + let ts = Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand()); + reply_builder = reply_builder.timestamp(ts) } - }; - - let sample = match decode_sample( - &mut env, - key_expr, - payload, - encoding, - sample_kind, - timestamp_enabled, - timestamp_ntp_64, - qos, - ) { - Ok(sample) => sample, - Err(err) => { - if let Err(err) = err.throw_on_jvm(&mut env) { - tracing::error!("Unable to throw exception on query reply failure: {}", err); - } - return; + if !attachment.is_null() { + reply_builder = reply_builder.attachment(decode_byte_array(&env, attachment)?); } - }; - - let attachment = if !attachment.is_null() { - match decode_byte_array(&env, attachment) { - Ok(attachment_bytes) => Some(vec_to_attachment(attachment_bytes)), - Err(err) => { - if let Err(err) = err.throw_on_jvm(&mut env) { - tracing::error!("Unable to throw exception on query reply failure: {}", err); - } - return; - } - } - } else { - None - }; - - let query = Arc::from_raw(query_ptr); - query_reply(env, &query, Ok(sample), attachment); - mem::forget(query); + reply_builder = reply_builder.express(qos_express != 0); + reply_builder = reply_builder.priority(Priority::try_from(qos_priority as u8).unwrap()); // The numeric value is always within range. + reply_builder = if qos_congestion_control != 0 { + reply_builder.congestion_control(CongestionControl::Block) + } else { + reply_builder.congestion_control(CongestionControl::Drop) + }; + reply_builder.wait().map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); } -/// Replies with error to a Zenoh query via JNI. +/// Replies with `error` to a Zenoh [Query] via JNI, freeing the query in the process. /// -/// This function is meant to be called from Java/Kotlin through JNI. -/// -/// Support: -/// - Replying with error is a feature that is not yet supported by Zenoh. This implementation is -/// meant to prepare the API to these expected changes. Calling this function now would cause an -/// exception. -/// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. -/// - `ptr`: The raw pointer to the Zenoh query. -/// - `key_expr`: The key expression associated with the query result. -/// - `payload`: The payload as a `JByteArray`. -/// - `encoding`: The encoding of the payload as a jint. -/// - `attachment`: The user attachment bytes. +/// - `query_ptr`: The raw pointer to the Zenoh query. +/// - `payload`: The payload for the reply. +/// - `encoding_id`: The encoding id of the payload. +/// - `encoding_schema`: Nullable encoding schema. /// -/// Safety: +/// # Safety: /// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. /// - It assumes that the provided raw pointer to the Zenoh query is valid and has not been modified or freed. -/// - The ownership of the Zenoh query is not transferred, and it remains valid after this call. /// - May throw a JNI exception in case of failure, which should be handled by the caller. +/// - The query pointer is freed after calling this function (queries shouldn't be replied more than once), +/// therefore the query isn't valid anymore after that. /// #[no_mangle] #[allow(non_snake_case)] pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( mut env: JNIEnv, _class: JClass, - ptr: *const zenoh::queryable::Query, + query_ptr: *const Query, payload: JByteArray, - encoding: jint, - attachment: JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, ) { - let errorValue = match decode_value(&env, payload, encoding) { - Ok(value) => value, - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!("Unable to throw exception on query reply failure. {}", err) - }); - return; + let _ = || -> Result<()> { + let query = Arc::from_raw(query_ptr); + let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + query + .reply_err(decode_byte_array(&env, payload)?) + .encoding(encoding) + .wait() + .map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); +} + +/// Replies with `delete` to a Zenoh [Query] via JNI, freeing the query in the process. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `query_ptr`: The raw pointer to the Zenoh query. +/// - `key_expr_ptr`: Nullable key expression pointer associated with the query result. This parameter +/// is meant to be used with declared key expressions, which have a pointer associated to them. +/// In case of it being null, then the `key_expr_string` will be used to perform the reply. +/// - `key_expr_str`: The string representation of the key expression associated with the query result. +/// - `timestamp_enabled`: A boolean indicating whether the timestamp is enabled. +/// - `timestamp_ntp_64`: The NTP64 timestamp value. +/// - `attachment`: Nullable user attachment encoded as a byte array. +/// - `qos_*`: QoS parameters for the reply. +/// +/// # Safety: +/// - This function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided raw pointer to the Zenoh query is valid and has not been modified or freed. +/// - May throw a JNI exception in case of failure, which should be handled by the caller. +/// - The query pointer is freed after calling this function (queries shouldn't be replied more than once), +/// therefore the query isn't valid anymore after that. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyDeleteViaJNI( + mut env: JNIEnv, + _class: JClass, + query_ptr: *const Query, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + timestamp_enabled: jboolean, + timestamp_ntp_64: jlong, + attachment: /*nullable*/ JByteArray, + qos_express: jboolean, + qos_priority: jint, + qos_congestion_control: jint, +) { + let _ = || -> Result<()> { + let query = Arc::from_raw(query_ptr); + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let mut reply_builder = query.reply_del(key_expr); + if timestamp_enabled != 0 { + let ts = Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand()); + reply_builder = reply_builder.timestamp(ts) } - }; - let attachment: Option = if !attachment.is_null() { - match decode_byte_array(&env, attachment) { - Ok(attachment_bytes) => Some(vec_to_attachment(attachment_bytes)), - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!("Unable to throw exception on query reply failure. {}", err) - }); - return; - } + if !attachment.is_null() { + reply_builder = reply_builder.attachment(decode_byte_array(&env, attachment)?); } - } else { - None - }; - - let query = Arc::from_raw(ptr); - query_reply(env, &query, Err(errorValue), attachment); - mem::forget(query) + reply_builder = reply_builder.express(qos_express != 0); + reply_builder = reply_builder.priority(Priority::try_from(qos_priority as u8).unwrap()); // The numeric value is always within range. + reply_builder = if qos_congestion_control != 0 { + reply_builder.congestion_control(CongestionControl::Block) + } else { + reply_builder.congestion_control(CongestionControl::Drop) + }; + reply_builder.wait().map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); } -/// Frees the memory associated with a Zenoh query raw pointer via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. +/// Frees the Query via JNI. /// /// Parameters: /// - `_env`: The JNI environment. @@ -209,169 +220,7 @@ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_replyErrorViaJNI( pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQuery_freePtrViaJNI( _env: JNIEnv, _: JClass, - ptr: *const zenoh::queryable::Query, + query_ptr: *const Query, ) { - Arc::from_raw(ptr); -} - -/// Handles a Zenoh query callback via JNI. -/// -/// This function is responsible for invoking the query callback function provided by the user from Java/Kotlin. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `query`: The Zenoh [Query] to be passed to the callback function. -/// - `callback_global_ref`: A global object reference of the callback function. -/// -/// Returns: -/// - A [Result] indicating success or failure. -/// -/// Note: -/// - The callback reference `callback_global_ref` should point to the Java/Kotlin implementation -/// of the `onQuery` function (which receives a `Query` as a parameter) from the `Callback` interface. -/// -pub(crate) fn on_query( - mut env: JNIEnv, - query: Query, - callback_global_ref: &GlobalRef, -) -> Result<()> { - let selector = query.selector(); - let value = query.value(); - - let selector_params_jstr = - env.new_string(selector.parameters().to_string()) - .map_err(|err| { - Error::Jni(format!( - "Could not create a JString through JNI for the Query key expression. {}", - err - )) - })?; - - let (with_value, payload, encoding) = if let Some(value) = value { - let byte_array = env - .byte_array_from_slice(value.payload.contiguous().as_ref()) - .map_err(|err| Error::Jni(err.to_string()))?; - let encoding: i32 = value.encoding.prefix().to_owned() as i32; - (true, byte_array, encoding) - } else { - (false, JPrimitiveArray::default(), 0) - }; - - let attachment_bytes = query - .attachment() - .map_or_else( - || env.byte_array_from_slice(&[]), - |attachment| { - env.byte_array_from_slice(attachment_to_vec(attachment.clone()).as_slice()) - }, - ) - .map_err(|err| Error::Jni(format!("Error processing attachment of reply: {}.", err)))?; - - let key_expr_str = env - .new_string(&query.key_expr().to_string()) - .map_err(|err| { - Error::Jni(format!( - "Could not create a JString through JNI for the Query key expression. {}", - err - )) - })?; - - let query_ptr = Arc::into_raw(Arc::new(query)); - - let result = env - .call_method( - callback_global_ref, - "run", - "(Ljava/lang/String;Ljava/lang/String;Z[BI[BJ)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&selector_params_jstr), - JValue::from(with_value), - JValue::from(&payload), - JValue::from(encoding), - JValue::from(&attachment_bytes), - JValue::from(query_ptr as jlong), - ], - ) - .map(|_| ()) - .map_err(|err| { - // The callback could not be invoked, therefore the created kotlin query object won't be - // used. Since `query_ptr` as well as `key_expr_ptr` was created within this function - // and remains unaltered, it is safe to reclaim ownership of the memory by converting - // the raw pointers back into an `Arc` and freeing the memory. - unsafe { - Arc::from_raw(query_ptr); - }; - _ = env.exception_describe(); - Error::Jni(format!("{}", err)) - }); - - _ = env - .delete_local_ref(key_expr_str) - .map_err(|err| tracing::error!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(selector_params_jstr) - .map_err(|err| tracing::error!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(payload) - .map_err(|err| tracing::error!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(attachment_bytes) - .map_err(|err| tracing::error!("Error deleting local ref: {}", err)); - result -} - -/// Helper function to perform a reply to a query. -fn query_reply( - mut env: JNIEnv, - query: &Arc, - reply: core::result::Result, - attachment: Option, -) { - let result = if let Some(attachment) = attachment { - query - .reply(reply) - .with_attachment(attachment) - .unwrap_or_else(|(builder, _)| { - tracing::warn!("Unable to append attachment to query reply"); - builder - }) - .res() - .map_err(|err| Error::Session(err.to_string())) - } else { - query - .reply(reply) - .res() - .map_err(|err| Error::Session(err.to_string())) - }; - match result { - Ok(_) => {} - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!("Unable to throw exception on query reply failure. {}", err) - }); - } - } -} - -pub(crate) fn decode_query_target(target: jint) -> Result { - match target { - 0 => Ok(QueryTarget::BestMatching), - 1 => Ok(QueryTarget::All), - 2 => Ok(QueryTarget::AllComplete), - value => Err(Error::Session(format!( - "Unable to decode QueryTarget {value}" - ))), - } -} - -pub(crate) fn decode_consolidation(consolidation: jint) -> Result { - match consolidation { - 0 => Ok(ConsolidationMode::None), - 1 => Ok(ConsolidationMode::Monotonic), - 2 => Ok(ConsolidationMode::Latest), - value => Err(Error::Session(format!( - "Unable to decode consolidation {value}" - ))), - } + Arc::from_raw(query_ptr); } diff --git a/zenoh-jni/src/queryable.rs b/zenoh-jni/src/queryable.rs index cef88748d..07ed6e1b4 100644 --- a/zenoh-jni/src/queryable.rs +++ b/zenoh-jni/src/queryable.rs @@ -14,31 +14,17 @@ use std::sync::Arc; -use jni::{ - objects::{JClass, JObject, JString}, - sys::jboolean, - JNIEnv, -}; -use zenoh::prelude::r#sync::*; -use zenoh::{queryable::Queryable, Session}; +use jni::{objects::JClass, JNIEnv}; +use zenoh::query::Queryable; -use crate::{ - errors::{Error, Result}, - key_expr::process_kotlin_key_expr, - query::on_query, - utils::{get_callback_global_ref, get_java_vm, load_on_close}, -}; - -/// Frees the memory associated with a Zenoh queryable raw pointer via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. +/// Frees the [Queryable]. /// -/// Parameters: +/// # Parameters: /// - `_env`: The JNI environment. /// - `_class`: The JNI class. -/// - `ptr`: The raw pointer to the Zenoh queryable ([Queryable]). +/// - `queryable_ptr`: The raw pointer to the Zenoh queryable ([Queryable]). /// -/// Safety: +/// # Safety: /// - The function is marked as unsafe due to raw pointer manipulation. /// - It assumes that the provided queryable pointer is valid and has not been modified or freed. /// - The function takes ownership of the raw pointer and releases the associated memory. @@ -49,70 +35,7 @@ use crate::{ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNIQueryable_freePtrViaJNI( _env: JNIEnv, _: JClass, - ptr: *const zenoh::queryable::Queryable<'_, ()>, + queryable_ptr: *const Queryable<'_, ()>, ) { - Arc::from_raw(ptr); -} - -/// Declares a Zenoh queryable via JNI. -/// -/// Parameters: -/// - `env`: A mutable reference to the JNI environment. -/// - `key_expr_ptr`: Raw pointer to a declared [KeyExpr] to be used for the queryable. May be null in case -/// of using a non declared key expression, in which case the key_expr_str parameter will be used instead. -/// - `key_expr_str`: String representation of the key expression to be used to declare the queryable. -/// - `session_ptr`: Raw pointer to the [Session] from which to declare the queryable.. -/// - `callback`: The callback function as an instance of the `Callback` interface in Java/Kotlin. -/// - `on_close`: The `on_close` callback function as an instance of the `JNIOnCloseCallback` interface in -/// Java/Kotlin, to be called when Zenoh notfies that no more queries will be received. -/// - `complete`: The completeness of the queryable. -/// -/// Returns: -/// - A [Result] containing a raw pointer to the declared Zenoh queryable ([Queryable]) in case of success, -/// or an [Error] variant in case of failure. -/// -/// Safety: -/// - The returned raw pointer should be stored appropriately and later freed using [Java_io_zenoh_jni_JNIQueryable_freePtrViaJNI]. -/// -pub(crate) unsafe fn declare_queryable( - env: &mut JNIEnv, - key_expr_ptr: *const KeyExpr<'static>, - key_expr_str: JString, - session_ptr: *const zenoh::Session, - callback: JObject, - on_close: JObject, - complete: jboolean, -) -> Result> { - let java_vm = Arc::new(get_java_vm(env)?); - let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let key_expr = process_kotlin_key_expr(env, &key_expr_str, key_expr_ptr)?; - let complete = complete != 0; - let on_close = load_on_close(&java_vm, on_close_global_ref); - let session: Arc = Arc::from_raw(session_ptr); - tracing::debug!("Declaring queryable through JNI on {}", key_expr); - let queryable = session - .declare_queryable(key_expr) - .callback(move |query| { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - let env = match java_vm.attach_current_thread_as_daemon() { - Ok(env) => env, - Err(err) => { - tracing::error!("Unable to attach thread for queryable callback: {}", err); - return; - } - }; - - tracing::debug!("Receiving query through JNI: {}", query.to_string()); - match on_query(env, query, &callback_global_ref) { - Ok(_) => tracing::debug!("Queryable callback called successfully."), - Err(err) => tracing::error!("Error calling queryable callback: {}", err), - } - }) - .complete(complete); - - std::mem::forget(session); - queryable - .res() - .map_err(|err| Error::Session(format!("Error declaring queryable: {}", err))) + Arc::from_raw(queryable_ptr); } diff --git a/zenoh-jni/src/reply.rs b/zenoh-jni/src/reply.rs deleted file mode 100644 index dad1307f8..000000000 --- a/zenoh-jni/src/reply.rs +++ /dev/null @@ -1,161 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use jni::{ - objects::{GlobalRef, JObject, JValue}, - sys::jint, - JNIEnv, -}; -use zenoh::{ - prelude::{SplitBuffer, ZenohId}, - query::Reply, - sample::Sample, - value::Value, -}; - -use crate::{errors::Error, utils::attachment_to_vec}; -use crate::{errors::Result, sample::qos_into_jbyte}; - -pub(crate) fn on_reply( - mut env: JNIEnv, - reply: Reply, - callback_global_ref: &GlobalRef, -) -> Result<()> { - match reply.sample { - Ok(sample) => on_reply_success(&mut env, reply.replier_id, sample, callback_global_ref), - Err(value) => on_reply_error(&mut env, reply.replier_id, value, callback_global_ref), - } -} - -fn on_reply_success( - env: &mut JNIEnv, - replier_id: ZenohId, - sample: Sample, - callback_global_ref: &GlobalRef, -) -> Result<()> { - let zenoh_id = env - .new_string(replier_id.to_string()) - .map_err(|err| Error::Jni(err.to_string()))?; - - let byte_array = env - .byte_array_from_slice(sample.value.payload.contiguous().as_ref()) - .map_err(|err| Error::Jni(err.to_string()))?; - - let encoding: jint = sample.value.encoding.prefix().to_owned() as jint; - let kind = sample.kind as jint; - - let (timestamp, is_valid) = sample - .timestamp - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = sample - .attachment - .map_or_else( - || env.byte_array_from_slice(&[]), - |attachment| env.byte_array_from_slice(attachment_to_vec(attachment).as_slice()), - ) - .map_err(|err| Error::Jni(format!("Error processing attachment of reply: {}.", err)))?; - - let key_expr_str = env.new_string(sample.key_expr.to_string()).map_err(|err| { - Error::Jni(format!( - "Could not create a JString through JNI for the Sample key expression. {}", - err - )) - })?; - - let result = match env.call_method( - callback_global_ref, - "run", - "(Ljava/lang/String;ZLjava/lang/String;[BIIJZB[B)V", - &[ - JValue::from(&zenoh_id), - JValue::from(true), - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(qos_into_jbyte(sample.qos)), - JValue::from(&attachment_bytes), - ], - ) { - Ok(_) => Ok(()), - Err(err) => { - _ = env.exception_describe(); - Err(Error::Jni(format!("On GET callback error: {}", err))) - } - }; - - _ = env - .delete_local_ref(key_expr_str) - .map_err(|err| tracing::error!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(zenoh_id) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(byte_array) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(attachment_bytes) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - result -} - -fn on_reply_error( - env: &mut JNIEnv, - replier_id: ZenohId, - value: Value, - callback_global_ref: &GlobalRef, -) -> Result<()> { - let zenoh_id = env - .new_string(replier_id.to_string()) - .map_err(|err| Error::Jni(err.to_string()))?; - - let byte_array = env - .byte_array_from_slice(value.payload.contiguous().as_ref()) - .map_err(|err| Error::Jni(err.to_string()))?; - let encoding: jint = value.encoding.prefix().to_owned() as jint; - - let result = match env.call_method( - callback_global_ref, - "run", - "(Ljava/lang/String;ZJ[BIIJZ)V", - &[ - JValue::from(&zenoh_id), - JValue::from(false), - JValue::from(&JObject::null()), - JValue::from(&byte_array), - JValue::from(encoding), - JValue::from(0), - JValue::from(0), - JValue::from(false), - ], - ) { - Ok(_) => Ok(()), - Err(err) => { - _ = env.exception_describe(); - Err(Error::Jni(format!("On GET callback error: {}", err))) - } - }; - - _ = env - .delete_local_ref(zenoh_id) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(byte_array) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - result -} diff --git a/zenoh-jni/src/sample.rs b/zenoh-jni/src/sample.rs deleted file mode 100644 index 43495c19a..000000000 --- a/zenoh-jni/src/sample.rs +++ /dev/null @@ -1,110 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use crate::{ - errors::{Error, Result}, - value::decode_value, -}; -use jni::{ - objects::{JByteArray, JClass}, - sys::{jboolean, jbyte, jint, jlong}, - JNIEnv, -}; -use uhlc::{Timestamp, ID, NTP64}; -use zenoh::{ - prelude::{KeyExpr, SampleKind}, - sample::{QoS, Sample}, -}; - -/// Attempts to reconstruct a Zenoh [Sample] from the Java/Kotlin fields specified. -#[allow(clippy::too_many_arguments)] -pub(crate) fn decode_sample( - env: &mut JNIEnv, - key_expr: KeyExpr<'static>, - payload: JByteArray, - encoding: jint, - sample_kind: jint, - timestamp_enabled: jboolean, - timestamp_ntp_64: jlong, - qos: jbyte, -) -> Result { - let value = decode_value(env, payload, encoding)?; - let mut sample = Sample::new(key_expr, value); - sample.kind = decode_sample_kind(sample_kind)?; - sample.timestamp = if timestamp_enabled != 0 { - Some(Timestamp::new(NTP64(timestamp_ntp_64 as u64), ID::rand())) - } else { - None - }; - sample.qos = qos_from_jbyte(qos); - Ok(sample) -} - -/// Converts a Java/Kotlin Integer into a [SampleKind]. -pub(crate) fn decode_sample_kind(sample_kind: jint) -> Result { - match SampleKind::try_from(sample_kind as u64) { - Ok(kind) => Ok(kind), - Err(sample_kind) => Err(Error::Jni(format!( - "Unable to process sample kind {}.", - sample_kind, - ))), - } -} - -pub fn qos_from_jbyte(qos: jbyte) -> QoS { - unsafe { std::mem::transmute::(qos) } -} - -pub fn qos_into_jbyte(qos: QoS) -> jbyte { - unsafe { std::mem::transmute::(qos) } -} - -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIQoS_getPriorityViaJNI( - _env: JNIEnv, - _class: JClass, - qos: jbyte, -) -> jint { - qos_from_jbyte(qos).priority() as jint -} - -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIQoS_getCongestionControlViaJNI( - _env: JNIEnv, - _class: JClass, - qos: jbyte, -) -> jint { - qos_from_jbyte(qos).congestion_control() as jint -} - -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIQoS_getExpressdViaJNI( - _env: JNIEnv, - _class: JClass, - qos: jbyte, -) -> jboolean { - qos_from_jbyte(qos).express() as jboolean -} - -#[no_mangle] -#[allow(non_snake_case)] -pub extern "C" fn Java_io_zenoh_jni_JNIQoS_00024Companion_getDefaultQoSViaJNI( - _env: JNIEnv, - _class: JClass, -) -> jbyte { - qos_into_jbyte(QoS::default()) -} diff --git a/zenoh-jni/src/session.rs b/zenoh-jni/src/session.rs index 69c9de7c9..5975c5f27 100644 --- a/zenoh-jni/src/session.rs +++ b/zenoh-jni/src/session.rs @@ -14,81 +14,85 @@ use crate::errors::{Error, Result}; use crate::key_expr::process_kotlin_key_expr; -use crate::publisher::declare_publisher; -use crate::put::on_put; -use crate::query::{decode_consolidation, decode_query_target}; -use crate::queryable::declare_queryable; -use crate::reply::on_reply; -use crate::subscriber::declare_subscriber; -use crate::utils::{ - decode_byte_array, decode_string, get_callback_global_ref, get_java_vm, load_on_close, - vec_to_attachment, -}; -use crate::value::decode_value; - -use jni::objects::{JByteArray, JClass, JObject, JString}; +use crate::{jni_error, utils::*}; +use crate::{session_error, throw_exception}; + +use jni::objects::{GlobalRef, JByteArray, JClass, JObject, JString, JValue}; use jni::sys::{jboolean, jint, jlong}; use jni::JNIEnv; +use std::mem; use std::ops::Deref; use std::ptr::null; use std::sync::Arc; use std::time::Duration; -use zenoh::config::Config; -use zenoh::prelude::r#sync::*; +use zenoh::config::{Config, ZenohId}; +use zenoh::key_expr::KeyExpr; +use zenoh::prelude::{EncodingBuilderTrait, Wait}; +use zenoh::pubsub::{Publisher, Subscriber}; +use zenoh::qos::QoSBuilderTrait; +use zenoh::query::{Query, Queryable, ReplyError, Selector}; +use zenoh::sample::{Sample, SampleBuilderTrait}; +use zenoh::session::{Session, SessionDeclarations}; /// Open a Zenoh session via JNI. /// -/// This function is meant to be called from Java/Kotlin through JNI. +/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute +/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require +/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. /// -/// Parameters: +/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. +/// +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `config`: The path to the Zenoh config file. -/// -/// Returns: -/// - An [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. -/// -/// If opening the session fails, a `SessionException` is thrown on the JVM, and a null pointer is returned. +/// - `config_path`: Nullable path to the Zenoh config file. If null, the default configuration will be loaded. /// #[no_mangle] #[allow(non_snake_case)] pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionViaJNI( mut env: JNIEnv, _class: JClass, - config_path: JString, -) -> *const zenoh::Session { + config_path: /*nullable*/ JString, +) -> *const Session { let session = open_session(&mut env, config_path); match session { Ok(session) => Arc::into_raw(Arc::new(session)), Err(err) => { tracing::error!("Unable to open session: {}", err); - _ = Error::Session(err.to_string()) - .throw_on_jvm(&mut env) - .map_err(|err| { - tracing::error!("Unable to throw exception on session failure: {}", err) - }); + throw_exception!(env, session_error!(err)); null() } } } -/// Open a Zenoh session via JNI. +/// Open a Zenoh session with the configuration pointed out by `config_path`. /// -/// This function is meant to be called from Java/Kotlin through JNI. +/// If the config path provided is null then the default configuration is loaded. /// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class (parameter required by the JNI interface but unused). -/// - `config`: The path to the Zenoh config file. +fn open_session(env: &mut JNIEnv, config_path: JString) -> Result { + let config = if config_path.is_null() { + Config::default() + } else { + let config_file_path = decode_string(env, &config_path)?; + Config::from_file(config_file_path).map_err(|err| session_error!(err))? + }; + zenoh::open(config) + .wait() + .map_err(|err| session_error!(err)) +} + +/// Open a Zenoh session with a JSON configuration. /// -/// Returns: -/// - An [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute -/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require -/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. +/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute +/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require +/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. /// -/// If opening the session fails, a `SessionException` is thrown on the JVM, and a null pointer is returned. +/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class (parameter required by the JNI interface but unused). +/// - `json_config`: Configuration as a JSON string. /// #[no_mangle] #[allow(non_snake_case)] @@ -96,190 +100,191 @@ pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithJsonConfigViaJNI( mut env: JNIEnv, _class: JClass, json_config: JString, -) -> *const zenoh::Session { +) -> *const Session { let session = open_session_with_json_config(&mut env, json_config); match session { Ok(session) => Arc::into_raw(Arc::new(session)), Err(err) => { tracing::error!("Unable to open session: {}", err); - _ = Error::Session(err.to_string()) - .throw_on_jvm(&mut env) - .map_err(|err| { - tracing::error!("Unable to throw exception on session failure: {}", err) - }); + throw_exception!(env, session_error!(err)); null() } } } -/// Open a Zenoh session. -/// -/// Loads the session with the provided by [config_path]. If the config path provided is empty then -/// the default configuration is loaded. +/// Open a Zenoh session with the provided json configuration. /// -/// Returns: -/// - A [Result] with a [zenoh::Session] in case of success or an [Error::Session] in case of failure. -/// -fn open_session(env: &mut JNIEnv, config_path: JString) -> Result { - let config_file_path = decode_string(env, &config_path)?; - let config = if config_file_path.is_empty() { - Config::default() - } else { - Config::from_file(config_file_path).map_err(|err| Error::Session(err.to_string()))? - }; +fn open_session_with_json_config(env: &mut JNIEnv, json_config: JString) -> Result { + let json_config = decode_string(env, &json_config)?; + let mut deserializer = + json5::Deserializer::from_str(&json_config).map_err(|err| session_error!(err))?; + let config = Config::from_deserializer(&mut deserializer).map_err(|err| match err { + Ok(c) => session_error!("Invalid configuration: {}", c), + Err(e) => session_error!("JSON error: {}", e), + })?; zenoh::open(config) - .res() - .map_err(|err| Error::Session(err.to_string())) + .wait() + .map_err(|err| session_error!(err)) } -/// Open a Zenoh session. +/// Open a Zenoh session with a YAML configuration. /// -/// Loads the session with the provided by [config_path]. If the config path provided is empty then -/// the default configuration is loaded. +/// It returns an [Arc] raw pointer to the Zenoh Session, which should be stored as a private read-only attribute +/// of the session object in the Java/Kotlin code. Subsequent calls to other session functions will require +/// this raw pointer to retrieve the [Session] using `Arc::from_raw`. /// -/// Returns: -/// - A [Result] with a [zenoh::Session] in case of success or an [Error::Session] in case of failure. +/// If opening the session fails, an exception is thrown on the JVM, and a null pointer is returned. /// -fn open_session_with_json_config(env: &mut JNIEnv, json_config: JString) -> Result { - let json_config = decode_string(env, &json_config)?; - let config = if json_config.is_empty() { - Config::default() - } else { - let mut deserializer = match json5::Deserializer::from_str(&json_config) { - Ok(deserializer) => Ok(deserializer), - Err(err) => Err(Error::Session(err.to_string())), - }?; - Config::from_deserializer(&mut deserializer).map_err(|err| match err { - Ok(c) => Error::Session(format!("Invalid configuration: {}", c)), - Err(e) => Error::Session(format!("JSON error: {}", e)), - })? - }; +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class (parameter required by the JNI interface but unused). +/// - `yaml_config`: Configuration as a YAML string. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNISession_openSessionWithYamlConfigViaJNI( + mut env: JNIEnv, + _class: JClass, + yaml_config: JString, +) -> *const Session { + let session = open_session_with_yaml_config(&mut env, yaml_config); + match session { + Ok(session) => Arc::into_raw(Arc::new(session)), + Err(err) => { + tracing::error!("Unable to open session: {}", err); + throw_exception!(env, session_error!(err)); + null() + } + } +} + +/// Open a Zenoh session with the provided yaml configuration. +/// +fn open_session_with_yaml_config(env: &mut JNIEnv, yaml_config: JString) -> Result { + let yaml_config = decode_string(env, &yaml_config)?; + let deserializer = serde_yaml::Deserializer::from_str(&yaml_config); + let config = Config::from_deserializer(deserializer).map_err(|err| match err { + Ok(c) => session_error!("Invalid configuration: {}", c), + Err(e) => session_error!("JSON error: {}", e), + })?; zenoh::open(config) - .res() - .map_err(|err| Error::Session(err.to_string())) + .wait() + .map_err(|err| session_error!(err)) } /// Closes a Zenoh session via JNI. /// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. -/// - `ptr`: The raw pointer to the Zenoh session. +/// - `session_ptr`: The raw pointer to the Zenoh session. /// -/// Safety: +/// # Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. /// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The ownership of the session is not transferred, and the session pointer remains valid -/// after this function call, so it is safe to continue using the session. -/// - It is the responsibility of the caller to ensure that the session is not used after it has -/// been freed or dropped. /// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// - After the session is closed, the provided pointer is no more valid. /// #[no_mangle] #[allow(non_snake_case, unused)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_closeSessionViaJNI( mut env: JNIEnv, _class: JClass, - ptr: *const zenoh::Session, + session_ptr: *const Session, ) { - let ptr = Arc::try_unwrap(Arc::from_raw(ptr)); + let ptr = Arc::try_unwrap(Arc::from_raw(session_ptr)); match ptr { Ok(session) => { // Do nothing, the pointer will be freed. } Err(arc_session) => { let ref_count = Arc::strong_count(&arc_session); - tracing::error!("Unable to close the session."); - _ = Error::Session(format!( + throw_exception!(env, session_error!( "Attempted to close the session, but at least one strong reference to it is still alive (ref count: {}). All the declared publishers, subscribers, and queryables need to be dropped first.", ref_count - )) - .throw_on_jvm(&mut env) - .map_err(|err| tracing::error!("Unable to throw exception on session failure: {}", err)); + )); } }; } /// Declare a Zenoh publisher via JNI. /// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher. +/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the publisher, may be null. /// - `key_expr_str`: String representation of the [KeyExpr] to be used for the publisher. /// It is only considered when the key_expr_ptr parameter is null, meaning the function is /// receiving a key expression that was not declared. -/// - `session_ptr`: The raw pointer to the Zenoh [Session] from which to declare the publisher. -/// - `congestion_control`: The [CongestionControl] mechanism specified as an ordinal. -/// - `priority`: The [Priority] mechanism specified as an ordinal. +/// - `session_ptr`: Raw pointer to the Zenoh [Session] to be used for the publisher. +/// - `congestion_control`: The [zenoh::publisher::CongestionControl] configuration as an ordinal. +/// - `priority`: The [zenoh::core::Priority] configuration as an ordinal. +/// - `is_express`: The express config of the publisher (see [zenoh::prelude::QoSBuilderTrait]). /// -/// Returns: +/// # Returns: /// - A raw pointer to the declared Zenoh publisher or null in case of failure. /// -/// Safety: +/// # Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. /// - It assumes that the provided session pointer is valid and has not been modified or freed. /// - The ownership of the session is not transferred, and the session pointer remains valid /// after this function call so it is safe to use it after this call. -/// - The returned raw pointer should be stored appropriately and later freed using `Java_io_zenoh_jni_JNIPublisher_freePtrViaJNI`. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. +/// - The function may throw an exception in case of failure, which should be handled by the caller. /// #[no_mangle] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( mut env: JNIEnv, _class: JClass, - key_expr_ptr: *const KeyExpr<'static>, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const zenoh::Session, + session_ptr: *const Session, congestion_control: jint, priority: jint, -) -> *const zenoh::publication::Publisher<'static> { - let result = declare_publisher( - &mut env, - key_expr_ptr, - key_expr_str, - session_ptr, - congestion_control, - priority, - ); - match result { - Ok(ptr) => ptr, - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on publisher declaration failure. {}", - err - ) - }); - null() + is_express: jboolean, +) -> *const Publisher<'static> { + let session = Arc::from_raw(session_ptr); + let publisher_ptr = || -> Result<*const Publisher<'static>> { + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let congestion_control = decode_congestion_control(congestion_control)?; + let priority = decode_priority(priority)?; + let result = session + .declare_publisher(key_expr) + .congestion_control(congestion_control) + .priority(priority) + .express(is_express != 0) + .wait(); + match result { + Ok(publisher) => Ok(Arc::into_raw(Arc::new(publisher))), + Err(err) => Err(session_error!(err)), } - } + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }); + std::mem::forget(session); + publisher_ptr } /// Performs a `put` operation in the Zenoh session via JNI. /// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// /// Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation. +/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation, may be null. /// - `key_expr_str`: String representation of the [KeyExpr] to be used for the operation. /// It is only considered when the key_expr_ptr parameter is null, meaning the function is /// receiving a key expression that was not declared. /// - `session_ptr`: Raw pointer to the [Session] to be used for the operation. /// - `payload`: The payload to send through the network. -/// - `encoding`: The [Encoding] of the put operation. +/// - `encoding_id`: The encoding id of the payload. +/// - `encoding_schema`: Optional encoding schema, may be null. /// - `congestion_control`: The [CongestionControl] mechanism specified. /// - `priority`: The [Priority] mechanism specified. -/// - `sample_kind`: The [SampleKind] of the put operation. +/// - `is_express`: The express flag. /// - `attachment`: Optional attachment encoded into a byte array. May be null. /// /// Safety: @@ -287,53 +292,116 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declarePublisherViaJNI( /// - It assumes that the provided session pointer is valid and has not been modified or freed. /// - The session pointer remains valid and the ownership of the session is not transferred, /// allowing safe usage of the session after this function call. -/// - The function may throw a JNI exception or a Session exception in case of failure, which -/// should be handled by the Java/Kotlin caller. +/// - The function may throw an exception in case of failure, which should be handled by the Java/Kotlin caller. /// #[no_mangle] #[allow(non_snake_case)] pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( mut env: JNIEnv, _class: JClass, - key_expr_ptr: *const KeyExpr<'static>, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const zenoh::Session, + session_ptr: *const Session, payload: JByteArray, - encoding: jint, + encoding_id: jint, + encoding_schema: JString, congestion_control: jint, priority: jint, - sample_kind: jint, + is_express: jboolean, attachment: JByteArray, ) { let session = Arc::from_raw(session_ptr); - match on_put( - &mut env, - key_expr_ptr, - key_expr_str, - &session, - payload, - encoding, - congestion_control, - priority, - sample_kind, - attachment, - ) { - Ok(_) => {} - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on query declaration failure. {}", - err - ) - }); + let _ = || -> Result<()> { + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let payload = decode_byte_array(&env, payload)?; + let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + let congestion_control = decode_congestion_control(congestion_control)?; + let priority = decode_priority(priority)?; + + let mut put_builder = session + .put(&key_expr, payload) + .congestion_control(congestion_control) + .encoding(encoding) + .express(is_express != 0) + .priority(priority); + + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + put_builder = put_builder.attachment(attachment) } - } + + put_builder + .wait() + .map(|_| tracing::trace!("Put on '{key_expr}'")) + .map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); std::mem::forget(session); } -/// Declare a Zenoh subscriber via JNI. +/// Performs a `delete` operation in the Zenoh session via JNI. /// -/// This function is meant to be called from Java/Kotlin code through JNI. +/// Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The JNI class. +/// - `key_expr_ptr`: Raw pointer to the [KeyExpr] to be used for the operation, may be null. +/// - `key_expr_str`: String representation of the [KeyExpr] to be used for the operation. +/// It is only considered when the key_expr_ptr parameter is null, meaning the function is +/// receiving a key expression that was not declared. +/// - `session_ptr`: Raw pointer to the [Session] to be used for the operation. +/// - `congestion_control`: The [CongestionControl] mechanism specified. +/// - `priority`: The [Priority] mechanism specified. +/// - `is_express`: The express flag. +/// - `attachment`: Optional attachment encoded into a byte array. May be null. +/// +/// Safety: +/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. +/// - It assumes that the provided session pointer is valid and has not been modified or freed. +/// - The session pointer remains valid and the ownership of the session is not transferred, +/// allowing safe usage of the session after this function call. +/// - The function may throw a JNI exception or a Session exception in case of failure, which +/// should be handled by the Java/Kotlin caller. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_deleteViaJNI( + mut env: JNIEnv, + _class: JClass, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, + key_expr_str: JString, + session_ptr: *const Session, + congestion_control: jint, + priority: jint, + is_express: jboolean, + attachment: JByteArray, +) { + let session = Arc::from_raw(session_ptr); + let _ = || -> Result<()> { + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let congestion_control = decode_congestion_control(congestion_control)?; + let priority = decode_priority(priority)?; + + let mut delete_builder = session + .delete(&key_expr) + .congestion_control(congestion_control) + .express(is_express != 0) + .priority(priority); + + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + delete_builder = delete_builder.attachment(attachment) + } + + delete_builder + .wait() + .map(|_| tracing::trace!("Delete on '{key_expr}'")) + .map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); + std::mem::forget(session); +} + +/// Declare a Zenoh subscriber via JNI. /// /// Parameters: /// - `env`: The JNI environment. @@ -345,10 +413,10 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( /// - `session_ptr`: The raw pointer to the Zenoh session. /// - `callback`: The callback function as an instance of the `JNISubscriberCallback` interface in Java/Kotlin. /// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called upon closing the subscriber. -/// - `reliability`: The [Reliability] value as an ordinal. +/// - `reliability`: The reliability value as an ordinal. /// /// Returns: -/// - A raw pointer to the declared Zenoh subscriber or null in case of failure. +/// - A raw pointer to the declared Zenoh subscriber. In case of failure, an exception is thrown and null is returned. /// /// Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. @@ -364,33 +432,105 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_putViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( mut env: JNIEnv, _class: JClass, - key_expr_ptr: *const KeyExpr<'static>, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const zenoh::Session, + session_ptr: *const Session, callback: JObject, on_close: JObject, reliability: jint, -) -> *const zenoh::subscriber::Subscriber<'static, ()> { - match declare_subscriber( - &mut env, - key_expr_ptr, - key_expr_str, - session_ptr, - callback, - on_close, - reliability, - ) { - Ok(subscriber_ptr) => subscriber_ptr, - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on subscriber declaration failure: {}", - err - ) - }); - null() - } - } +) -> *const Subscriber<'static, ()> { + let session = Arc::from_raw(session_ptr); + || -> Result<*const Subscriber<'static, ()>> { + let java_vm = Arc::new(get_java_vm(&mut env)?); + let callback_global_ref = get_callback_global_ref(&mut env, callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let reliability = decode_reliability(reliability)?; + let on_close = load_on_close(&java_vm, on_close_global_ref); + + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + tracing::debug!("Declaring subscriber on '{}'...", key_expr); + + let result = session + .declare_subscriber(key_expr.to_owned()) + .callback(move |sample| { + on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure + let _ = || -> Result<()> { + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + jni_error!("Unable to attach thread for subscriber: {}", err) + })?; + let byte_array = bytes_to_java_array(&env, sample.payload()) + .map(|array| env.auto_local(array))?; + + let encoding_id: jint = sample.encoding().id() as jint; + let encoding_schema = match sample.encoding().schema() { + Some(schema) => slice_to_java_string(&env, schema)?, + None => JString::default(), + }; + let kind = sample.kind() as jint; + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|array| env.auto_local(array)) + .map_err(|err| jni_error!("Error processing attachment: {}", err))?; + + let key_expr_str = + env.auto_local(env.new_string(sample.key_expr().to_string()).map_err( + |err| jni_error!("Error processing sample key expr: {}", err), + )?); + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + env.call_method( + &callback_global_ref, + "run", + "(Ljava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) + .map_err(|err| jni_error!(err))?; + Ok(()) + }() + .map_err(|err| tracing::error!("On subscriber callback error: {err}")); + }) + .reliability(reliability) + .wait(); + + let subscriber = + result.map_err(|err| session_error!("Unable to declare subscriber: {}", err))?; + + tracing::debug!( + "Subscriber declared on '{}' with reliability '{:?}'.", + key_expr, + reliability + ); + std::mem::forget(session); + Ok(Arc::into_raw(Arc::new(subscriber))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }) } /// Declare a Zenoh queryable via JNI. @@ -410,7 +550,7 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( /// - `complete`: The completeness of the queryable. /// /// Returns: -/// - A raw pointer to the declared Zenoh queryable or null in case of failure. +/// - A raw pointer to the declared Zenoh queryable. In case of failure, an exception is thrown and null is returned. /// /// Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. @@ -426,49 +566,149 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareSubscriberViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( mut env: JNIEnv, _class: JClass, - key_expr_ptr: *const KeyExpr<'static>, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - session_ptr: *const zenoh::Session, + session_ptr: *const Session, callback: JObject, on_close: JObject, complete: jboolean, -) -> *const zenoh::queryable::Queryable<'static, ()> { - match declare_queryable( - &mut env, - key_expr_ptr, - key_expr_str, - session_ptr, - callback, - on_close, - complete, - ) { - Ok(queryable) => Arc::into_raw(Arc::new(queryable)), - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on query declaration failure. {}", - err - ) - }); - null() - } - } +) -> *const Queryable<'static, ()> { + let session = Arc::from_raw(session_ptr); + let query_ptr = || -> Result<*const Queryable<'static, ()>> { + let java_vm = Arc::new(get_java_vm(&mut env)?); + let callback_global_ref = get_callback_global_ref(&mut env, callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let complete = complete != 0; + let on_close = load_on_close(&java_vm, on_close_global_ref); + tracing::debug!("Declaring queryable through JNI on {}", key_expr); + let builder = session + .declare_queryable(key_expr) + .callback(move |query| { + on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure + let env = match java_vm.attach_current_thread_as_daemon() { + Ok(env) => env, + Err(err) => { + tracing::error!("Unable to attach thread for queryable callback: {}", err); + return; + } + }; + + tracing::debug!("Receiving query through JNI: {}", query.to_string()); + match on_query(env, query, &callback_global_ref) { + Ok(_) => tracing::debug!("Queryable callback called successfully."), + Err(err) => tracing::error!("Error calling queryable callback: {}", err), + } + }) + .complete(complete); + + let queryable = builder + .wait() + .map_err(|err| session_error!("Error declaring queryable: {}", err))?; + Ok(Arc::into_raw(Arc::new(queryable))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }); + std::mem::forget(session); + query_ptr +} + +fn on_query(mut env: JNIEnv, query: Query, callback_global_ref: &GlobalRef) -> Result<()> { + let selector_params_jstr = env + .new_string(query.parameters().to_string()) + .map(|value| env.auto_local(value)) + .map_err(|err| { + jni_error!( + "Could not create a JString through JNI for the Query key expression. {}", + err + ) + })?; + + let (payload, encoding_id, encoding_schema) = if let Some(payload) = query.payload() { + let encoding = query.encoding().unwrap(); //If there is payload, there is encoding. + let encoding_id = encoding.id() as jint; + let encoding_schema = encoding + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(&env, schema), + ) + .map(|value| env.auto_local(value))?; + let byte_array = bytes_to_java_array(&env, payload).map(|value| env.auto_local(value))?; + (byte_array, encoding_id, encoding_schema) + } else { + ( + env.auto_local(JByteArray::default()), + 0, + env.auto_local(JString::default()), + ) + }; + + let attachment_bytes = query + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(&env, attachment), + ) + .map(|value| env.auto_local(value)) + .map_err(|err| jni_error!("Error processing attachment of reply: {}.", err))?; + + let key_expr_str = env + .new_string(&query.key_expr().to_string()) + .map(|key_expr| env.auto_local(key_expr)) + .map_err(|err| { + jni_error!( + "Could not create a JString through JNI for the Query key expression: {}.", + err + ) + })?; + + let query_ptr = Arc::into_raw(Arc::new(query)); + + let result = env + .call_method( + callback_global_ref, + "run", + "(Ljava/lang/String;Ljava/lang/String;[BILjava/lang/String;[BJ)V", + &[ + JValue::from(&key_expr_str), + JValue::from(&selector_params_jstr), + JValue::from(&payload), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + JValue::from(&attachment_bytes), + JValue::from(query_ptr as jlong), + ], + ) + .map(|_| ()) + .map_err(|err| { + // The callback could not be invoked, therefore the created kotlin query object won't be + // used. Since `query_ptr` as well as `key_expr_ptr` was created within this function + // and remains unaltered, it is safe to reclaim ownership of the memory by converting + // the raw pointers back into an `Arc` and freeing the memory. + unsafe { + Arc::from_raw(query_ptr); + }; + _ = env.exception_describe(); + jni_error!(err) + }); + result } /// Declare a [KeyExpr] through a [Session] via JNI. /// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. /// - `session_ptr`: A raw pointer to the Zenoh [Session] from which to declare the key expression. /// - `key_expr_str`: A Java String with the intended key expression. /// -/// Returns: -/// - A raw pointer to the declared key expression or null in case of failure. +/// # Returns: +/// - A raw pointer to the declared key expression. In case of failure, an exception is thrown and null is returned. /// -/// Safety: +/// # Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. /// - It assumes that the provided session pointer is valid and has not been modified or freed. /// - The session pointer remains valid and the ownership of the session is not transferred, @@ -480,41 +720,50 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareQueryableViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( mut env: JNIEnv, _class: JClass, - session_ptr: *const zenoh::Session, + session_ptr: *const Session, key_expr_str: JString, ) -> *const KeyExpr<'static> { - match declare_keyexpr(&mut env, session_ptr, key_expr_str) { - Ok(key_expr) => Arc::into_raw(Arc::new(key_expr)), - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on key expr declaration failure. {}", + let session: Arc = Arc::from_raw(session_ptr); + let key_expr_ptr = || -> Result<*const KeyExpr<'static>> { + let key_expr_str = decode_string(&mut env, &key_expr_str)?; + let key_expr = session + .declare_keyexpr(key_expr_str.to_owned()) + .wait() + .map_err(|err| { + session_error!( + "Unable to declare key expression '{}': {}", + key_expr_str, err ) - }); - null() - } - } + })?; + Ok(Arc::into_raw(Arc::new(key_expr))) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + null() + }); + mem::forget(session); + key_expr_ptr } /// Undeclare a [KeyExpr] through a [Session] via JNI. /// /// The key expression must have been previously declared on the specified session, otherwise an -/// error is thrown and propagated to the caller. +/// exception is thrown. /// -/// This function is meant to be called from Java/Kotlin code through JNI. +/// This functions frees the key expression pointer provided. /// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. /// - `session_ptr`: A raw pointer to the Zenoh [Session] from which to undeclare the key expression. /// - `key_expr_ptr`: A raw pointer to the [KeyExpr] to undeclare. /// -/// Safety: +/// # Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. /// - It assumes that the provided session and keyexpr pointers are valid and have not been modified or freed. -/// - Both session pointer and key expression pointers will remain valid. -/// Their ownership is not transferred, allowing safe usage of the session and the key expression after this function call. +/// - The session pointer remains valid after this function call. +/// - The key expression pointer is voided after this function call. /// - The function may throw an exception in case of failure, which should be handled by the caller. /// #[no_mangle] @@ -522,122 +771,45 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_declareKeyExprViaJNI( pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_undeclareKeyExprViaJNI( mut env: JNIEnv, _class: JClass, - session_ptr: *const zenoh::Session, + session_ptr: *const Session, key_expr_ptr: *const KeyExpr<'static>, ) { let session = Arc::from_raw(session_ptr); let key_expr = Arc::from_raw(key_expr_ptr); let key_expr_clone = key_expr.deref().clone(); - match session.undeclare(key_expr_clone).res() { - Ok(_) => {} - Err(err) => { - _ = Error::Session(format!( - "Unable to declare key expression {key_expr}: {}", - err - )) - .throw_on_jvm(&mut env) - } - } - std::mem::forget(session); - std::mem::forget(key_expr); -} - -/// Performs a `get` operation in the Zenoh session via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: -/// - `env`: The JNI environment. -/// - `_class`: The JNI class. -/// - `key_expr_ptr`: Raw pointer to a declared [KeyExpr] to be used for the query. May be null in case -/// of using a non declared key expression, in which case the key_expr_str parameter will be used instead. -/// - `key_expr_str`: String representation of the key expression to be used for the query. It is not -/// considered if a key_expr_ptr is provided. -/// - `selector_params`: Parameters of the selector. -/// - `session_ptr`: A raw pointer to the Zenoh session. -/// - `callback`: An instance of the Java/Kotlin `JNIGetCallback` function interface to be called upon receiving a reply. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when the get operation won't receive -/// any more replies. -/// - `timeout_ms`: The timeout in milliseconds. -/// - `target`: The [QueryTarget] as the ordinal of the enum. -/// - `consolidation`: The [ConsolidationMode] as the ordinal of the enum. -/// - `attachment`: An optional attachment encoded into a byte array. -/// -/// Safety: -/// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. -/// - It assumes that the provided session pointer is valid and has not been modified or freed. -/// - The session pointer remains valid and the ownership of the session is not transferred, -/// allowing safe usage of the session after this function call. -/// - The function may throw a JNI exception in case of failure, which should be handled by the caller. -/// -/// Throws: -/// - An exception in case of failure handling the query. -/// -#[no_mangle] -#[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( - mut env: JNIEnv, - _class: JClass, - key_expr_ptr: *const KeyExpr<'static>, - key_expr_str: JString, - selector_params: JString, - session_ptr: *const zenoh::Session, - callback: JObject, - on_close: JObject, - timeout_ms: jlong, - target: jint, - consolidation: jint, - attachment: JByteArray, -) { - let session = Arc::from_raw(session_ptr); - match on_get_query( - &mut env, - key_expr_ptr, - key_expr_str, - selector_params, - &session, - callback, - on_close, - timeout_ms, - target, - consolidation, - None, - attachment, - ) { + match session.undeclare(key_expr_clone).wait() { Ok(_) => {} Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on get operation failure. {}", - err - ) - }); + throw_exception!( + env, + session_error!("Unable to declare key expression '{}': {}", key_expr, err) + ); } } std::mem::forget(session); + // `key_expr` is intentionally left to be freed by Rust } /// Performs a `get` operation in the Zenoh session via JNI with Value. /// -/// This function is meant to be called from Java/Kotlin code through JNI. -/// -/// Parameters: +/// # Parameters: /// - `env`: The JNI environment. /// - `_class`: The JNI class. /// - `key_expr_ptr`: Raw pointer to a declared [KeyExpr] to be used for the query. May be null in case -/// of using a non declared key expression, in which case the key_expr_str parameter will be used instead. +/// of using a non declared key expression, in which case the `key_expr_str` parameter will be used instead. /// - `key_expr_str`: String representation of the key expression to be used to declare the query. It is not -/// considered if a key_expr_ptr is provided. -/// - `selector_params`: Parameters of the selector. +/// considered if a `key_expr_ptr` is provided. +/// - `selector_params`: Optional parameters of the selector. /// - `session_ptr`: A raw pointer to the Zenoh [Session]. /// - `callback`: A Java/Kotlin callback to be called upon receiving a reply. /// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when no more replies will be received. /// - `timeout_ms`: The timeout in milliseconds. -/// - `target`: The [QueryTarget] as the ordinal of the enum. -/// - `consolidation`: The [ConsolidationMode] as the ordinal of the enum. -/// - `payload`: The payload of the [Value] -/// - `encoding`: The [Encoding] as the ordinal of the enum. +/// - `target`: The query target as the ordinal of the enum. +/// - `consolidation`: The consolidation mode as the ordinal of the enum. /// - `attachment`: An optional attachment encoded into a byte array. +/// - `payload`: Optional payload for the query. +/// - `encoding_id`: The encoding of the payload. +/// - `encoding_schema`: The encoding schema of the payload, may be null. /// /// Safety: /// - The function is marked as unsafe due to raw pointer manipulation and JNI interaction. @@ -651,157 +823,229 @@ pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( /// #[no_mangle] #[allow(non_snake_case)] -pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getWithValueViaJNI( +pub unsafe extern "C" fn Java_io_zenoh_jni_JNISession_getViaJNI( mut env: JNIEnv, _class: JClass, - key_expr_ptr: *const KeyExpr<'static>, + key_expr_ptr: /*nullable*/ *const KeyExpr<'static>, key_expr_str: JString, - selector_params: JString, - session_ptr: *const zenoh::Session, + selector_params: /*nullable*/ JString, + session_ptr: *const Session, callback: JObject, on_close: JObject, timeout_ms: jlong, target: jint, consolidation: jint, - payload: JByteArray, - encoding: jint, - attachment: JByteArray, + attachment: /*nullable*/ JByteArray, + payload: /*nullable*/ JByteArray, + encoding_id: jint, + encoding_schema: /*nullable*/ JString, ) { let session = Arc::from_raw(session_ptr); - match on_get_query( - &mut env, - key_expr_ptr, - key_expr_str, - selector_params, - &session, - callback, - on_close, - timeout_ms, - target, - consolidation, - Some((payload, encoding)), - attachment, - ) { - Ok(_) => {} - Err(err) => { - _ = err.throw_on_jvm(&mut env).map_err(|err| { - tracing::error!( - "Unable to throw exception on get operation failure. {}", - err - ) - }); + let _ = || -> Result<()> { + let key_expr = process_kotlin_key_expr(&mut env, &key_expr_str, key_expr_ptr)?; + let java_vm = Arc::new(get_java_vm(&mut env)?); + let callback_global_ref = get_callback_global_ref(&mut env, callback)?; + let on_close_global_ref = get_callback_global_ref(&mut env, on_close)?; + let query_target = decode_query_target(target)?; + let consolidation = decode_consolidation(consolidation)?; + let timeout = Duration::from_millis(timeout_ms as u64); + let on_close = load_on_close(&java_vm, on_close_global_ref); + let selector_params = if selector_params.is_null() { + String::new() + } else { + decode_string(&mut env, &selector_params)? + }; + let selector = Selector::owned(&key_expr, selector_params); + let mut get_builder = session + .get(selector) + .callback(move |reply| { + || -> Result<()> { + on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure + tracing::debug!("Receiving reply through JNI: {:?}", reply); + let mut env = java_vm.attach_current_thread_as_daemon().map_err(|err| { + jni_error!("Unable to attach thread for GET query callback: {}.", err) + })?; + + match reply.result() { + Ok(sample) => on_reply_success( + &mut env, + reply.replier_id(), + sample, + &callback_global_ref, + ), + Err(error) => on_reply_error( + &mut env, + reply.replier_id(), + error, + &callback_global_ref, + ), + } + }() + .unwrap_or_else(|err| tracing::error!("Error on get callback: {err}")); + }) + .target(query_target) + .timeout(timeout) + .consolidation(consolidation); + + if !payload.is_null() { + let encoding = decode_encoding(&mut env, encoding_id, &encoding_schema)?; + get_builder = get_builder.encoding(encoding); + get_builder = get_builder.payload(decode_byte_array(&env, payload)?); } - } + + if !attachment.is_null() { + let attachment = decode_byte_array(&env, attachment)?; + get_builder = get_builder.attachment::>(attachment); + } + + get_builder + .wait() + .map(|_| tracing::trace!("Performing get on '{key_expr}'.",)) + .map_err(|err| session_error!(err)) + }() + .map_err(|err| throw_exception!(env, err)); std::mem::forget(session); } -/// Performs a `get` operation in the Zenoh session via JNI. -/// -/// Parameters: -/// - `env`: A mutable reference to the JNI environment. -/// - `key_expr_ptr`: Raw pointer to a declared [KeyExpr] to be used for the query. May be null in case -/// of using a non declared key expression, in which case the key_expr_str parameter will be used instead. -/// - `key_expr_str`: String representation of the key expression to be used to declare the query. It is not -/// considered if a key_expr_ptr is provided. -/// - `session`: An `Arc` representing the Zenoh session. -/// - `callback`: A Java/Kotlin `JNIGetCallback` function interface to be called upon receiving a reply. -/// - `on_close`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when Zenoh notifies -/// that no more replies will be received. -/// - `timeout_ms`: The timeout in milliseconds. -/// - `target`: The [QueryTarget] as the ordinal of the enum. -/// - `consolidation`: The [ConsolidationMode] as the ordinal of the enum. -/// - `value_params`: Parameters of the value (payload as [JByteArray] and encoding as [jint]) to -/// be set in case the get is performed "with value". -/// - `encoded_attachment`: An optional attachment encoded into a byte array. -/// -/// Returns: -/// - A `Result` indicating the result of the `get` operation. -/// -#[allow(clippy::too_many_arguments)] -fn on_get_query( +fn on_reply_success( env: &mut JNIEnv, - key_expr_ptr: *const KeyExpr<'static>, - key_expr_str: JString, - selector_params: JString, - session: &Arc, - callback: JObject, - on_close: JObject, - timeout_ms: jlong, - target: jint, - consolidation: jint, - value_params: Option<(JByteArray, jint)>, - encoded_attachment: JByteArray, + replier_id: Option, + sample: &Sample, + callback_global_ref: &GlobalRef, ) -> Result<()> { - let key_expr = unsafe { process_kotlin_key_expr(env, &key_expr_str, key_expr_ptr) }?; - let java_vm = Arc::new(get_java_vm(env)?); - let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let query_target = decode_query_target(target)?; - let consolidation = decode_consolidation(consolidation)?; - let selector_params = decode_string(env, &selector_params)?; - let timeout = Duration::from_millis(timeout_ms as u64); - let on_close = load_on_close(&java_vm, on_close_global_ref); - let selector = Selector::from(&key_expr).with_parameters(&selector_params); - let mut get_builder = session - .get(selector) - .callback(move |reply| { - on_close.noop(); // Does nothing, but moves `on_close` inside the closure so it gets destroyed with the closure - tracing::debug!("Receiving reply through JNI: {:?}", reply); - let env = match java_vm.attach_current_thread_as_daemon() { - Ok(env) => env, - Err(err) => { - tracing::error!("Unable to attach thread for GET query callback: {}", err); - return; - } - }; - match on_reply(env, reply, &callback_global_ref) { - Ok(_) => {} - Err(err) => tracing::error!("{}", err), - } - }) - .target(query_target) - .timeout(timeout) - .consolidation(consolidation); - - let mut binding = None; - if let Some((payload, encoding)) = value_params { - let value = decode_value(env, payload, encoding)?; - get_builder = get_builder.with_value(value.to_owned()); - binding = Some(value) - } + let zenoh_id = replier_id + .map_or_else( + || Ok(JString::default()), + |replier_id| { + env.new_string(replier_id.to_string()) + .map_err(|err| jni_error!(err)) + }, + ) + .map(|value| env.auto_local(value))?; - if !encoded_attachment.is_null() { - let aux = decode_byte_array(env, encoded_attachment)?; - let attachment = vec_to_attachment(aux); - get_builder = get_builder.with_attachment(attachment); - } + let byte_array = + bytes_to_java_array(env, sample.payload()).map(|value| env.auto_local(value))?; + let encoding: jint = sample.encoding().id() as jint; + let encoding_schema = sample + .encoding() + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(env, schema), + ) + .map(|value| env.auto_local(value))?; + let kind = sample.kind() as jint; - get_builder - .res() - .map(|_| { - tracing::trace!( - "Performing get on {key_expr:?}, with target '{query_target:?}', with timeout '{timeout:?}', consolidation '{consolidation:?}', with value: '{binding:?}'", + let (timestamp, is_valid) = sample + .timestamp() + .map(|timestamp| (timestamp.get_time().as_u64(), true)) + .unwrap_or((0, false)); + + let attachment_bytes = sample + .attachment() + .map_or_else( + || Ok(JByteArray::default()), + |attachment| bytes_to_java_array(env, attachment), + ) + .map(|value| env.auto_local(value)) + .map_err(|err| jni_error!("Error processing attachment of reply: {}.", err))?; + + let key_expr_str = env + .new_string(sample.key_expr().to_string()) + .map(|value| env.auto_local(value)) + .map_err(|err| { + jni_error!( + "Could not create a JString through JNI for the Sample key expression. {}", + err ) - }) - .map_err(|err| Error::Session(err.to_string()))?; - Ok(()) + })?; + + let express = sample.express(); + let priority = sample.priority() as jint; + let cc = sample.congestion_control() as jint; + + let result = match env.call_method( + callback_global_ref, + "run", + "(Ljava/lang/String;ZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&zenoh_id), + JValue::from(true), + JValue::from(&key_expr_str), + JValue::from(&byte_array), + JValue::from(encoding), + JValue::from(&encoding_schema), + JValue::from(kind), + JValue::from(timestamp as i64), + JValue::from(is_valid), + JValue::from(&attachment_bytes), + JValue::from(express), + JValue::from(priority), + JValue::from(cc), + ], + ) { + Ok(_) => Ok(()), + Err(err) => { + _ = env.exception_describe(); + Err(jni_error!("On GET callback error: {}", err)) + } + }; + result } -pub(crate) unsafe fn declare_keyexpr( +fn on_reply_error( env: &mut JNIEnv, - session_ptr: *const Session, - key_expr: JString, -) -> Result> { - let key_expr = decode_string(env, &key_expr)?; - let session: Arc = Arc::from_raw(session_ptr); - let result = session.declare_keyexpr(key_expr.to_owned()).res(); - std::mem::forget(session); + replier_id: Option, + reply_error: &ReplyError, + callback_global_ref: &GlobalRef, +) -> Result<()> { + let zenoh_id = replier_id + .map_or_else( + || Ok(JString::default()), + |replier_id| { + env.new_string(replier_id.to_string()) + .map_err(|err| jni_error!(err)) + }, + ) + .map(|value| env.auto_local(value))?; - match result { - Ok(key_expr) => Ok(key_expr), - Err(err) => Err(Error::Session(format!( - "Unable to declare key expression {key_expr}: {}", - err - ))), - } + let payload = + bytes_to_java_array(env, reply_error.payload()).map(|value| env.auto_local(value))?; + let encoding_id: jint = reply_error.encoding().id() as jint; + let encoding_schema = reply_error + .encoding() + .schema() + .map_or_else( + || Ok(JString::default()), + |schema| slice_to_java_string(env, schema), + ) + .map(|value| env.auto_local(value))?; + let result = match env.call_method( + callback_global_ref, + "run", + "(Ljava/lang/String;ZLjava/lang/String;[BILjava/lang/String;IJZ[BZII)V", + &[ + JValue::from(&zenoh_id), + JValue::from(false), + JValue::from(&JString::default()), + JValue::from(&payload), + JValue::from(encoding_id), + JValue::from(&encoding_schema), + // The remaining parameters aren't used in case of replying error, so we set them to default. + JValue::from(0 as jint), + JValue::from(0_i64), + JValue::from(false), + JValue::from(&JByteArray::default()), + JValue::from(false), + JValue::from(0 as jint), + JValue::from(0 as jint), + ], + ) { + Ok(_) => Ok(()), + Err(err) => { + _ = env.exception_describe(); + Err(jni_error!("On GET callback error: {}", err)) + } + }; + result } diff --git a/zenoh-jni/src/subscriber.rs b/zenoh-jni/src/subscriber.rs index 4f1e17798..3462ec7e0 100644 --- a/zenoh-jni/src/subscriber.rs +++ b/zenoh-jni/src/subscriber.rs @@ -14,34 +14,17 @@ use std::sync::Arc; -use jni::{ - objects::{JClass, JObject, JString, JValue}, - sys::jint, - JNIEnv, -}; -use zenoh::prelude::r#sync::*; -use zenoh::subscriber::Subscriber; +use jni::{objects::JClass, JNIEnv}; +use zenoh::pubsub::Subscriber; -use crate::{ - errors::{Error, Result}, - utils::attachment_to_vec, -}; -use crate::{ - key_expr::process_kotlin_key_expr, - sample::qos_into_jbyte, - utils::{get_callback_global_ref, get_java_vm, load_on_close}, -}; - -/// Frees the memory associated with a Zenoh subscriber raw pointer via JNI. -/// -/// This function is meant to be called from Java/Kotlin code through JNI. +/// Frees the [Subscriber]. /// -/// Parameters: +/// # Parameters: /// - `_env`: The JNI environment. /// - `_class`: The JNI class. -/// - `ptr`: The raw pointer to the Zenoh subscriber ([Subscriber]). +/// - `subscriber_ptr`: The raw pointer to the Zenoh subscriber ([Subscriber]). /// -/// Safety: +/// # Safety: /// - The function is marked as unsafe due to raw pointer manipulation. /// - It assumes that the provided subscriber pointer is valid and has not been modified or freed. /// - The function takes ownership of the raw pointer and releases the associated memory. @@ -52,153 +35,7 @@ use crate::{ pub(crate) unsafe extern "C" fn Java_io_zenoh_jni_JNISubscriber_freePtrViaJNI( _env: JNIEnv, _: JClass, - ptr: *const zenoh::subscriber::Subscriber<()>, + subscriber_ptr: *const Subscriber<()>, ) { - Arc::from_raw(ptr); -} - -/// Declares a Zenoh subscriber via JNI. -/// -/// Parameters: -/// - `env`: A mutable reference to the JNI environment. -/// - `key_expr_ptr`: Raw pointer to the key expression to be used for the subscriber. May be null, in -/// which case the key_expr_str parameter will be used. -/// - `key_expr_str`: String representation of the key expression to be used to declare the subscriber. -/// Not considered if the key_expr_ptr parameter is provided. -/// - `session_ptr`: Raw pointer to the session to be used for the declaration.. -/// - `callback`: The callback function as an instance of the `Callback` interface in Java/Kotlin. -/// - `onClose`: A Java/Kotlin `JNIOnCloseCallback` function interface to be called when the subscriber is undeclared. -/// - `reliability`: The [Reliability] configuration as an ordinal. -/// -/// Returns: -/// - A [Result] containing a raw pointer to the declared Zenoh subscriber ([Subscriber]) in case of success, -/// or an [Error] variant in case of failure. -/// -/// Safety: -/// - The returned raw pointer should be stored appropriately and later freed using [Java_io_zenoh_jni_JNISubscriber_freePtrViaJNI]. -/// -pub(crate) unsafe fn declare_subscriber( - env: &mut JNIEnv, - key_expr_ptr: *const KeyExpr<'static>, - key_expr_str: JString, - session_ptr: *const zenoh::Session, - callback: JObject, - on_close: JObject, - reliability: jint, -) -> Result<*const Subscriber<'static, ()>> { - let java_vm = Arc::new(get_java_vm(env)?); - let callback_global_ref = get_callback_global_ref(env, callback)?; - let on_close_global_ref = get_callback_global_ref(env, on_close)?; - let reliability = decode_reliability(reliability)?; - let on_close = load_on_close(&java_vm, on_close_global_ref); - - let key_expr = process_kotlin_key_expr(env, &key_expr_str, key_expr_ptr)?; - tracing::debug!("Declaring subscriber on '{}'...", key_expr); - let session = Arc::from_raw(session_ptr); - - let result = session - .declare_subscriber(key_expr.to_owned()) - .callback(move |sample| { - on_close.noop(); // Moves `on_close` inside the closure so it gets destroyed with the closure - - let mut env = match java_vm.attach_current_thread_as_daemon() { - Ok(env) => env, - Err(err) => { - tracing::error!("Unable to attach thread for subscriber: {}", err); - return; - } - }; - - let byte_array = - match env.byte_array_from_slice(sample.value.payload.contiguous().as_ref()) { - Ok(byte_array) => byte_array, - Err(err) => { - tracing::error!("On subscriber callback error: {}", err); - return; - } - }; - - let encoding: jint = sample.value.encoding.prefix().to_owned() as jint; - let kind = sample.kind as jint; - let (timestamp, is_valid) = sample - .timestamp - .map(|timestamp| (timestamp.get_time().as_u64(), true)) - .unwrap_or((0, false)); - - let attachment_bytes = match sample.attachment.map_or_else( - || env.byte_array_from_slice(&[]), - |attachment| env.byte_array_from_slice(attachment_to_vec(attachment).as_slice()), - ) { - Ok(byte_array) => byte_array, - Err(err) => { - tracing::error!( - "On subscriber callback error. Error processing attachment: {}.", - err - ); - return; - } - }; - - let key_expr_str = match env.new_string(sample.key_expr.to_string()) { - Ok(key_expr_str) => key_expr_str, - Err(err) => { - tracing::error!( - "Could not create a JString through JNI for the Sample key expression. {}", - err - ); - return; - } - }; - - if let Err(err) = env.call_method( - &callback_global_ref, - "run", - "(Ljava/lang/String;[BIIJZB[B)V", - &[ - JValue::from(&key_expr_str), - JValue::from(&byte_array), - JValue::from(encoding), - JValue::from(kind), - JValue::from(timestamp as i64), - JValue::from(is_valid), - JValue::from(qos_into_jbyte(sample.qos)), - JValue::from(&attachment_bytes), - ], - ) { - tracing::error!("On subscriber callback error: {}", err); - } - _ = env - .delete_local_ref(key_expr_str) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(byte_array) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - _ = env - .delete_local_ref(attachment_bytes) - .map_err(|err| tracing::debug!("Error deleting local ref: {}", err)); - }) - .reliability(reliability) - .res(); - - std::mem::forget(session); - - let subscriber = - result.map_err(|err| Error::Session(format!("Unable to declare subscriber: {}", err)))?; - - tracing::debug!( - "Subscriber declared on '{}' with reliability '{:?}'.", - key_expr, - reliability - ); - Ok(Arc::into_raw(Arc::new(subscriber))) -} - -fn decode_reliability(reliability: jint) -> Result { - match reliability { - 0 => Ok(Reliability::BestEffort), - 1 => Ok(Reliability::Reliable), - value => Err(Error::Session(format!( - "Unable to decode reliability '{value}'" - ))), - } + Arc::from_raw(subscriber_ptr); } diff --git a/zenoh-jni/src/utils.rs b/zenoh-jni/src/utils.rs index de7feb41c..96c705f94 100644 --- a/zenoh-jni/src/utils.rs +++ b/zenoh-jni/src/utils.rs @@ -14,40 +14,60 @@ use std::sync::Arc; +use crate::{ + errors::{Error, Result}, + jni_error, session_error, throw_exception, +}; use jni::{ objects::{JByteArray, JObject, JString}, + sys::jint, JNIEnv, JavaVM, }; -use zenoh::sample::{Attachment, AttachmentBuilder}; - -use crate::errors::{Error, Result}; +use zenoh::{ + bytes::{Encoding, ZBytes}, + internal::buffers::ZSlice, + pubsub::Reliability, + qos::{CongestionControl, Priority}, + query::{ConsolidationMode, QueryTarget}, +}; /// Converts a JString into a rust String. pub(crate) fn decode_string(env: &mut JNIEnv, string: &JString) -> Result { let binding = env .get_string(string) - .map_err(|err| Error::Jni(format!("Error while retrieving JString: {}", err)))?; + .map_err(|err| jni_error!("Error while retrieving JString: {}", err))?; let value = binding .to_str() - .map_err(|err| Error::Jni(format!("Error decoding JString: {}", err)))?; + .map_err(|err| jni_error!("Error decoding JString: {}", err))?; Ok(value.to_string()) } +pub(crate) fn decode_encoding( + env: &mut JNIEnv, + encoding: jint, + schema: &JString, +) -> Result { + let schema: Option = if schema.is_null() { + None + } else { + Some(decode_string(env, schema)?.into_bytes().into()) + }; + let encoding_id = + u16::try_from(encoding).map_err(|err| jni_error!("Failed to decode encoding: {}", err))?; + Ok(Encoding::new(encoding_id, schema)) +} + pub(crate) fn get_java_vm(env: &mut JNIEnv) -> Result { env.get_java_vm() - .map_err(|err| Error::Jni(format!("Unable to retrieve JVM reference: {:?}", err))) + .map_err(|err| jni_error!("Unable to retrieve JVM reference: {}", err)) } pub(crate) fn get_callback_global_ref( env: &mut JNIEnv, callback: JObject, ) -> crate::errors::Result { - env.new_global_ref(callback).map_err(|err| { - Error::Jni(format!( - "Unable to get reference to the provided callback: {}", - err - )) - }) + env.new_global_ref(callback) + .map_err(|err| jni_error!("Unable to get reference to the provided callback: {}", err)) } /// Helper function to convert a JByteArray into a Vec. @@ -55,14 +75,72 @@ pub(crate) fn decode_byte_array(env: &JNIEnv<'_>, payload: JByteArray) -> Result let payload_len = env .get_array_length(&payload) .map(|length| length as usize) - .map_err(|err| Error::Jni(err.to_string()))?; + .map_err(|err| jni_error!(err))?; let mut buff = vec![0; payload_len]; env.get_byte_array_region(payload, 0, &mut buff[..]) - .map_err(|err| Error::Jni(err.to_string()))?; + .map_err(|err| jni_error!(err))?; let buff: Vec = unsafe { std::mem::transmute::, Vec>(buff) }; Ok(buff) } +pub(crate) fn decode_priority(priority: jint) -> Result { + Priority::try_from(priority as u8) + .map_err(|err| session_error!("Error retrieving priority: {}.", err)) +} + +pub(crate) fn decode_congestion_control(congestion_control: jint) -> Result { + match congestion_control { + 1 => Ok(CongestionControl::Block), + 0 => Ok(CongestionControl::Drop), + value => Err(session_error!("Unknown congestion control '{}'.", value)), + } +} + +pub(crate) fn decode_query_target(target: jint) -> Result { + match target { + 0 => Ok(QueryTarget::BestMatching), + 1 => Ok(QueryTarget::All), + 2 => Ok(QueryTarget::AllComplete), + value => Err(session_error!("Unable to decode QueryTarget '{}'.", value)), + } +} + +pub(crate) fn decode_consolidation(consolidation: jint) -> Result { + match consolidation { + 0 => Ok(ConsolidationMode::Auto), + 1 => Ok(ConsolidationMode::None), + 2 => Ok(ConsolidationMode::Monotonic), + 3 => Ok(ConsolidationMode::Latest), + value => Err(session_error!("Unable to decode consolidation '{}'", value)), + } +} + +pub(crate) fn decode_reliability(reliability: jint) -> Result { + match reliability { + 0 => Ok(Reliability::BestEffort), + 1 => Ok(Reliability::Reliable), + value => Err(session_error!("Unable to decode reliability '{}'", value)), + } +} + +pub(crate) fn bytes_to_java_array<'a>(env: &JNIEnv<'a>, slice: &ZBytes) -> Result> { + env.byte_array_from_slice( + slice + .deserialize::>() + .map_err(|err| session_error!("Unable to deserialize slice: {}", err))? + .as_ref(), + ) + .map_err(|err| jni_error!(err)) +} + +pub(crate) fn slice_to_java_string<'a>(env: &JNIEnv<'a>, slice: &ZSlice) -> Result> { + env.new_string( + String::from_utf8(slice.to_vec()) + .map_err(|err| session_error!("Unable to decode string: {}", err))?, + ) + .map_err(|err| jni_error!(err)) +} + /// A type that calls a function when dropped pub(crate) struct CallOnDrop(core::mem::MaybeUninit); impl CallOnDrop { @@ -102,75 +180,12 @@ pub(crate) fn load_on_close( Ok(_) => (), Err(err) => { _ = env.exception_describe(); - _ = Error::Jni(format!("Error while running 'onClose' callback: {}", err)) - .throw_on_jvm(&mut env) - .map_err(|err| { - tracing::error!( - "Unable to throw exception upon 'onClose' failure: {}", - err - ) - }); + throw_exception!( + env, + jni_error!("Error while running 'onClose' callback: {}", err) + ); } } } }) } - -/// This function is used in conjunction with the Kotlin function -/// `decodeAttachment(attachmentBytes: ByteArray): Attachment` which takes a byte array with the -/// format , repeating this -/// pattern for as many pairs there are in the attachment. -/// -/// The kotlin function expects both key size and value size to be i32 integers expressed with -/// little endian format. -/// -pub(crate) fn attachment_to_vec(attachment: Attachment) -> Vec { - let mut buffer: Vec = Vec::new(); - for (key, value) in attachment.iter() { - buffer.extend((key.len() as i32).to_le_bytes()); - buffer.extend(&key[..]); - buffer.extend((value.len() as i32).to_le_bytes()); - buffer.extend(&value[..]); - } - buffer -} - -/// This function is used in conjunction with the Kotlin function -/// `encodeAttachment(attachment: Attachment): ByteArray` which converts the attachment into a -/// ByteArray with the format , repeating this -/// pattern for as many pairs there are in the attachment. -/// -/// Both key size and value size are i32 integers with little endian format. -/// -pub(crate) fn vec_to_attachment(bytes: Vec) -> Attachment { - let mut builder = AttachmentBuilder::new(); - let mut idx = 0; - let i32_size = std::mem::size_of::(); - let mut slice_size; - - while idx < bytes.len() { - slice_size = i32::from_le_bytes( - bytes[idx..idx + i32_size] - .try_into() - .expect("Error decoding i32 while processing attachment."), //This error should never happen. - ); - idx += i32_size; - - let key = &bytes[idx..idx + slice_size as usize]; - idx += slice_size as usize; - - slice_size = i32::from_le_bytes( - bytes[idx..idx + i32_size] - .try_into() - .expect("Error decoding i32 while processing attachment."), //This error should never happen. - ); - idx += i32_size; - - let value = &bytes[idx..idx + slice_size as usize]; - idx += slice_size as usize; - - builder.insert(key, value); - } - - builder.build() -} diff --git a/zenoh-jni/src/value.rs b/zenoh-jni/src/value.rs deleted file mode 100644 index d3f27b9e8..000000000 --- a/zenoh-jni/src/value.rs +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -use crate::{errors::Result, utils::decode_byte_array}; -use jni::{objects::JByteArray, JNIEnv}; -use zenoh::{ - buffers::{writer::Writer, ZBuf}, - prelude::{Encoding, HasWriter, KnownEncoding}, - value::Value, -}; - -pub(crate) fn build_value(payload: &[u8], encoding: KnownEncoding) -> Value { - let mut zbuf = ZBuf::default(); - let mut writer = zbuf.writer(); - _ = writer.write(payload); - Value::new(zbuf).encoding(Encoding::Exact(encoding)) -} - -pub(crate) fn decode_value(env: &JNIEnv<'_>, payload: JByteArray, encoding: i32) -> Result { - let buff = decode_byte_array(env, payload)?; - let encoding = match KnownEncoding::try_from(encoding as u8) { - Ok(encoding) => encoding, - Err(_) => { - tracing::debug!("Unable to retrieve encoding. Setting Empty encoding."); - KnownEncoding::Empty - } - }; - Ok(build_value(&buff[..], encoding)) -} diff --git a/zenoh-jni/src/zbytes.rs b/zenoh-jni/src/zbytes.rs new file mode 100644 index 000000000..da067f499 --- /dev/null +++ b/zenoh-jni/src/zbytes.rs @@ -0,0 +1,197 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +use std::collections::HashMap; + +use jni::{ + objects::{JByteArray, JClass, JList, JMap, JObject}, + sys::{jbyteArray, jobject}, + JNIEnv, +}; +use zenoh::bytes::ZBytes; + +use crate::{errors::Result, jni_error, session_error, utils::bytes_to_java_array}; +use crate::{throw_exception, utils::decode_byte_array}; + +/// +/// Map serialization and deserialization +/// + +/// Serializes a Map, returning the resulting ByteArray. +/// +/// # Parameters +/// - `env``: the JNI environment. +/// - `_class`: The java class. +/// - `map`: A Java bytearray map. +/// +/// # Returns: +/// - Returns the serialized map as a byte array. +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeIntoMapViaJNI( + mut env: JNIEnv, + _class: JClass, + map: JObject, +) -> jbyteArray { + || -> Result { + let zbytes = java_map_to_zbytes(&mut env, &map).map_err(|err| jni_error!(err))?; + let byte_array = bytes_to_java_array(&env, &zbytes)?; + Ok(byte_array.as_raw()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::null().as_raw() + }) +} + +fn java_map_to_zbytes(env: &mut JNIEnv, map: &JObject) -> jni::errors::Result { + let jmap = JMap::from_env(env, map)?; + let mut iterator = jmap.iter(env)?; + let mut rust_map: HashMap, Vec> = HashMap::new(); + while let Some((key, value)) = iterator.next(env)? { + let key_bytes = env.convert_byte_array(env.auto_local(JByteArray::from(key)))?; + let value_bytes = env.convert_byte_array(env.auto_local(JByteArray::from(value)))?; + rust_map.insert(key_bytes, value_bytes); + } + Ok(ZBytes::serialize(rust_map)) +} + +/// Deserializes a serialized bytearray map, returning the original map. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The Java class. +/// - `serialized_map`: The byte array resulting of the serialization of a bytearray map. +/// +/// # Returns +/// - The original byte array map before serialization. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeIntoMapViaJNI( + mut env: JNIEnv, + _class: JClass, + serialized_map: JByteArray, +) -> jobject { + || -> Result { + let payload = decode_byte_array(&env, serialized_map)?; + let zbytes = ZBytes::new(payload); + let deserialization: HashMap, Vec> = zbytes + .deserialize::, Vec>>() + .map_err(|err| session_error!(err))?; + hashmap_to_java_map(&mut env, &deserialization).map_err(|err| jni_error!(err)) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::null().as_raw() + }) +} + +fn hashmap_to_java_map( + env: &mut JNIEnv, + hashmap: &HashMap, Vec>, +) -> jni::errors::Result { + let map = env.new_object("java/util/HashMap", "()V", &[])?; + let jmap = JMap::from_env(env, &map)?; + + for (k, v) in hashmap.iter() { + let key = env.byte_array_from_slice(k.as_slice())?; + let value = env.byte_array_from_slice(v.as_slice())?; + jmap.put(env, &key, &value)?; + } + Ok(map.as_raw()) +} + +/// +/// List serialization and deserialization +/// + +/// Serializes a list of byte arrays, returning a byte array. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The Java class. +/// - `list`: The Java list of byte arrays to serialize. +/// +/// # Returns: +/// - The serialized list as a ByteArray. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_serializeIntoListViaJNI( + mut env: JNIEnv, + _class: JClass, + list: JObject, +) -> jbyteArray { + || -> Result { + let zbytes = java_list_to_zbytes(&mut env, &list).map_err(|err| jni_error!(err))?; + let byte_array = bytes_to_java_array(&env, &zbytes)?; + Ok(byte_array.as_raw()) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::null().as_raw() + }) +} + +fn java_list_to_zbytes(env: &mut JNIEnv, list: &JObject) -> jni::errors::Result { + let jmap = JList::from_env(env, list)?; + let mut iterator = jmap.iter(env)?; + let mut rust_vec: Vec> = Vec::new(); + while let Some(value) = iterator.next(env)? { + let value_bytes = env.auto_local(JByteArray::from(value)); + let value_bytes = env.convert_byte_array(value_bytes)?; + rust_vec.push(value_bytes); + } + let zbytes = ZBytes::from_iter(rust_vec); + Ok(zbytes) +} + +/// Deserializes a serialized list of byte arrrays, returning the original list. +/// +/// # Parameters: +/// - `env`: The JNI environment. +/// - `_class`: The Java class. +/// - `serialized_list`: The byte array resulting of the serialization of a bytearray list. +/// +/// # Returns: +/// - The original list of byte arrays prior to serialization. +/// +#[no_mangle] +#[allow(non_snake_case)] +pub extern "C" fn Java_io_zenoh_jni_JNIZBytes_deserializeIntoListViaJNI( + mut env: JNIEnv, + _class: JClass, + serialized_list: JByteArray, +) -> jobject { + || -> Result { + let payload = decode_byte_array(&env, serialized_list)?; + let zbytes = ZBytes::new(payload); + zbytes_to_java_list(&mut env, &zbytes).map_err(|err| jni_error!(err)) + }() + .unwrap_or_else(|err| { + throw_exception!(env, err); + JObject::null().as_raw() + }) +} + +fn zbytes_to_java_list(env: &mut JNIEnv, zbytes: &ZBytes) -> jni::errors::Result { + let array_list = env.new_object("java/util/ArrayList", "()V", &[])?; + let jlist = JList::from_env(env, &array_list)?; + for value in zbytes.iter::>() { + let value = &mut env.byte_array_from_slice(value.unwrap().as_slice())?; //The unwrap is unfallible. + jlist.add(env, value)?; + } + Ok(array_list.as_raw()) +} diff --git a/zenoh-kotlin/build.gradle.kts b/zenoh-kotlin/build.gradle.kts index 16c49998c..40eaaef6e 100644 --- a/zenoh-kotlin/build.gradle.kts +++ b/zenoh-kotlin/build.gradle.kts @@ -60,11 +60,15 @@ kotlin { implementation("commons-net:commons-net:3.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") + implementation("org.jetbrains.kotlin:kotlin-reflect") } } val commonTest by getting { dependencies { implementation(kotlin("test")) + implementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + implementation("org.junit.jupiter:junit-jupiter-params:5.10.0") } } if (androidEnabled) { @@ -105,13 +109,13 @@ kotlin { } } } - tasks.withType { doFirst { // The line below is added for the Android Unit tests which are equivalent to the JVM tests. // For them to work we need to specify the path to the native library as a system property and not as a jvmArg. systemProperty("java.library.path", "../zenoh-jni/target/$buildMode") } + useJUnitPlatform() } tasks.whenObjectAdded { diff --git a/zenoh-kotlin/src/androidMain/kotlin/io.zenoh/Zenoh.kt b/zenoh-kotlin/src/androidMain/kotlin/io.zenoh/Zenoh.kt index c8d9ad082..2d8c3d5ca 100644 --- a/zenoh-kotlin/src/androidMain/kotlin/io.zenoh/Zenoh.kt +++ b/zenoh-kotlin/src/androidMain/kotlin/io.zenoh/Zenoh.kt @@ -18,18 +18,9 @@ package io.zenoh * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the * log level configuration. */ -internal actual class Zenoh private actual constructor() { - - actual companion object { - private const val ZENOH_LIB_NAME = "zenoh_jni" - private const val ZENOH_LOGS_PROPERTY = "zenoh.logger" - - private var instance: Zenoh? = null - - actual fun load() { - instance ?: Zenoh().also { instance = it } - } - } +internal actual object ZenohLoad { + private const val ZENOH_LIB_NAME = "zenoh_jni" + private const val ZENOH_LOGS_PROPERTY = "zenoh.logger" init { System.loadLibrary(ZENOH_LIB_NAME) diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Config.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Config.kt index da43c92f2..a4c17a52a 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Config.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Config.kt @@ -16,22 +16,98 @@ package io.zenoh import java.io.File import java.nio.file.Path -import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement - /** + * # Config + * * Config class to set the Zenoh configuration to be used through a [Session]. * - * @property path The path to the configuration file. - * @constructor Create empty Config + * The configuration can be specified in two different ways: + * - By providing a file or a path to a file with the configuration + * - By providing a raw string configuration. + * + * Either way, the supported formats are `yaml`, `json` and `json5`. + * + * ## Example: + * - Json5 + * ```kotlin + * val json5config = """ + * { + * mode: "peer", + * connect: { + * endpoints: ["tcp/localhost:7450"], + * }, + * scouting: { + * multicast: { + * enabled: false, + * } + * } + * } + * """.trimIndent() + * val config = Config(config = json5Config, format = Config.Format.JSON5) + * Session.open(config).onSuccess { + * // ... + * } + * ``` + * + * - Json + * ```kotlin + * val jsonConfig = """ + * { + * mode: "peer", + * listen: { + * endpoints: ["tcp/localhost:7450"], + * }, + * scouting: { + * multicast: { + * enabled: false, + * } + * } + * } + * """.trimIndent() + * val config = Config(config = json5Config, format = Config.Format.JSON) + * Session.open(config).onSuccess { + * // ... + * } + * ``` + * + * - Yaml + * ```kotlin + * val yamlConfig = """ + * mode: peer + * connect: + * endpoints: + * - tcp/localhost:7450 + * scouting: + * multicast: + * enabled: false + * """.trimIndent() + * val config = Config(config = yamlConfig, format = Config.Format.YAML) + * Session.open(config).onSuccess { + * // ... + * } + * ``` + * + * Visit the [default configuration](https://github.com/eclipse-zenoh/zenoh/blob/main/DEFAULT_CONFIG.json5) for more + * information on the Zenoh config parameters. + * + * @property path The path to the configuration file (supported types: JSON5, JSON and YAML). + * @property config Raw string configuration, (supported types: JSON5, JSON and YAML). + * @property format [Format] of the configuration. */ -class Config private constructor(internal val path: Path? = null, internal val jsonConfig: JsonElement? = null) { +class Config private constructor(internal val path: Path? = null, internal val config: String? = null, val format: Format? = null) { + + enum class Format { + YAML, + JSON, + JSON5 + } companion object { /** - * Loads the default zenoh configuration. + * Returns the default config. */ fun default(): Config { return Config() @@ -43,7 +119,7 @@ class Config private constructor(internal val path: Path? = null, internal val j * @param file The zenoh config file. */ fun from(file: File): Config { - return Config(file.toPath()) + return Config(path = file.toPath()) } /** @@ -56,12 +132,10 @@ class Config private constructor(internal val path: Path? = null, internal val j } /** - * Loads the configuration from the [json] specified. - * - * @param json The raw zenoh config. + * Loads the configuration from the [config] param. The [Format] needs to be specified explicitly. */ - fun from(json: String): Config { - return Config(jsonConfig = Json.decodeFromString(json)) + fun from(config: String, format: Format): Config { + return Config(config = config, format = format) } /** @@ -69,8 +143,6 @@ class Config private constructor(internal val path: Path? = null, internal val j * * @param jsonElement The zenoh config as a [JsonElement]. */ - fun from(jsonElement: JsonElement) = Config(jsonElement) + fun from(jsonElement: JsonElement) = Config(config = jsonElement.toString(), format = Format.JSON) } - - private constructor(jsonConfig: JsonElement) : this(null, jsonConfig = jsonConfig) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt index 6712ef157..3852d4091 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Session.kt @@ -16,15 +16,18 @@ package io.zenoh import io.zenoh.exceptions.SessionException import io.zenoh.handlers.Callback +import io.zenoh.handlers.ChannelHandler +import io.zenoh.handlers.Handler import io.zenoh.jni.JNISession import io.zenoh.keyexpr.KeyExpr +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes import io.zenoh.publication.Delete import io.zenoh.publication.Publisher import io.zenoh.publication.Put import io.zenoh.query.* import io.zenoh.queryable.Query import io.zenoh.queryable.Queryable -import io.zenoh.sample.Attachment import io.zenoh.sample.Sample import io.zenoh.selector.Selector import io.zenoh.subscriber.Reliability @@ -37,9 +40,10 @@ import java.time.Duration * A Zenoh Session, the core interaction point with a Zenoh network. * * A session is typically associated with declarations such as [Publisher]s, [Subscriber]s, or [Queryable]s, which are - * declared using [declarePublisher], [declareSubscriber], and [declareQueryable], respectively. + * declared using [declarePublisher], [declareSubscriber], and [declareQueryable], respectively. It is also possible to + * declare key expressions ([KeyExpr]) as well with [declareKeyExpr] for optimization purposes. + * * Other operations such as simple Put, Get or Delete can be performed from a session using [put], [get] and [delete]. - * Finally, it's possible to declare key expressions ([KeyExpr]) as well. * * Sessions are open upon creation and can be closed manually by calling [close]. Alternatively, the session will be * automatically closed when used with Java's try-with-resources statement or its Kotlin counterpart, [use]. @@ -52,20 +56,12 @@ class Session private constructor(private val config: Config) : AutoCloseable { private var jniSession: JNISession? = null + private var declarations = mutableListOf() + companion object { private val sessionClosedException = SessionException("Session is closed.") - /** - * Open a [Session] with the default [Config]. - * - * @return a [Result] with the [Session] on success. - */ - fun open(): Result { - val session = Session(Config.default()) - return session.launch() - } - /** * Open a [Session] with the provided [Config]. * @@ -78,19 +74,19 @@ class Session private constructor(private val config: Config) : AutoCloseable { } } - init { - Zenoh.load() - } - /** Close the session. */ override fun close() { + declarations.removeIf { + it.undeclare() + true + } + jniSession?.close() jniSession = null } - @Suppress("removal") protected fun finalize() { - jniSession?.close() + close() } /** @@ -98,141 +94,318 @@ class Session private constructor(private val config: Config) : AutoCloseable { * * Example: * ```kotlin - * Session.open().onSuccess { + * Session.open(Config.default()).onSuccess { * it.use { session -> * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * session.declarePublisher(keyExpr) - * .priority(Priority.REALTIME) - * .congestionControl(CongestionControl.DROP) - * .res().onSuccess { pub -> - * pub.use { - * println("Publisher declared on $keyExpr.") - * var i = 0 - * while (true) { - * val payload = "Hello for the ${i}th time!" - * println(payload) - * pub.put(payload).res() - * Thread.sleep(1000) - * i++ - * } + * session.declarePublisher(keyExpr).onSuccess { pub -> + * pub.use { + * println("Publisher declared on $keyExpr.") + * var i = 0 + * while (true) { + * val payload = "Hello for the ${i}th time!" + * println(payload) + * pub.put(payload) + * Thread.sleep(1000) + * i++ * } * } + * } * } * } * } * ``` * * @param keyExpr The [KeyExpr] the publisher will be associated to. - * @return A resolvable [Publisher.Builder] + * @param qos The [QoS] configuration of the publisher. + * @return The result of the declaration, returning the publisher in case of success. */ - fun declarePublisher(keyExpr: KeyExpr): Publisher.Builder = Publisher.Builder(this, keyExpr) + fun declarePublisher(keyExpr: KeyExpr, qos: QoS = QoS.default()): Result { + return resolvePublisher(keyExpr, qos) + } /** - * Declare a [Subscriber] on the session. - * - * The default receiver is a [Channel], but can be changed with the [Subscriber.Builder.with] functions. + * Declare a [Subscriber] on the session, specifying a callback to handle incoming samples. * * Example: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> + * session.declareSubscriber(keyExpr, callback = { sample -> println(sample) }).onSuccess { + * println("Declared subscriber on $keyExpr.") + * } + * } + * } + * } + * ``` + * + * @param keyExpr The [KeyExpr] the subscriber will be associated to. + * @param callback Callback to handle the received samples. + * @param onClose Callback function to be called when the subscriber is closed. + * @param reliability The reliability the subscriber wishes to obtain from the network. + * @return A result with the [Subscriber] in case of success. + */ + fun declareSubscriber( + keyExpr: KeyExpr, + callback: Callback, + onClose: (() -> Unit)? = null, + reliability: Reliability = Reliability.BEST_EFFORT + ): Result> { + val resolvedOnClose = fun() { + onClose?.invoke() + } + return resolveSubscriber(keyExpr, callback, resolvedOnClose, Unit, reliability) + } + + /** + * Declare a [Subscriber] on the session, specifying a handler to handle incoming samples. * + * Example: * ```kotlin - * Session.open().onSuccess { session -> + * + * class ExampleHandler: Handler { + * override fun handle(t: Sample) = println(t) + * + * override fun receiver() = Unit + * + * override fun onClose() = println("Closing handler") + * } + * + * Session.open(Config.default()).onSuccess { session -> * session.use { * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> - * session.declareSubscriber(keyExpr) - * .bestEffort() - * .res() - * .onSuccess { subscriber -> - * subscriber.use { - * println("Declared subscriber on $keyExpr.") - * runBlocking { - * val receiver = subscriber.receiver!! - * val iterator = receiver.iterator() - * while (iterator.hasNext()) { - * val sample = iterator.next() - * println(sample) - * } - * } + * session.declareSubscriber(keyExpr, handler = ExampleHandler()) + * .onSuccess { + * println("Declared subscriber on $keyExpr.") * } - * } + * } * } * } - * } * ``` * * @param keyExpr The [KeyExpr] the subscriber will be associated to. - * @return A [Subscriber.Builder] with a [Channel] receiver. + * @param handler [Handler] implementation to handle the received samples. [Handler.onClose] will be called + * upon closing the session. + * @param onClose Callback function to be called when the subscriber is closed. + * @param reliability The reliability the subscriber wishes to obtain from the network. + * @return A result with the [Subscriber] in case of success. */ - fun declareSubscriber(keyExpr: KeyExpr): Subscriber.Builder> = Subscriber.newBuilder(this, keyExpr) + fun declareSubscriber( + keyExpr: KeyExpr, + handler: Handler, + onClose: (() -> Unit)? = null, + reliability: Reliability = Reliability.BEST_EFFORT + ): Result> { + val resolvedOnClose = fun() { + handler.onClose() + onClose?.invoke() + } + val callback = Callback { t: Sample -> handler.handle(t) } + return resolveSubscriber(keyExpr, callback, resolvedOnClose, handler.receiver(), reliability) + } /** - * Declare a [Queryable] on the session. + * Declare a [Subscriber] on the session, specifying a channel pipe the received samples. + * + * Example: + * ```kotlin + * + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> + * val samplesChannel = Channel() + * session.declareSubscriber(keyExpr, channel = samplesChannel) + * .onSuccess { + * println("Declared subscriber on $keyExpr.") + * } + * } + * // ... + * } + * } + * ``` * - * The default receiver is a [Channel], but can be changed with the [Queryable.Builder.with] functions. + * @param keyExpr The [KeyExpr] the subscriber will be associated to. + * @param channel [Channel] instance through which the received samples will be piped. Once the subscriber is + * closed, the channel is closed as well. + * @param onClose Callback function to be called when the subscriber is closed. [Handler.onClose] will be called + * upon closing the session. + * @param reliability The reliability the subscriber wishes to obtain from the network. + * @return A result with the [Subscriber] in case of success. + */ + fun declareSubscriber( + keyExpr: KeyExpr, + channel: Channel, + onClose: (() -> Unit)? = null, + reliability: Reliability = Reliability.BEST_EFFORT + ): Result>> { + val channelHandler = ChannelHandler(channel) + val resolvedOnClose = fun() { + channelHandler.onClose() + onClose?.invoke() + } + val callback = Callback { t: Sample -> channelHandler.handle(t) } + return resolveSubscriber(keyExpr, callback, resolvedOnClose, channelHandler.receiver(), reliability) + } + + /** + * Declare a [Queryable] on the session with a callback. * * Example: * ```kotlin - * Session.open().onSuccess { session -> session.use { + * Session.open(Config.default()).onSuccess { session -> session.use { + * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> + * println("Declaring Queryable") + * val queryable = session.declareQueryable(keyExpr, callback = { query -> + * query.replySuccess(keyExpr, value = Value("Hello!")) + * .onSuccess { println("Replied hello.") } + * .onFailure { println(it) } + * }).getOrThrow() + * } + * }} + * ``` + * + * @param keyExpr The [KeyExpr] the queryable will be associated to. + * @param callback The callback to handle the received queries. + * @param onClose Callback to be run upon closing the queryable. + * @param complete The queryable completeness. + * @return A result with the queryable. + * @see Query + */ + fun declareQueryable( + keyExpr: KeyExpr, + callback: Callback, + onClose: (() -> Unit)? = null, + complete: Boolean = false + ): Result> { + return resolveQueryable(keyExpr, callback, fun() { onClose?.invoke() }, Unit, complete) + } + + /** + * Declare a [Queryable] on the session with a [Handler]. + * + * Example: we create a class `ExampleHandler` that implements the [Handler] interface to reply + * to the incoming queries: + * + * ```kotlin + * class ExampleHandler: Handler { + * override fun handle(t: Query) = query.replySuccess(query.keyExpr, value = Value("Hello!")) + * + * override fun receiver() = Unit + * + * override fun onClose() = println("Closing handler") + * } + * ``` + * + * Then we'd use it as follows: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> session.use { * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> * println("Declaring Queryable") - * session.declareQueryable(keyExpr).res().onSuccess { queryable -> - * queryable.use { - * it.receiver?.let { receiverChannel -> - * runBlocking { - * val iterator = receiverChannel.iterator() - * while (iterator.hasNext()) { - * iterator.next().use { query -> - * println("Received query at ${query.keyExpr}") - * query.reply(keyExpr) - * .success("Hello!") - * .withKind(SampleKind.PUT) - * .withTimeStamp(TimeStamp.getCurrentTime()) - * .res() - * .onSuccess { println("Replied hello.") } - * .onFailure { println(it) } - * } - * } + * val exampleHandler = ExampleHandler() + * val queryable = session.declareQueryable(keyExpr, handler = exampleHandler).getOrThrow() + * // ... + * } + * }} + * ``` + * + * @param keyExpr The [KeyExpr] the queryable will be associated to. + * @param handler The [Handler] to handle the incoming queries. [Handler.onClose] will be called upon + * closing the queryable. + * @param onClose Callback to be run upon closing the queryable. + * @param complete The completeness of the queryable. + * @return A result with the queryable. + */ + fun declareQueryable( + keyExpr: KeyExpr, + handler: Handler, + onClose: (() -> Unit)? = null, + complete: Boolean = false + ): Result> { + return resolveQueryable(keyExpr, { t: Query -> handler.handle(t) }, fun() { + handler.onClose() + onClose?.invoke() + }, handler.receiver(), complete) + } + + /** + * Declare a [Queryable] with a [Channel] to pipe the incoming queries. + * + * Example: + * ```kotlin + * Session.open(config).onSuccess { session -> + * session.use { + * key.intoKeyExpr().onSuccess { keyExpr -> + * println("Declaring Queryable on $key...") + * session.declareQueryable(keyExpr, Channel()).onSuccess { queryable -> + * runBlocking { + * for (query in queryable.receiver) { + * val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" + * println(">> [Queryable] Received Query '${query.selector}' $valueInfo") + * query.replySuccess(keyExpr, value = Value("Example reply")) + * .onSuccess { println(">> [Queryable ] Performed reply...") } + * .onFailure { println(">> [Queryable ] Error sending reply: $it") } * } * } * } * } * } - * }} + * } * ``` * - * * @param keyExpr The [KeyExpr] the queryable will be associated to. - * @return A [Queryable.Builder] with a [Channel] receiver. + * @param channel The [Channel] to receive the incoming queries. It will be closed upon closing the queryable. + * @param onClose Callback to be run upon closing the queryable. + * @param complete The completeness of the queryable. + * @return A result with the queryable, where the [Queryable.receiver] is the provided [Channel]. */ - fun declareQueryable(keyExpr: KeyExpr): Queryable.Builder> = Queryable.newBuilder(this, keyExpr) + fun declareQueryable( + keyExpr: KeyExpr, + channel: Channel, + onClose: (() -> Unit)? = null, + complete: Boolean = false + ): Result>> { + val handler = ChannelHandler(channel) + return resolveQueryable(keyExpr, { t: Query -> handler.handle(t) }, fun() { + handler.onClose() + onClose?.invoke() + }, handler.receiver(), complete) + } /** * Declare a [KeyExpr]. * * Informs Zenoh that you intend to use the provided Key Expression repeatedly. + * Also, declared key expression provide additional optimizations by associating + * it with a native key expression representation, minimizing the amount of operations + * performed between the JVM and the Rust layer of this library. + * + * A declared key expression is associated to the session from which it was declared. + * It can be undeclared with the function [undeclare], or alternatively when closing + * the session it will be automatically undeclared. Undeclaring a key expression causes + * it to be downgraded to a regular key expression without optimizations, this means + * that operations can still be performed with it. * - * It is generally not needed to declare key expressions, as declaring a subscriber, - * a queryable, or a publisher will also inform Zenoh of your intent to use their - * key expressions repeatedly. + * When declaring a subscriber, a queryable, or a publisher, it is not necessary + * to declare the key expression beforehand, since Zenoh is already informed of your + * intent to use their key expressions repeatedly. It can be handy when doing instead + * many repeated puts or reply operations. * * Example: * ```kotlin - * Session.open().onSuccess { session -> session.use { - * session.declareKeyExpr("demo/kotlin/example").res().onSuccess { keyExpr -> - * keyExpr.use { - * session.declarePublisher(it).res().onSuccess { publisher -> - * // ... - * } - * } + * Session.open(Config.default()).onSuccess { session -> session.use { + * val keyExpr = session.declareKeyExpr("demo/kotlin/example").getOrThrow() + * for (i in 0..999) { + * put(keyExpr, "Put number $i!") * } * }} * ``` * - * @param keyExpr The intended Key expression. - * @return A resolvable returning an optimized representation of the passed `keyExpr`. + * @param keyExpr The intended key expression. + * @return A result with the declared key expression. */ - fun declareKeyExpr(keyExpr: String): Resolvable = Resolvable { - return@Resolvable jniSession?.run { - declareKeyExpr(keyExpr) + fun declareKeyExpr(keyExpr: String): Result { + return jniSession?.run { + declareKeyExpr(keyExpr).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } @@ -243,198 +416,473 @@ class Session private constructor(private val config: Config) : AutoCloseable { * otherwise the operation will result in a failure. * * @param keyExpr The key expression to undeclare. - * @return A resolvable returning the status of the undeclare operation. + * @return A result with the status of the undeclare operation. */ - fun undeclare(keyExpr: KeyExpr): Resolvable = Resolvable { - return@Resolvable jniSession?.run { + fun undeclare(keyExpr: KeyExpr): Result { + return jniSession?.run { undeclareKeyExpr(keyExpr) } ?: Result.failure(sessionClosedException) } /** - * Declare a [Get] with a [Channel] receiver. + * Performs a Get query on the [selector], handling the replies with a callback. * + * A callback must be provided to handle the incoming replies. A basic query can be achieved + * as follows: * ```kotlin - * val timeout = Duration.ofMillis(10000) - * println("Opening Session") - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> - * session.get(keyExpr) - * .consolidation(ConsolidationMode.NONE) - * .target(QueryTarget.BEST_MATCHING) - * .withValue("Get value example") - * .with { reply -> println("Received reply $reply") } - * .timeout(timeout) - * .res() - * .onSuccess { - * // Leaving the session alive the same duration as the timeout for the sake of this example. - * Thread.sleep(timeout.toMillis()) + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoSelector().onSuccess { selector -> + * session.get(selector, callback = { reply -> println(reply) }) + * } + * } + * } + * ``` + * + * Additionally, other optional parameters to the query can be specified, and the result + * of the operation can be checked as well: + * + * Example: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoSelector().onSuccess { selector -> + * session.get( + * selector, + * callback = { reply -> println(reply) }, + * value = Value("Example value"), + * target = QueryTarget.BEST_MATCHING, + * attachment = ZBytes.from("Example attachment"), + * timeout = Duration.ofMillis(1000), + * onClose = { println("Query terminated.") } + * ).onSuccess { + * println("Get query launched...") + * }.onFailure { + * println("Error: $it") * } * } * } * } * ``` - * @param selector The [KeyExpr] to be used for the get operation. - * @return a resolvable [Get.Builder] with a [Channel] receiver. + * + * @param selector The [Selector] on top of which the get query will be performed. + * @param callback [Callback] to handle the replies. + * @param value Optional [Value] for the query. + * @param attachment Optional attachment. + * @param target The [QueryTarget] of the query. + * @param consolidation The [ConsolidationMode] configuration. + * @param onClose Callback to be executed when the query is terminated. + * @return A [Result] with the status of the query. When [Result.success] is returned, that means + * the query was properly launched and not that it has received all the possible replies (this + * can't be known from the perspective of the query). */ - fun get(selector: Selector): Get.Builder> = Get.newBuilder(this, selector) + fun get( + selector: Selector, + callback: Callback, + value: Value? = null, + attachment: ZBytes? = null, + timeout: Duration = Duration.ofMillis(10000), + target: QueryTarget = QueryTarget.BEST_MATCHING, + consolidation: ConsolidationMode = ConsolidationMode.NONE, + onClose: (() -> Unit)? = null + ) : Result { + return resolveGet ( + selector = selector, + callback = callback, + onClose = fun() { onClose?.invoke() }, + receiver = Unit, + timeout = timeout, + target = target, + consolidation = consolidation, + value = value, + attachment = attachment + ) + } /** - * Declare a [Get] with a [Channel] receiver. + * Performs a Get query on the [selector], handling the replies with a [Handler]. * + * A handler must be provided to handle the incoming replies. For instance, imagine we implement + * a `QueueHandler`: * ```kotlin - * val timeout = Duration.ofMillis(10000) - * println("Opening Session") - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> - * session.get(keyExpr) - * .consolidation(ConsolidationMode.NONE) - * .target(QueryTarget.BEST_MATCHING) - * .withValue("Get value example") - * .with { reply -> println("Received reply $reply") } - * .timeout(timeout) - * .res() - * .onSuccess { - * // Leaving the session alive the same duration as the timeout for the sake of this example. - * Thread.sleep(timeout.toMillis()) + * class QueueHandler : Handler> { + * private val queue: ArrayDeque = ArrayDeque() + * + * override fun handle(t: T) { + * queue.add(t) + * } + * + * override fun receiver(): ArrayDeque { + * return queue + * } + * + * override fun onClose() { + * println("Received in total ${queue.size} elements.") + * } + * } + * ``` + * + * then we could use it as follows: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoSelector().onSuccess { selector -> + * val handler = QueueHandler() + * val receiver = session.get(selector, handler).getOrThrow() + * // ... + * for (reply in receiver) { + * println(reply) + * } + * } + * } + * } + * ``` + * + * Additionally, other optional parameters to the query can be specified, and the result + * of the operation can be checked as well: + * + * Example: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoSelector().onSuccess { selector -> + * val handler = QueueHandler() + * session.get( + * selector, + * handler, + * value = Value("Example value"), + * target = QueryTarget.BEST_MATCHING, + * attachment = ZBytes.from("Example attachment"), + * timeout = Duration.ofMillis(1000), + * onClose = { println("Query terminated.") } + * ).onSuccess { receiver -> + * println("Get query launched...") + * // ... + * for (reply in receiver) { + * println(reply) + * } + * }.onFailure { + * println("Error: $it") * } * } * } * } * ``` - * @param keyExpr The [KeyExpr] to be used for the get operation. - * @return a resolvable [Get.Builder] with a [Channel] receiver. + * + * @param selector The [Selector] on top of which the get query will be performed. + * @param handler [Handler] to handle the replies. + * @param value Optional [Value] for the query. + * @param attachment Optional attachment. + * @param target The [QueryTarget] of the query. + * @param consolidation The [ConsolidationMode] configuration. + * @param onClose Callback to be executed when the query is terminated. + * @return A [Result] with the [handler]'s receiver of type [R]. When [Result.success] is returned, that means + * the query was properly launched and not that it has received all the possible replies (this + * can't be known from the perspective of the query). + */ + fun get( + selector: Selector, + handler: Handler, + value: Value? = null, + attachment: ZBytes? = null, + timeout: Duration = Duration.ofMillis(10000), + target: QueryTarget = QueryTarget.BEST_MATCHING, + consolidation: ConsolidationMode = ConsolidationMode.NONE, + onClose: (() -> Unit)? = null + ) : Result { + return resolveGet( + selector = selector, + callback = { r: Reply -> handler.handle(r) }, + onClose = fun() { + handler.onClose() + onClose?.invoke() + }, + receiver = handler.receiver(), + timeout = timeout, + target = target, + consolidation = consolidation, + value = value, + attachment = attachment + ) + } + + /** + * Performs a Get query on the [selector], handling the replies with a blocking [Channel]. + * + * Example: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoSelector().onSuccess { selector -> + * session.get(selector, channel = Channel()).onSuccess { channel -> + * runBlocking { + * for (reply in channel) { + * println("Received $reply") + * } + * } + * }.onFailure { + * println("Error: $it") + * } + * } + * } + * } + * ``` + * + * Additionally, other optional parameters to the query can be specified, and the result + * of the operation can be checked as well: + * + * Example: + * + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoSelector().onSuccess { selector -> + * session.get(selector, + * channel = Channel(), + * value = Value("Example value"), + * target = QueryTarget.BEST_MATCHING, + * attachment = ZBytes.from("Example attachment"), + * timeout = Duration.ofMillis(1000), + * onClose = { println("Query terminated.") } + * ).onSuccess { channel -> + * runBlocking { + * for (reply in channel) { + * println("Received $reply") + * } + * } + * }.onFailure { + * println("Error: $it") + * } + * } + * } + * } + * ``` + * + * @param selector The [Selector] on top of which the get query will be performed. + * @param channel Blocking [Channel] to handle the replies. + * @param value Optional [Value] for the query. + * @param attachment Optional attachment. + * @param target The [QueryTarget] of the query. + * @param consolidation The [ConsolidationMode] configuration. + * @param onClose Callback to be executed when the query is terminated. + * @return A [Result] with the [channel] on success. When [Result.success] is returned, that means + * the query was properly launched and not that it has received all the possible replies (this + * can't be known from the perspective of the query). */ - fun get(keyExpr: KeyExpr): Get.Builder> = Get.newBuilder(this, Selector(keyExpr)) + fun get( + selector: Selector, + channel: Channel, + value: Value? = null, + attachment: ZBytes? = null, + timeout: Duration = Duration.ofMillis(10000), + target: QueryTarget = QueryTarget.BEST_MATCHING, + consolidation: ConsolidationMode = ConsolidationMode.NONE, + onClose: (() -> Unit)? = null + ) : Result> { + val channelHandler = ChannelHandler(channel) + return resolveGet( + selector = selector, + callback = { r: Reply -> channelHandler.handle(r) }, + onClose = fun() { + channelHandler.onClose() + onClose?.invoke() + }, + receiver = channelHandler.receiver(), + timeout = timeout, + target = target, + consolidation = consolidation, + value = value, + attachment = attachment + ) + } /** * Declare a [Put] with the provided value on the specified key expression. * * Example: * ```kotlin - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * session.put(keyExpr, Value("Hello")) - * .congestionControl(CongestionControl.BLOCK) - * .priority(Priority.REALTIME) - * .kind(SampleKind.PUT) - * .res() - * .onSuccess { println("Put 'Hello' on $keyExpr.") } - * }} + * Session.open(config).onSuccess { session -> + * session.use { + * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> + * session.put(keyExpr, value = Value("Example value")).getOrThrow() + * } + * // ... + * } + * } + * ``` + * + * Additionally, a [QoS] configuration can be specified as well as an attachment, for instance: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> + * val exampleQoS = QoS( + * congestionControl = CongestionControl.DROP, + * express = true, + * priority = Priority.DATA_HIGH) + * val exampleAttachment = "exampleAttachment".into() + * session.put( + * keyExpr, + * value = Value("Example value"), + * qos = exampleQoS, + * attachment = exampleAttachment).getOrThrow() + * } + * // ... + * } * } * ``` * * @param keyExpr The [KeyExpr] to be used for the put operation. * @param value The [Value] to be put. - * @return A resolvable [Put.Builder]. + * @param qos The [QoS] configuration. + * @param attachment Optional attachment. + * @return A [Result] with the status of the put operation. */ - fun put(keyExpr: KeyExpr, value: Value): Put.Builder = Put.newBuilder(this, keyExpr, value) + fun put(keyExpr: KeyExpr, value: Value, qos: QoS = QoS.default(), attachment: ZBytes? = null) : Result { + val put = Put(keyExpr, value, qos, attachment) + return resolvePut(keyExpr, put) + } /** - * Declare a [Put] with the provided value on the specified key expression. + * Declare a [Put] with the provided message on the specified key expression. * * Example: * ```kotlin - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * session.put(keyExpr, "Hello") - * .congestionControl(CongestionControl.BLOCK) - * .priority(Priority.REALTIME) - * .kind(SampleKind.PUT) - * .res() - * .onSuccess { println("Put 'Hello' on $keyExpr.") } - * }} + * Session.open(config).onSuccess { session -> + * session.use { + * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> + * session.put(keyExpr, "Example message").getOrThrow() + * } + * // ... + * } + * } + * ``` + * + * Additionally, a [QoS] configuration can be specified as well as an attachment, for instance: + * ```kotlin + * Session.open(Config.default()).onSuccess { session -> + * session.use { + * "a/b/c".intoKeyExpr().onSuccess { keyExpr -> + * val exampleQoS = QoS( + * congestionControl = CongestionControl.DROP, + * express = true, + * priority = Priority.DATA_HIGH) + * val exampleAttachment = "exampleAttachment".into() + * session.put( + * keyExpr, + * message = "Example message", + * qos = exampleQoS, + * attachment = exampleAttachment).getOrThrow() + * } + * // ... + * } * } * ``` * * @param keyExpr The [KeyExpr] to be used for the put operation. - * @param message The message to be put. - * @return A resolvable [Put.Builder]. + * @param message The [String] message to put. + * @param qos The [QoS] configuration. + * @param attachment Optional attachment. + * @return A [Result] with the status of the put operation. */ - fun put(keyExpr: KeyExpr, message: String): Put.Builder = Put.newBuilder(this, keyExpr, Value(message)) + fun put(keyExpr: KeyExpr, message: String, qos: QoS = QoS.default(), attachment: ZBytes? = null) : Result { + val put = Put(keyExpr, Value(message), qos, attachment) + return resolvePut(keyExpr, put) + } /** - * Declare a [Delete]. + * Perform a delete operation. * * Example: - * * ```kotlin - * println("Opening Session") - * Session.open().onSuccess { session -> + * Session.open(config).onSuccess { session -> * session.use { - * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> - * session.delete(keyExpr) - * .res() - * .onSuccess { - * println("Performed a delete on $keyExpr.") - * } + * key.intoKeyExpr().onSuccess { keyExpr -> + * println("Deleting resources matching '$keyExpr'...") + * session.delete(keyExpr) * } * } * } * ``` * * @param keyExpr The [KeyExpr] to be used for the delete operation. - * @return a resolvable [Delete.Builder]. + * @param qos The [QoS] configuration. + * @param attachment Optional [ZBytes] attachment. + * @return a [Result] with the status of the operation. */ - fun delete(keyExpr: KeyExpr): Delete.Builder = Delete.newBuilder(this, keyExpr) + fun delete(keyExpr: KeyExpr, qos: QoS = QoS.default(), attachment: ZBytes? = null): Result { + val delete = Delete(keyExpr, qos, attachment) + return resolveDelete(keyExpr, delete) + } /** Returns if session is open or has been closed. */ fun isOpen(): Boolean { return jniSession != null } - internal fun resolvePublisher(builder: Publisher.Builder): Result { + private fun resolvePublisher(keyExpr: KeyExpr, qos: QoS): Result { return jniSession?.run { - declarePublisher(builder) + declarePublisher(keyExpr, qos).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } - internal fun resolveSubscriber( + private fun resolveSubscriber( keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, reliability: Reliability ): Result> { return jniSession?.run { - declareSubscriber(keyExpr, callback, onClose, receiver, reliability) + declareSubscriber(keyExpr, callback, onClose, receiver, reliability).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } - internal fun resolveQueryable( + private fun resolveQueryable( keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, complete: Boolean ): Result> { return jniSession?.run { - declareQueryable(keyExpr, callback, onClose, receiver, complete) + declareQueryable(keyExpr, callback, onClose, receiver, complete).onSuccess { declarations.add(it) } } ?: Result.failure(sessionClosedException) } - internal fun resolveGet( + private fun resolveGet( selector: Selector, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, timeout: Duration, target: QueryTarget, consolidation: ConsolidationMode, value: Value?, - attachment: Attachment?, - ): Result { + attachment: ZBytes?, + ): Result { return jniSession?.run { - performGet(selector, callback, onClose, receiver, timeout, target, consolidation, value, attachment) + performGet( + selector, + callback, + onClose, + receiver, + timeout, + target, + consolidation, + value?.payload, + value?.encoding, + attachment + ) } ?: Result.failure(sessionClosedException) } - internal fun resolvePut(keyExpr: KeyExpr, put: Put): Result = runCatching { + private fun resolvePut(keyExpr: KeyExpr, put: Put): Result = runCatching { jniSession?.run { performPut(keyExpr, put) } } - internal fun resolveDelete(keyExpr:KeyExpr, delete: Delete): Result = runCatching { - jniSession?.run { performPut(keyExpr, delete) } + private fun resolveDelete(keyExpr: KeyExpr, delete: Delete): Result = runCatching { + jniSession?.run { performDelete(keyExpr, delete) } } /** Launches the session through the jni session, returning the [Session] on success. */ @@ -445,4 +893,3 @@ class Session private constructor(private val config: Config) : AutoCloseable { .onFailure { jniSession = null } } } - diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/SessionDeclaration.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/SessionDeclaration.kt index 1c747e40f..ecdfd8602 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/SessionDeclaration.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/SessionDeclaration.kt @@ -18,13 +18,10 @@ package io.zenoh * Session declaration. * * A session declaration is either a [io.zenoh.publication.Publisher], - * a [io.zenoh.subscriber.Subscriber] or a [io.zenoh.queryable.Queryable] declared from a [Session]. + * a [io.zenoh.subscriber.Subscriber], a [io.zenoh.queryable.Queryable] or a [io.zenoh.keyexpr.KeyExpr] declared from a [Session]. */ interface SessionDeclaration { - /** Returns true if the declaration has not been undeclared. */ - fun isValid(): Boolean - /** Undeclare a declaration. No further operations should be performed after calling this function. */ fun undeclare() } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Zenoh.kt index ee1561306..a6861ba8f 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Zenoh.kt @@ -18,8 +18,4 @@ package io.zenoh * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the * log level configuration. */ -internal expect class Zenoh private constructor() { - companion object { - fun load() - } -} +internal expect object ZenohLoad \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt index 06097dcfa..16d6c91f5 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/ChannelHandler.kt @@ -21,15 +21,13 @@ import kotlinx.coroutines.runBlocking /** * Channel handler * - * Implementation of a [Handler] with a [Channel] receiver. This handler is intended to be used - * as the default handler by the [io.zenoh.queryable.Queryable], [io.zenoh.subscriber.Subscriber] and [io.zenoh.query.Get], - * allowing us to send the incoming elements through a [Channel] within the context of a Kotlin coroutine. + * Implementation of a [Handler] with a [Channel] receiver. * * @param T * @property channel * @constructor Create empty Channel handler */ -class ChannelHandler(private val channel: Channel) : Handler> { +internal class ChannelHandler(private val channel: Channel) : Handler> { override fun handle(t: T) { runBlocking { channel.send(t) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt index 774c3ce29..18da152a9 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/handlers/Handler.kt @@ -23,7 +23,6 @@ import io.zenoh.ZenohType * **Example**: * ```kotlin * class QueueHandler : Handler> { - * * private val queue: ArrayDeque = ArrayDeque() * * override fun handle(t: T) { @@ -43,11 +42,7 @@ import io.zenoh.ZenohType * * That `QueueHandler` could then be used as follows, for instance for a subscriber: * ```kotlin - * val handler = QueueHandler() - * val receiver = session.declareSubscriber(keyExpr) - * .with(handler) - * .res() - * .onSuccess { ... } + * val subscriber = session.declareSubscriber(keyExpr, handler = QueueHandler()).getOrThrow() * ``` * * @param T A receiving [ZenohType], either a [io.zenoh.sample.Sample], a [io.zenoh.query.Reply] or a [io.zenoh.queryable.Query]. @@ -72,8 +67,7 @@ interface Handler { * * For instances of [io.zenoh.queryable.Queryable] and [io.zenoh.subscriber.Subscriber], * Zenoh triggers this callback when they are closed or undeclared. In the case of a Get query - * (see [io.zenoh.query.Get]), it is invoked when no more elements of type [T] are expected - * to be received. + * it is invoked when no more elements of type [T] are expected to be received. */ fun onClose() } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt index b8d4e9b57..553df26fb 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIKeyExpr.kt @@ -14,19 +14,21 @@ package io.zenoh.jni -import io.zenoh.Zenoh +import io.zenoh.ZenohLoad import io.zenoh.keyexpr.KeyExpr internal class JNIKeyExpr(internal val ptr: Long) { companion object { + init { + ZenohLoad + } + fun tryFrom(keyExpr: String): Result = runCatching { - Zenoh.load() // It may happen the zenoh library is not yet loaded when creating a key expression. KeyExpr(tryFromViaJNI(keyExpr)) } fun autocanonize(keyExpr: String): Result = runCatching { - Zenoh.load() KeyExpr(autocanonizeViaJNI(keyExpr)) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt index 451954ad5..f1b57f971 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIPublisher.kt @@ -14,10 +14,7 @@ package io.zenoh.jni -import io.zenoh.* -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority -import io.zenoh.sample.Attachment +import io.zenoh.protocol.ZBytes import io.zenoh.value.Value /** @@ -31,19 +28,19 @@ internal class JNIPublisher(private val ptr: Long) { * Put operation. * * @param value The [Value] to be put. - * @param attachment Optional [Attachment]. + * @param attachment Optional attachment. */ - fun put(value: Value, attachment: Attachment?): Result = runCatching { - putViaJNI(value.payload, value.encoding.knownEncoding.ordinal, attachment?.let { encodeAttachment(it) }, ptr) + fun put(value: Value, attachment: ZBytes?): Result = runCatching { + putViaJNI(value.payload.bytes, value.encoding.id.ordinal, value.encoding.schema, attachment?.bytes, ptr) } /** * Delete operation. * - * @param attachment Optional [Attachment]. + * @param attachment Optional attachment. */ - fun delete(attachment: Attachment?): Result = runCatching { - deleteViaJNI(attachment?.let { encodeAttachment(it) }, ptr) + fun delete(attachment: ZBytes?): Result = runCatching { + deleteViaJNI(attachment?.bytes, ptr) } /** @@ -55,61 +52,14 @@ internal class JNIPublisher(private val ptr: Long) { freePtrViaJNI(ptr) } - /** - * Set the congestion control policy of the publisher. - * - * This function is not thread safe. - * - * @param congestionControl: The [CongestionControl] policy. - * @return A [Result] with the status of the operation. - */ - fun setCongestionControl(congestionControl: CongestionControl): Result = runCatching { - setCongestionControlViaJNI(congestionControl.value, ptr) - } - - /** - * Set the priority policy of the publisher. - * - * This function is not thread safe. - * - * @param priority: The [Priority] policy. - * @return A [Result] with the status of the operation. - */ - fun setPriority(priority: Priority): Result = runCatching { - setPriorityViaJNI(priority.value, ptr) - } - - /** - * Set the congestion control policy of the publisher through JNI. - * - * This function is NOT thread safe. - * - * @param congestionControl The congestion control policy. - * @param ptr Pointer to the publisher. - */ - private external fun setCongestionControlViaJNI(congestionControl: Int, ptr: Long) - - /** - * Set the priority policy of the publisher through JNI. - * - * This function is NOT thread safe. - * - * @param priority The priority policy. - * @param ptr Pointer to the publisher. - */ - private external fun setPriorityViaJNI(priority: Int, ptr: Long) - - - /** Puts through the native Publisher. */ @Throws(Exception::class) private external fun putViaJNI( - valuePayload: ByteArray, valueEncoding: Int, encodedAttachment: ByteArray?, ptr: Long + valuePayload: ByteArray, encodingId: Int, encodingSchema: String?, attachment: ByteArray?, ptr: Long ) @Throws(Exception::class) - private external fun deleteViaJNI(encodedAttachment: ByteArray?, ptr: Long) + private external fun deleteViaJNI(attachment: ByteArray?, ptr: Long) - /** Frees the underlying native Publisher. */ private external fun freePtrViaJNI(ptr: Long) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQoS.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQoS.kt deleted file mode 100644 index 102ce4460..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQoS.kt +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright (c) 2024 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.jni - -import io.zenoh.prelude.CongestionControl; -import io.zenoh.prelude.Priority; - -internal class JNIQoS internal constructor(internal val qos: Byte) { - - internal constructor(): this(getDefaultQoSViaJNI()) - - fun getExpress(): Boolean { - return getExpressViaJNI(qos) - } - - fun getCongestionControl(): CongestionControl { - return CongestionControl.fromInt(getCongestionControlViaJNI(qos)) - } - - fun getPriority(): Priority { - return Priority.fromInt(getPriorityViaJNI(qos)) - } - - companion object { - private external fun getDefaultQoSViaJNI(): Byte - } - - private external fun getPriorityViaJNI(_qos: Byte): Int - private external fun getCongestionControlViaJNI(_qos: Byte): Int - private external fun getExpressViaJNI(_qos:Byte): Boolean -} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt index a40bd6654..f881fbc1e 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIQuery.kt @@ -14,8 +14,12 @@ package io.zenoh.jni +import io.zenoh.keyexpr.KeyExpr +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes import io.zenoh.sample.Sample import io.zenoh.value.Value +import org.apache.commons.net.ntp.TimeStamp /** * Adapter class for interacting with a Query using JNI. @@ -32,20 +36,38 @@ internal class JNIQuery(private val ptr: Long) { ptr, sample.keyExpr.jniKeyExpr?.ptr ?: 0, sample.keyExpr.keyExpr, - sample.value.payload, - sample.value.encoding.knownEncoding.ordinal, - sample.kind.ordinal, + sample.value.payload.bytes, + sample.value.encoding.id.ordinal, + sample.value.encoding.schema, timestampEnabled, if (timestampEnabled) sample.timestamp!!.ntpValue() else 0, - sample.qos.jniQoS.qos, - sample.attachment?.let { encodeAttachment(it) }, + sample.attachment?.bytes, + sample.qos.express, + sample.qos.priority.value, + sample.qos.congestionControl.value ) } fun replyError(errorValue: Value): Result = runCatching { - replyErrorViaJNI(ptr, errorValue.payload, errorValue.encoding.knownEncoding.ordinal) + replyErrorViaJNI(ptr, errorValue.payload.bytes, errorValue.encoding.id.ordinal, errorValue.encoding.schema) } + fun replyDelete(keyExpr: KeyExpr, timestamp: TimeStamp?, attachment: ZBytes?, qos: QoS): Result = + runCatching { + val timestampEnabled = timestamp != null + replyDeleteViaJNI( + ptr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + timestampEnabled, + if (timestampEnabled) timestamp!!.ntpValue() else 0, + attachment?.bytes, + qos.express, + qos.priority.value, + qos.congestionControl.value + ) + } + fun close() { freePtrViaJNI(ptr) } @@ -56,12 +78,14 @@ internal class JNIQuery(private val ptr: Long) { keyExprPtr: Long, keyExprString: String, valuePayload: ByteArray, - valueEncoding: Int, - sampleKind: Int, + valueEncodingId: Int, + valueEncodingSchema: String?, timestampEnabled: Boolean, timestampNtp64: Long, - qos: Byte, attachment: ByteArray?, + qosExpress: Boolean, + qosPriority: Int, + qosCongestionControl: Int, ) @Throws(Exception::class) @@ -69,6 +93,20 @@ internal class JNIQuery(private val ptr: Long) { queryPtr: Long, errorValuePayload: ByteArray, errorValueEncoding: Int, + encodingSchema: String?, + ) + + @Throws(Exception::class) + private external fun replyDeleteViaJNI( + queryPtr: Long, + keyExprPtr: Long, + keyExprString: String, + timestampEnabled: Boolean, + timestampNtp64: Long, + attachment: ByteArray?, + qosExpress: Boolean, + qosPriority: Int, + qosCongestionControl: Int, ) /** Frees the underlying native Query. */ diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt index 317283360..886b04b43 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNISession.kt @@ -15,23 +15,25 @@ package io.zenoh.jni import io.zenoh.* +import io.zenoh.prelude.Encoding +import io.zenoh.prelude.Encoding.ID import io.zenoh.exceptions.SessionException import io.zenoh.handlers.Callback import io.zenoh.jni.callbacks.JNIOnCloseCallback -import io.zenoh.prelude.KnownEncoding import io.zenoh.jni.callbacks.JNIGetCallback import io.zenoh.jni.callbacks.JNIQueryableCallback import io.zenoh.jni.callbacks.JNISubscriberCallback import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.Encoding -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.QoS +import io.zenoh.prelude.* +import io.zenoh.protocol.ZBytes +import io.zenoh.protocol.ZenohID +import io.zenoh.protocol.into +import io.zenoh.publication.Delete import io.zenoh.publication.Publisher import io.zenoh.publication.Put import io.zenoh.query.* import io.zenoh.queryable.Query import io.zenoh.queryable.Queryable -import io.zenoh.sample.Attachment import io.zenoh.sample.Sample import io.zenoh.selector.Selector import io.zenoh.subscriber.Reliability @@ -44,51 +46,62 @@ import java.util.concurrent.atomic.AtomicLong /** Adapter class to handle the communication with the Zenoh JNI code for a [Session]. */ internal class JNISession { + companion object { + init { + ZenohLoad + } + } + /* Pointer to the underlying Rust zenoh session. */ private var sessionPtr: AtomicLong = AtomicLong(0) - fun open(config: Config): Result = runCatching { - config.jsonConfig?.let { jsonConfig -> - sessionPtr.set(openSessionWithJsonConfigViaJNI(jsonConfig.toString())) - } ?: run { - sessionPtr.set(openSessionViaJNI(config.path?.toString().orEmpty())) + fun open(config: Config?): Result = runCatching { + val session = when { + config == null -> openSessionViaJNI(null) + config.config != null -> { + when (config.format) { + Config.Format.YAML -> openSessionWithYamlConfigViaJNI(config.config) + else -> openSessionWithJsonConfigViaJNI(config.config) + } + } + else -> openSessionViaJNI(config.path?.toString()) } + sessionPtr.set(session) } fun close(): Result = runCatching { closeSessionViaJNI(sessionPtr.get()) } - fun declarePublisher(builder: Publisher.Builder): Result = runCatching { + fun declarePublisher(keyExpr: KeyExpr, qos: QoS): Result = runCatching { val publisherRawPtr = declarePublisherViaJNI( - builder.keyExpr.jniKeyExpr?.ptr ?: 0, - builder.keyExpr.keyExpr, + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, sessionPtr.get(), - builder.congestionControl.value, - builder.priority.value, + qos.congestionControl.value, + qos.priority.value, + qos.express ) Publisher( - builder.keyExpr, + keyExpr, + qos, JNIPublisher(publisherRawPtr), - builder.congestionControl, - builder.priority, ) } fun declareSubscriber( - keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R?, reliability: Reliability + keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R, reliability: Reliability ): Result> = runCatching { val subCallback = - JNISubscriberCallback { keyExpr, payload, encoding, kind, timestampNTP64, timestampIsValid, qos, attachmentBytes -> + JNISubscriberCallback { keyExpr, payload, encodingId, encodingSchema, kind, timestampNTP64, timestampIsValid, attachmentBytes, express: Boolean, priority: Int, congestionControl: Int -> val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val attachment = attachmentBytes.takeIf { it.isNotEmpty() }?.let { decodeAttachment(it) } val sample = Sample( KeyExpr(keyExpr, null), - Value(payload, Encoding(KnownEncoding.fromInt(encoding))), + Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)), SampleKind.fromInt(kind), timestamp, - QoS(qos), - attachment + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() ) callback.run(sample) } @@ -99,20 +112,24 @@ internal class JNISession { } fun declareQueryable( - keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R?, complete: Boolean + keyExpr: KeyExpr, callback: Callback, onClose: () -> Unit, receiver: R, complete: Boolean ): Result> = runCatching { val queryCallback = - JNIQueryableCallback { keyExpr: String, selectorParams: String, withValue: Boolean, payload: ByteArray?, encoding: Int, attachmentBytes: ByteArray, queryPtr: Long -> + JNIQueryableCallback { keyExpr: String, selectorParams: String, payload: ByteArray?, encodingId: Int, encodingSchema: String?, attachmentBytes: ByteArray?, queryPtr: Long -> val jniQuery = JNIQuery(queryPtr) val keyExpr2 = KeyExpr(keyExpr, null) - val selector = Selector(keyExpr2, selectorParams) - val value: Value? = if (withValue) Value(payload!!, Encoding(KnownEncoding.fromInt(encoding))) else null - val decodedAttachment = attachmentBytes.takeIf { it.isNotEmpty() }?.let { decodeAttachment(it) } - val query = Query(keyExpr2, selector, value, decodedAttachment, jniQuery) + val selector = if (selectorParams.isEmpty()) { + Selector(keyExpr2) + } else { + Selector(keyExpr2, selectorParams) + } + val value = payload?.let { Value(it, Encoding(ID.fromId(encodingId)!!, encodingSchema)) } + val query = Query(keyExpr2, selector, value, attachmentBytes?.into(), jniQuery) callback.run(query) } - val queryableRawPtr = - declareQueryableViaJNI(keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr.get(), queryCallback, onClose, complete) + val queryableRawPtr = declareQueryableViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr.get(), queryCallback, onClose, complete + ) Queryable(keyExpr, receiver, JNIQueryable(queryableRawPtr)) } @@ -120,63 +137,79 @@ internal class JNISession { selector: Selector, callback: Callback, onClose: () -> Unit, - receiver: R?, + receiver: R, timeout: Duration, target: QueryTarget, consolidation: ConsolidationMode, - value: Value?, - attachment: Attachment? - ): Result = runCatching { - val getCallback = - JNIGetCallback { replierId: String, success: Boolean, keyExpr: String, payload: ByteArray, encoding: Int, kind: Int, timestampNTP64: Long, timestampIsValid: Boolean, qos: Byte, attachmentBytes: ByteArray -> - if (success) { - val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null - val decodedAttachment = attachmentBytes.takeIf { it.isNotEmpty() }?.let { decodeAttachment(it) } - val sample = Sample( - KeyExpr(keyExpr, null), - Value(payload, Encoding(KnownEncoding.fromInt(encoding))), - SampleKind.fromInt(kind), - timestamp, - QoS(qos), - decodedAttachment - ) - val reply = Reply.Success(replierId, sample) - callback.run(reply) - } else { - val reply = Reply.Error(replierId, Value(payload, Encoding(KnownEncoding.fromInt(encoding)))) - callback.run(reply) + payload: ZBytes?, + encoding: Encoding?, + attachment: ZBytes? + ): Result = runCatching { + val getCallback = JNIGetCallback { + replierId: String?, + success: Boolean, + keyExpr: String?, + payload: ByteArray, + encodingId: Int, + encodingSchema: String?, + kind: Int, + timestampNTP64: Long, + timestampIsValid: Boolean, + attachmentBytes: ByteArray?, + express: Boolean, + priority: Int, + congestionControl: Int, + -> + val reply: Reply + if (success) { + val timestamp = if (timestampIsValid) TimeStamp(timestampNTP64) else null + when (SampleKind.fromInt(kind)) { + SampleKind.PUT -> { + val sample = Sample( + KeyExpr(keyExpr!!, null), + Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)), + SampleKind.fromInt(kind), + timestamp, + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express), + attachmentBytes?.into() + ) + reply = Reply.Success(replierId?.let { ZenohID(it) }, sample) + } + + SampleKind.DELETE -> { + reply = Reply.Delete( + replierId?.let { ZenohID(it) }, + KeyExpr(keyExpr!!, null), + timestamp, + attachmentBytes?.into(), + QoS(CongestionControl.fromInt(congestionControl), Priority.fromInt(priority), express) + ) + } } + } else { + reply = Reply.Error( + replierId?.let { ZenohID(it) }, + Value(payload, Encoding(ID.fromId(encodingId)!!, encodingSchema)) + ) } - - if (value == null) { - getViaJNI( - selector.keyExpr.jniKeyExpr?.ptr ?: 0, - selector.keyExpr.keyExpr, - selector.parameters, - sessionPtr.get(), - getCallback, - onClose, - timeout.toMillis(), - target.ordinal, - consolidation.ordinal, - attachment?.let { encodeAttachment(it) } - ) - } else { - getWithValueViaJNI( - selector.keyExpr.jniKeyExpr?.ptr ?: 0, - selector.keyExpr.keyExpr, - selector.parameters, - sessionPtr.get(), - getCallback, - onClose, - timeout.toMillis(), - target.ordinal, - consolidation.ordinal, - value.payload, - value.encoding.knownEncoding.ordinal, - attachment?.let { encodeAttachment(it) } - ) + callback.run(reply) } + + getViaJNI( + selector.keyExpr.jniKeyExpr?.ptr ?: 0, + selector.keyExpr.keyExpr, + selector.parameters, + sessionPtr.get(), + getCallback, + onClose, + timeout.toMillis(), + target.ordinal, + consolidation.ordinal, + attachment?.bytes, + payload?.bytes, + encoding?.id?.ordinal ?: ID.default().id, + encoding?.schema + ) receiver } @@ -188,7 +221,7 @@ internal class JNISession { fun undeclareKeyExpr(keyExpr: KeyExpr): Result = runCatching { keyExpr.jniKeyExpr?.run { undeclareKeyExprViaJNI(sessionPtr.get(), this.ptr) - keyExpr.close() + keyExpr.jniKeyExpr = null } ?: throw SessionException("Attempting to undeclare a non declared key expression.") } @@ -201,20 +234,41 @@ internal class JNISession { keyExpr.jniKeyExpr?.ptr ?: 0, keyExpr.keyExpr, sessionPtr.get(), - put.value.payload, - put.value.encoding.knownEncoding.ordinal, - put.congestionControl.value, - put.priority.value, - put.kind.ordinal, - put.attachment?.let { encodeAttachment(it) } + put.value.payload.bytes, + put.value.encoding.id.ordinal, + put.value.encoding.schema, + put.qos.congestionControl.value, + put.qos.priority.value, + put.qos.express, + put.attachment?.bytes + ) + } + + @Throws(Exception::class) + fun performDelete( + keyExpr: KeyExpr, + delete: Delete, + ) { + deleteViaJNI( + keyExpr.jniKeyExpr?.ptr ?: 0, + keyExpr.keyExpr, + sessionPtr.get(), + delete.qos.congestionControl.value, + delete.qos.priority.value, + delete.qos.express, + delete.attachment?.bytes ) } + @Throws(Exception::class) - private external fun openSessionViaJNI(configFilePath: String): Long + private external fun openSessionViaJNI(configFilePath: String?): Long @Throws(Exception::class) private external fun openSessionWithJsonConfigViaJNI(jsonConfig: String): Long + @Throws(Exception::class) + private external fun openSessionWithYamlConfigViaJNI(yamlConfig: String): Long + @Throws(Exception::class) private external fun closeSessionViaJNI(ptr: Long) @@ -224,7 +278,8 @@ internal class JNISession { keyExprString: String, sessionPtr: Long, congestionControl: Int, - priority: Int + priority: Int, + express: Boolean ): Long @Throws(Exception::class) @@ -257,7 +312,7 @@ internal class JNISession { private external fun getViaJNI( keyExprPtr: Long, keyExprString: String, - selectorParams: String, + selectorParams: String?, sessionPtr: Long, callback: JNIGetCallback, onClose: JNIOnCloseCallback, @@ -265,34 +320,33 @@ internal class JNISession { target: Int, consolidation: Int, attachmentBytes: ByteArray?, + payload: ByteArray?, + encodingId: Int, + encodingSchema: String?, ) @Throws(Exception::class) - private external fun getWithValueViaJNI( + private external fun putViaJNI( keyExprPtr: Long, keyExprString: String, - selectorParams: String, sessionPtr: Long, - callback: JNIGetCallback, - onClose: JNIOnCloseCallback, - timeoutMs: Long, - target: Int, - consolidation: Int, - payload: ByteArray, - encoding: Int, + valuePayload: ByteArray, + valueEncoding: Int, + valueEncodingSchema: String?, + congestionControl: Int, + priority: Int, + express: Boolean, attachmentBytes: ByteArray? ) @Throws(Exception::class) - private external fun putViaJNI( + private external fun deleteViaJNI( keyExprPtr: Long, keyExprString: String, sessionPtr: Long, - valuePayload: ByteArray, - valueEncoding: Int, congestionControl: Int, priority: Int, - kind: Int, + express: Boolean, attachmentBytes: ByteArray? ) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIUtils.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIUtils.kt deleted file mode 100644 index 9c9bb5ae2..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIUtils.kt +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.jni - -import io.zenoh.sample.Attachment - -/** - * Encode attachment as a byte array. - */ -internal fun encodeAttachment(attachment: Attachment): ByteArray { - return attachment.values.map { - val key = it.first - val keyLength = key.size.toByteArray() - val value = it.second - val valueLength = value.size.toByteArray() - keyLength + key + valueLength + value - }.reduce { acc, bytes -> acc + bytes } -} - -/** - * Decode an attachment as a byte array, recreating the original [Attachment]. - */ -internal fun decodeAttachment(attachmentBytes: ByteArray): Attachment { - var idx = 0 - var sliceSize: Int - val pairs: MutableList> = mutableListOf() - while (idx < attachmentBytes.size) { - sliceSize = attachmentBytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1)).toInt() - idx += Int.SIZE_BYTES - - val key = attachmentBytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - sliceSize = attachmentBytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1)).toInt() - idx += Int.SIZE_BYTES - - val value = attachmentBytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) - idx += sliceSize - - pairs.add(key to value) - } - return Attachment(pairs) -} - -/** - * Converts an integer into a byte array with little endian format. - */ -fun Int.toByteArray(): ByteArray { - val result = ByteArray(UInt.SIZE_BYTES) - (0 until UInt.SIZE_BYTES).forEach { - result[it] = this.shr(Byte.SIZE_BITS * it).toByte() - } - return result -} - -/** - * To int. The byte array is expected to be in Little Endian format. - * - * @return The integer value. - */ -fun ByteArray.toInt(): Int = - (((this[3].toUInt() and 0xFFu) shl 24) or ((this[2].toUInt() and 0xFFu) shl 16) or ((this[1].toUInt() and 0xFFu) shl 8) or (this[0].toUInt() and 0xFFu)).toInt() diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt new file mode 100644 index 000000000..804cd07d5 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/JNIZBytes.kt @@ -0,0 +1,36 @@ +package io.zenoh.jni + +import io.zenoh.ZenohLoad +import io.zenoh.protocol.ZBytes +import io.zenoh.protocol.into + +object JNIZBytes { + + init { + ZenohLoad + } + + fun serializeIntoList(list: List): ZBytes { + return serializeIntoListViaJNI(list.map { it.bytes }).into() + } + + fun deserializeIntoList(zbytes: ZBytes): List { + return deserializeIntoListViaJNI(zbytes.bytes).map { it.into() }.toList() + } + + fun serializeIntoMap(map: Map): ZBytes { + return serializeIntoMapViaJNI(map.map { (k, v) -> k.bytes to v.bytes }.toMap()).into() + } + + fun deserializeIntoMap(bytes: ZBytes): Map { + return deserializeIntoMapViaJNI(bytes.bytes).map { (k, v) -> k.into() to v.into() }.toMap() + } + + private external fun serializeIntoMapViaJNI(map: Map): ByteArray + + private external fun serializeIntoListViaJNI(list: List): ByteArray + + private external fun deserializeIntoMapViaJNI(payload: ByteArray): Map + + private external fun deserializeIntoListViaJNI(payload: ByteArray): List +} \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt index d5b6f2acf..b78fc33f8 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIGetCallback.kt @@ -17,15 +17,18 @@ package io.zenoh.jni.callbacks internal fun interface JNIGetCallback { fun run( - replierId: String, + replierId: String?, success: Boolean, - keyExpr: String, + keyExpr: String?, payload: ByteArray, - encoding: Int, + encodingId: Int, + encodingSchema: String?, kind: Int, timestampNTP64: Long, timestampIsValid: Boolean, - qos: Byte, - attachment: ByteArray, + attachment: ByteArray?, + express: Boolean, + priority: Int, + congestionControl: Int, ) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt index e9a942f59..31f5885f8 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNIQueryableCallback.kt @@ -17,9 +17,9 @@ package io.zenoh.jni.callbacks internal fun interface JNIQueryableCallback { fun run(keyExpr: String, selectorParams: String, - withValue: Boolean, payload: ByteArray?, - encoding: Int, - attachmentBytes: ByteArray, + encodingId: Int, + encodingSchema: String?, + attachmentBytes: ByteArray?, queryPtr: Long) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt index 4005f4574..76373c72b 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/jni/callbacks/JNISubscriberCallback.kt @@ -18,11 +18,14 @@ internal fun interface JNISubscriberCallback { fun run( keyExpr: String, payload: ByteArray, - encoding: Int, + encodingId: Int, + encodingSchema: String?, kind: Int, timestampNTP64: Long, timestampIsValid: Boolean, - qos: Byte, - attachment: ByteArray, + attachment: ByteArray?, + express: Boolean, + priority: Int, + congestionControl: Int, ) } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt index 17244bdd3..e6654f088 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/keyexpr/KeyExpr.kt @@ -14,9 +14,10 @@ package io.zenoh.keyexpr -import io.zenoh.Resolvable import io.zenoh.Session +import io.zenoh.SessionDeclaration import io.zenoh.jni.JNIKeyExpr +import io.zenoh.selector.Selector /** * # Address space @@ -59,7 +60,7 @@ import io.zenoh.jni.JNIKeyExpr * @param jniKeyExpr An optional [JNIKeyExpr] instance, present when the key expression was declared through [Session.declareKeyExpr], * it represents the native instance of the key expression. */ -class KeyExpr internal constructor(internal val keyExpr: String, internal var jniKeyExpr: JNIKeyExpr? = null): AutoCloseable { +class KeyExpr internal constructor(internal val keyExpr: String, internal var jniKeyExpr: JNIKeyExpr? = null): AutoCloseable, SessionDeclaration { companion object { @@ -114,9 +115,9 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn * Undeclare the key expression if it was previously declared on the specified [session]. * * @param session The session from which the key expression was previously declared. - * @return An empty [Resolvable]. + * @return A [Result] with the operation status. */ - fun undeclare(session: Session): Resolvable { + fun undeclare(session: Session): Result { return session.undeclare(this) } @@ -127,6 +128,10 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn return jniKeyExpr != null } + fun intoSelector(): Selector { + return Selector(this) + } + override fun toString(): String { return keyExpr } @@ -139,6 +144,10 @@ class KeyExpr internal constructor(internal val keyExpr: String, internal var jn jniKeyExpr = null } + override fun undeclare() { + close() + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/CongestionControl.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/CongestionControl.kt index 26ab9fcae..64d981c13 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/CongestionControl.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/CongestionControl.kt @@ -30,5 +30,7 @@ enum class CongestionControl (val value: Int) { companion object { fun fromInt(value: Int) = entries.first { it.value == value } + + fun default() = DROP } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt index 0e1b1dc3a..4fa635b70 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Encoding.kt @@ -15,63 +15,90 @@ package io.zenoh.prelude /** - * The encoding of a [io.zenoh.value.Value]. + * Default encoding values used by Zenoh. * - * A zenoh encoding is an HTTP Mime type and a string suffix. + * An encoding has a similar role to Content-type in HTTP: it indicates, when present, how data should be interpreted by the application. * - * **Suffixes are not yet supported by zenoh-jni and are currently ignored.** + * Please note the Zenoh protocol does not impose any encoding value, nor it operates on it. + * It can be seen as some optional metadata that is carried over by Zenoh in such a way the application may perform different operations depending on the encoding value. + * + * A set of associated constants are provided to cover the most common encodings for user convenience. + * This is particularly useful in helping Zenoh to perform additional network optimizations. * - * @property knownEncoding A [KnownEncoding]. - * @property suffix Suffix of the encoding. This parameter is not yet supported by Zenoh-JNI and is currently ignored. */ -class Encoding(val knownEncoding: KnownEncoding, val suffix: String = "") { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false +data class Encoding(val id: ID, val schema: String? = null) { - other as Encoding + /** + * The ID of the encoding. + * + * @property id The id of the encoding. + * @property encoding The encoding name. + */ + enum class ID(val id: Int, val encoding: String) { + ZENOH_BYTES(0, "zenoh/bytes"), + ZENOH_INT(1, "zenoh/int"), + ZENOH_UINT(2, "zenoh/uint"), + ZENOH_FLOAT(3, "zenoh/float"), + ZENOH_BOOL(4, "zenoh/bool"), + ZENOH_STRING(5, "zenoh/string"), + ZENOH_ERROR(6, "zenoh/error"), + APPLICATION_OCTET_STREAM(7, "application/octet-stream"), + TEXT_PLAIN(8, "text/plain"), + APPLICATION_JSON(9, "application/json"), + TEXT_JSON(10, "text/json"), + APPLICATION_CDR(11, "application/cdr"), + APPLICATION_CBOR(12, "application/cbor"), + APPLICATION_YAML(13, "application/yaml"), + TEXT_YAML(14, "text/yaml"), + TEXT_JSON5(15, "text/json5"), + APPLICATION_PYTHON_SERIALIZED_OBJECT(16, "application/python-serialized-object"), + APPLICATION_PROTOBUF(17, "application/protobuf"), + APPLICATION_JAVA_SERIALIZED_OBJECT(18, "application/java-serialized-object"), + APPLICATION_OPENMETRICS_TEXT(19, "application/openmetrics-text"), + IMAGE_PNG(20, "image/png"), + IMAGE_JPEG(21, "image/jpeg"), + IMAGE_GIF(22, "image/gif"), + IMAGE_BMP(23, "image/bmp"), + IMAGE_WEBP(24, "image/webp"), + APPLICATION_XML(25, "application/xml"), + APPLICATION_X_WWW_FORM_URLENCODED(26, "application/x-www-form-urlencoded"), + TEXT_HTML(27, "text/html"), + TEXT_XML(28, "text/xml"), + TEXT_CSS(29, "text/css"), + TEXT_JAVASCRIPT(30, "text/javascript"), + TEXT_MARKDOWN(31, "text/markdown"), + TEXT_CSV(32, "text/csv"), + APPLICATION_SQL(33, "application/sql"), + APPLICATION_COAP_PAYLOAD(34, "application/coap-payload"), + APPLICATION_JSON_PATCH_JSON(35, "application/json-patch+json"), + APPLICATION_JSON_SEQ(36, "application/json-seq"), + APPLICATION_JSONPATH(37, "application/jsonpath"), + APPLICATION_JWT(38, "application/jwt"), + APPLICATION_MP4(39, "application/mp4"), + APPLICATION_SOAP_XML(40, "application/soap+xml"), + APPLICATION_YANG(41, "application/yang"), + AUDIO_AAC(42, "audio/aac"), + AUDIO_FLAC(43, "audio/flac"), + AUDIO_MP4(44, "audio/mp4"), + AUDIO_OGG(45, "audio/ogg"), + AUDIO_VORBIS(46, "audio/vorbis"), + VIDEO_H261(47, "video/h261"), + VIDEO_H263(48, "video/h263"), + VIDEO_H264(49, "video/h264"), + VIDEO_H265(50, "video/h265"), + VIDEO_H266(51, "video/h266"), + VIDEO_MP4(52, "video/mp4"), + VIDEO_OGG(53, "video/ogg"), + VIDEO_RAW(54, "video/raw"), + VIDEO_VP8(55, "video/vp8"), + VIDEO_VP9(56, "video/vp9"); - if (knownEncoding != other.knownEncoding) return false - return suffix == other.suffix - } - - override fun hashCode(): Int { - var result = knownEncoding.hashCode() - result = 31 * result + suffix.hashCode() - return result + companion object { + private val idToEnum = entries.associateBy(ID::id) + internal fun fromId(id: Int): ID? = idToEnum[id] + internal fun default() = ZENOH_BYTES + } } } -/** - * Known encoding. An HTTP Mime type. - */ -enum class KnownEncoding { - EMPTY, - APP_OCTET_STREAM, - APP_CUSTOM, - TEXT_PLAIN, - APP_PROPERTIES, - APP_JSON, - APP_SQL, - APP_INTEGER, - APP_FLOAT, - APP_XML, - APP_XHTML_XML, - APP_X_WWW_FORM_URLENCODED, - TEXT_JSON, - TEXT_HTML, - TEXT_XML, - TEXT_CSS, - TEXT_CSV, - TEXT_JAVASCRIPT, - IMAGE_JPEG, - IMAGE_PNG, - IMAGE_GIF; - - companion object { - fun fromInt(value: Int) = entries.first { it.ordinal == value } - fun default() = EMPTY - } -} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Priority.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Priority.kt index 820c871f8..6907565dc 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Priority.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/Priority.kt @@ -33,6 +33,8 @@ enum class Priority(val value: Int) { companion object { fun fromInt(value: Int) = entries.first { it.value == value } + + fun default() = DATA } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt index ae0de0297..c8f0420ea 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/prelude/QoS.kt @@ -13,38 +13,23 @@ // package io.zenoh.prelude -import io.zenoh.jni.JNIQoS; -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority /** * Quality of service settings used to send zenoh message. + * + * @property congestionControl [CongestionControl] policy used for the message. + * @property priority [Priority] policy used for the message. + * @property express If true, the message is not batched in order to reduce the latency. */ -class QoS internal constructor(internal val jniQoS: JNIQoS) { - internal constructor(qos: Byte): this(JNIQoS(qos)) - - /** - * Returns priority of the message. - */ - fun priority(): Priority = jniQoS.getPriority() - - /** - * Returns congestion control setting of the message. - */ - fun congestionControl(): CongestionControl = jniQoS.getCongestionControl() - - /** - * Returns express flag. If it is true, the message is not batched to reduce the latency. - */ - fun express(): Boolean = jniQoS.getExpress() +data class QoS ( + val congestionControl: CongestionControl = CongestionControl.DROP, + val priority: Priority = Priority.DATA, + val express: Boolean = false +) { companion object { - - /** - * Returns default QoS settings. - */ - fun default(): QoS { - return QoS(JNIQoS()) - } + private val defaultQoS = QoS() + + fun default() = defaultQoS } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt new file mode 100644 index 000000000..9ad142452 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Deserializable.kt @@ -0,0 +1,26 @@ +package io.zenoh.protocol + +/** + * Deserializable interface. + * + * Classes implementing these two nested interfaces can be deserialized into a ZBytes object. + * + * The class must be declared as [Deserializable], but it's also necessary to make the companion + * object of the class implement the [Deserializable.From], as shown in the example below: + * + * ```kotlin + * class Foo(val content: String) : Deserializable { + * + * companion object: Deserializable.From { + * override fun from(zbytes: ZBytes): Foo { + * return Foo(zbytes.toString()) + * } + * } + * } + * ``` + */ +interface Deserializable { + interface From { + fun from(zbytes: ZBytes): Serializable + } +} \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt new file mode 100644 index 000000000..0c6bd8182 --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/Serializable.kt @@ -0,0 +1,18 @@ +package io.zenoh.protocol + +/** + * Serializable interface. + * + * Classes implementing this interface can be serialized into a ZBytes object. + * + * Example: + * ```kotlin + * class Foo(val content: String) : Serializable { + * + * override fun into(): ZBytes = content.into() + * } + * ``` + */ +interface Serializable { + fun into(): ZBytes +} \ No newline at end of file diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt new file mode 100644 index 000000000..1cb2dc75e --- /dev/null +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZBytes.kt @@ -0,0 +1,552 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh.protocol + +import io.zenoh.jni.JNIZBytes +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.reflect.KClass +import kotlin.reflect.KFunction1 +import kotlin.reflect.KType +import kotlin.reflect.full.* +import kotlin.reflect.jvm.jvmErasure +import kotlin.reflect.typeOf + +/** + * The ZBytes class (Zenoh bytes) represents the bytes received through the Zenoh network. + * + * It provides many utilities to serialize an object into a ZBytes, as well as to deserialize from a ZBytes instance. + * + * # Serialization + * + * Supported types: + * + * ## Raw types + * + * * Numeric: Byte, Short, Int, Long, Float and Double.** + * * String + * * ByteArray + * + * For the raw types, there are basically three ways to serialize them into a ZBytes, for instance let's suppose + * we want to serialize an `Int`, we could achieve it by:: + * * using the `into()` syntax: + * ```kotlin + * val exampleInt: Int = 256 + * val zbytes: ZBytes = exampleInt.into() + * ``` + * + * * using the `from()` syntax: + * ```kotlin + * val exampleInt: Int = 256 + * val zbytes: ZBytes = ZBytes.from(exampleInt) + * ``` + * + * * using the serialize syntax: + * ```kotlin + * val exampleInt: Int = 256 + * val zbytes: ZBytes = ZBytes.serialize(exampleInt).getOrThrow() + * ``` + * This approach works as well for the other mentioned types. + * + * ## Lists + * + * Lists are supported, but they must be either: + * - List of [Number] (Byte, Short, Int, Long, Float or Double) + * - List of [String] + * - List of [ByteArray] + * - List of [Serializable] + * + * The serialize syntax must be used: + * ```kotlin + * val myList = listOf(1, 2, 5, 8, 13, 21) + * val zbytes = ZBytes.serialize>(myList).getOrThrow() + * ``` + * + * ## Maps + * + * Maps are supported as well, with the restriction that their inner types must be either: + * - [Number] + * - [String] + * - [ByteArray] + * - [Serializable] + * + * ```kotlin + * val myMap: Map = mapOf("foo" to 1, "bar" to 2) + * val zbytes = ZBytes.serialize>(myMap).getOrThrow() + * ``` + * + * # Deserialization + * + * ## Raw types + * + * * Numeric: Byte, Short, Int, Long, Float and Double + * * String + * * ByteArray + * + * Example: + * + * For these raw types, you can use the functions `to`, that is + * - [toByte] + * - [toShort] + * - [toInt] + * - [toLong] + * - [toDouble] + * - [toString] + * - [toByteArray] + * + * For instance, for an Int: + * ```kotlin + * val example: Int = 256 + * val zbytes: ZBytes = exampleInt.into() + * val deserializedInt = zbytes.toInt() + * ``` + * + * Alternatively, the deserialize syntax can be used as well: + * ```kotlin + * val exampleInt: Int = 256 + * val zbytes: ZBytes = exampleInt.into() + * val deserializedInt = zbytes.deserialize().getOrThrow() + * ``` + * + * ## Lists + * + * Lists are supported, but they must be deserialized either into a: + * - List of [Number] (Byte, Short, Int, Long, Float or Double) + * - List of [String] + * - List of [ByteArray] + * - List of [Deserializable] + * + * To deserialize into a list, we need to use the deserialize syntax as follows: + * ```kotlin + * val inputList = listOf("sample1", "sample2", "sample3") + * payload = ZBytes.serialize(inputList).getOrThrow() + * val outputList = payload.deserialize>().getOrThrow() + * ``` + * + * ## Maps + * + * Maps are supported as well, with the restriction that their inner types must be either: + * - [Number] + * - [String] + * - [ByteArray] + * - [Deserializable] + * + * ```kotlin + * val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") + * payload = ZBytes.serialize(inputMap).getOrThrow() + * val outputMap = payload.deserialize>().getOrThrow() + * check(inputMap == outputMap) + * ``` + * + * # Custom serialization and deserialization + * + * ## Serialization + * + * For custom serialization, classes to be serialized need to implement the [Serializable] interface. + * For instance: + * + * ```kotlin + * class Foo(val content: String) : Serializable { + * + * /*Inherits: Serializable*/ + * override fun into(): ZBytes = content.into() + * } + * ``` + * + * This way, we can do: + * ```kotlin + * val foo = Foo("bar") + * val serialization = ZBytes.serialize(foo).getOrThrow() + * ``` + * + * Implementing the [Serializable] interface on a class enables the possibility of serializing lists and maps + * of that type, for instance: + * ```kotlin + * val list = listOf(Foo("bar"), Foo("buz"), Foo("fizz")) + * val zbytes = ZBytes.serialize>(list) + * ``` + * + * ## Deserialization + * + * For custom deserialization, classes to be serialized need to implement the [Deserializable] interface, and + * their companion object need to implement the [Deserializable.From] interface, for instance, let's make the + * `Foo` class (defined in the previous section) implement these interfaces: + * + * ```kotlin + * class Foo(val content: String) : Serializable, Deserializable { + * + * /*Inherits: Serializable*/ + * override fun into(): ZBytes = content.into() + * + * companion object: Deserializable.From { + * override fun from(zbytes: ZBytes): Foo { + * return Foo(zbytes.toString()) + * } + * } + * } + * ``` + * + * With this implementation, then the deserialization works as follows with the deserialization syntax: + * ```kotlin + * val foo = Foo("bar") + * val zbytes = ZBytes.serialize(foo).getOrThrow() + * val deserialization = zbytes.deserialize().getOrThrow() + * ``` + * + * Analogous to the serialization, we can deserialize into lists and maps of the type implementing + * the [Deserializable] interface: + * + * ```kotlin + * val list = listOf(Foo("bar"), Foo("buz"), Foo("fizz")) + * val zbytes = ZBytes.serialize>(list) + * val deserializedList = zbytes.deserialize>().getOrThrow() + * ``` + * + * ### Deserialization functions: + * + * The [deserialize] function admits an argument which by default is an emptyMap, consisting + * of a `Map>` map. This allows to specify types in a map, associating + * functions for deserialization for each of the types in the map. + * + * For instance, let's stick to the previous implementation of our example Foo class, when it + * only implemented the [Serializable] class: + * ```kotlin + * class Foo(val content: String) : Serializable { + * + * /*Inherits: Serializable*/ + * override fun into(): ZBytes = content.into() + * } + * ``` + * + * Instead of making it implement the [Deserializable] interface as explained previously, + * we could provide directly the deserialization function as follows: + * + * ```kotlin + * fun deserializeFoo(zbytes: ZBytes): Foo { + * return Foo(zbytes.toString()) + * } + * + * val foo = Foo("bar") + * val zbytes = ZBytes.serialize(foo) + * val deserialization = zbytes.deserialize(mapOf(typeOf() to ::deserializeFoo)).getOrThrow() + * ``` + */ +class ZBytes internal constructor(internal val bytes: ByteArray) : Serializable { + + companion object { + fun from(serializable: Serializable) = serializable.into() + fun from(string: String) = ZBytes(string.toByteArray()) + fun from(byteArray: ByteArray) = ZBytes(byteArray) + fun from(number: Number): ZBytes { + val byteArray = when (number) { + is Byte -> byteArrayOf(number) + is Short -> ByteBuffer.allocate(Short.SIZE_BYTES).apply { + order(ByteOrder.LITTLE_ENDIAN) + putShort(number) + }.array() + + is Int -> ByteBuffer.allocate(Int.SIZE_BYTES).apply { + order(ByteOrder.LITTLE_ENDIAN) + putInt(number) + }.array() + + is Long -> ByteBuffer.allocate(Long.SIZE_BYTES).apply { + order(ByteOrder.LITTLE_ENDIAN) + putLong(number) + }.array() + + is Float -> ByteBuffer.allocate(Float.SIZE_BYTES).apply { + order(ByteOrder.LITTLE_ENDIAN) + putFloat(number) + }.array() + + is Double -> ByteBuffer.allocate(Double.SIZE_BYTES).apply { + order(ByteOrder.LITTLE_ENDIAN) + putDouble(number) + }.array() + + else -> throw IllegalArgumentException("Unsupported number type") + } + return ZBytes(byteArray) + } + + /** + * Serialize an element of type [T] into a [ZBytes]. + * + * Supported types: + * - [Number]: Byte, Short, Int, Long, Float, Double + * - [String] + * - [ByteArray] + * - [Serializable] + * - Lists and Maps of the above-mentioned types. + * + * @see ZBytes + * @return a [Result] with the serialized [ZBytes]. + */ + inline fun serialize(t: T): Result = runCatching { + return serialize(t, T::class) + } + + fun serialize(t: T, clazz: KClass): Result = runCatching { + val type: KType = when (clazz) { + List::class -> typeOf>() + Map::class -> typeOf>() + else -> clazz.createType() + } + when { + typeOf>().isSupertypeOf(type) -> { + val list = t as List<*> + val zbytesList = list.map { it.into() } + return Result.success(JNIZBytes.serializeIntoList(zbytesList)) + } + + typeOf>().isSupertypeOf(type) -> { + val map = t as Map<*, *> + val zbytesMap = map.map { (k, v) -> k.into() to v.into() }.toMap() + return Result.success(JNIZBytes.serializeIntoMap(zbytesMap)) + } + + typeOf().isSupertypeOf(type) -> { + return Result.success((t as Any).into()) + } + + else -> throw IllegalArgumentException("Unsupported type '$type' for serialization.") + } + } + } + + /** + * Deserialize the [ZBytes] instance into an element of type [T]. + * + * Supported types: + * - [Number]: Byte, Short, Int, Long, Float, Double + * - [String] + * - [ByteArray] + * - [Deserializable] + * - Lists and Maps of the above-mentioned types. + * + * + * A map of types and functions for deserialization can also be provided. + * + * For instance: + * ```kotlin + * fun deserializeFoo(zbytes: ZBytes): Foo { + * return Foo(zbytes.toString()) + * } + * + * val foo = Foo("bar") + * val zbytes = ZBytes.serialize(foo) + * val deserialization = zbytes.deserialize(mapOf(typeOf() to ::deserializeFoo)).getOrThrow() + * ``` + * + * In case the provided type isn't associated with any of the functions provided in the [deserializers] map + * (if provided), the deserialization will carry on with the default behavior. + * + * @see ZBytes + * @see Deserializable + * @return a [Result] with the deserialization. + */ + inline fun deserialize( + deserializers: Map> = emptyMap() + ): Result { + val type = typeOf() + val deserializer = deserializers[type] + if (deserializer != null) { + return Result.success(deserializer(this) as T) + } + when { + typeOf>().isSupertypeOf(type) -> { + val itemsClass = type.arguments.firstOrNull()?.type?.jvmErasure + return deserialize(T::class, arg1clazz = itemsClass) + } + typeOf>().isSupertypeOf(type) -> { + val keyClass = type.arguments.getOrNull(0)?.type?.jvmErasure + val valueClass = type.arguments.getOrNull(1)?.type?.jvmErasure + return deserialize(T::class, arg1clazz = keyClass, arg2clazz = valueClass) + } + typeOf().isSupertypeOf(type) -> { + return deserialize(T::class) + } + } + throw IllegalArgumentException("Unsupported type for deserialization: '$type'.") + } + + /** + * Deserialize the [ZBytes] into an element of class [clazz]. + * + * It's generally preferable to use the [ZBytes.deserialize] function with reification, however + * this function is exposed for cases when reification needs to be avoided. + * + * Example: + * ```kotlin + * val list = listOf("value1", "value2", "value3") + * val zbytes = ZBytes.serialize(list).getOrThrow() + * val deserializedList = zbytes.deserialize(clazz = List::class, arg1clazz = String::class).getOrThrow() + * check(list == deserializedList) + * ``` + * + * Supported types: + * - [Number]: Byte, Short, Int, Long, Float, Double + * - [String] + * - [ByteArray] + * - [Deserializable] + * - Lists and Maps of the above-mentioned types. + * + * @see [ZBytes.deserialize] + * + * + * @param clazz: the [KClass] of the type to be serialized. + * @param arg1clazz Optional first nested parameter of the provided clazz, for instance when trying to deserialize + * into a `List`, arg1clazz should be set to `String::class`, when trying to deserialize into a + * `Map`, arg1clazz should be set to `Int::class`. Can be null if providing a basic type. + * @param arg2clazz Optional second nested parameter of the provided clazz, to be used for the cases of maps. + * For instance, when trying to deserialize into a `Map`, arg2clazz should be set to `String::class`. + * Can be null if providing a basic type. + */ + @Suppress("UNCHECKED_CAST") + fun deserialize( + clazz: KClass, + arg1clazz: KClass<*>? = null, + arg2clazz: KClass<*>? = null, + ): Result { + val type: KType = when (clazz) { + List::class -> typeOf>() + Map::class -> typeOf>() + else -> clazz.createType() + } + return when { + typeOf>().isSupertypeOf(type) -> { + val typeElement = arg1clazz?.createType() + if (typeElement != null) { + Result.success(JNIZBytes.deserializeIntoList(this).map { it.intoAny(typeElement) } as T) + } else { + Result.failure(IllegalArgumentException("Unsupported list type for deserialization: $type")) + } + } + + typeOf>().isSupertypeOf(type) -> { + val keyType = arg1clazz?.createType() + val valueType = arg2clazz?.createType() + if (keyType != null && valueType != null) { + Result.success(JNIZBytes.deserializeIntoMap(this) + .map { (k, v) -> k.intoAny(keyType) to v.intoAny(valueType) }.toMap() as T + ) + } else { + Result.failure(IllegalArgumentException("Unsupported map type for deserialization: $type")) + } + } + + typeOf().isSupertypeOf(type) -> { + Result.success(this.intoAny(type) as T) + } + + else -> Result.failure(IllegalArgumentException("Unsupported type for deserialization: $type")) + } + } + + + fun toByteArray() = bytes + + fun toByte(): Byte { + return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).get() + } + + fun toShort(): Short { + return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).short + } + + fun toInt(): Int { + return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).int + } + + fun toLong(): Long { + return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).long + } + + fun toFloat(): Float { + return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).float + } + + fun toDouble(): Double { + return ByteBuffer.wrap(this.bytes).order(ByteOrder.LITTLE_ENDIAN).double + } + + override fun toString() = bytes.decodeToString() + + override fun into(): ZBytes = this + + override fun equals(other: Any?) = other is ZBytes && bytes.contentEquals(other.bytes) + + override fun hashCode() = bytes.contentHashCode() +} + +fun Number.into(): ZBytes { + return ZBytes.from(this) +} + +fun String.into(): ZBytes { + return ZBytes.from(this) +} + +fun ByteArray.into(): ZBytes { + return ZBytes(this) +} + +@Throws(Exception::class) +internal fun Any?.into(): ZBytes { + return when (this) { + is String -> this.into() + is Number -> this.into() + is ByteArray -> this.into() + is Serializable -> this.into() + else -> throw IllegalArgumentException("Unsupported serializable type") + } +} + +@Throws(Exception::class) +internal fun ZBytes.intoAny(type: KType): Any { + return when (type) { + typeOf() -> this.toString() + typeOf() -> this.toByte() + typeOf() -> this.toShort() + typeOf() -> this.toInt() + typeOf() -> this.toLong() + typeOf() -> this.toFloat() + typeOf() -> this.toDouble() + typeOf() -> this.toByteArray() + typeOf() -> this + else -> { + when { + typeOf().isSupertypeOf(type) -> { + val companion = type.jvmErasure.companionObject + val function = companion?.declaredMemberFunctions?.find { it.name == "from" } + if (function != null) { + val result = function.call(type.jvmErasure.companionObjectInstance, this) + if (result != null) { + return result + } else { + throw Exception("The 'from' method returned null for the type '$type'.") + } + } else { + throw Exception("Implementation of 'from' method from the ${Deserializable.From::class} interface not found on element of type '$type'.") + } + } + + else -> throw IllegalArgumentException("Unsupported type '$type' for deserialization.") + } + + } + } +} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZenohID.kt similarity index 76% rename from zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt rename to zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZenohID.kt index decb9ae39..953fb226a 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/Resolvable.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/protocol/ZenohID.kt @@ -12,12 +12,9 @@ // ZettaScale Zenoh Team, // -package io.zenoh +package io.zenoh.protocol /** - * A resolvable function interface meant to be used by Zenoh builders. + * The global unique id of a Zenoh peer. */ -fun interface Resolvable { - - fun res(): Result -} +class ZenohID(val id: String) diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt index 65386ada6..5406429e4 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Delete.kt @@ -14,90 +14,17 @@ package io.zenoh.publication -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority -import io.zenoh.Session -import io.zenoh.value.Value import io.zenoh.keyexpr.KeyExpr +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes /** - * Delete operation to perform on Zenoh on a key expression. + * Delete operation. * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "demo/kotlin/example".intoKeyExpr().onSuccess { keyExpr -> - * session.delete(keyExpr) - * .res() - * .onSuccess { - * println("Performed a delete on $keyExpr") - * } - * } - * } - * } - * ``` - * - * A delete operation is a special case of a Put operation, it is analogous to perform a Put with an empty value and - * specifying the sample kind to be `DELETE`. + * @property keyExpr The [KeyExpr] for the delete operation. + * @property qos The [QoS] configuration. + * @property attachment Optional attachment. */ -class Delete private constructor( - keyExpr: KeyExpr, - value: Value, - congestionControl: CongestionControl, - priority: Priority, - kind: SampleKind, -) : Put(keyExpr, value, congestionControl, priority, kind, null) { - - companion object { - /** - * Creates a new [Builder] associated with the specified [session] and [keyExpr]. - * - * @param session The [Session] from which the Delete will be performed. - * @param keyExpr The [KeyExpr] upon which the Delete will be performed. - * @return A [Delete] operation [Builder]. - */ - fun newBuilder(session: Session, keyExpr: KeyExpr): Builder { - return Builder(session, keyExpr) - } - } - - /** - * Builder to construct a [Delete] operation. - * - * @property session The [Session] from which the Delete will be performed - * @property keyExpr The [KeyExpr] from which the Delete will be performed - * @property congestionControl The [CongestionControl] to be applied when routing the data, - * defaults to [CongestionControl.DROP] - * @property priority The [Priority] of zenoh messages, defaults to [Priority.DATA]. - * @constructor Create a [Delete] builder. - */ - class Builder internal constructor( - val session: Session, - val keyExpr: KeyExpr, - private var congestionControl: CongestionControl = CongestionControl.DROP, - private var priority: Priority = Priority.DATA, - ) { - - /** Change the [CongestionControl] to apply when routing the data. */ - fun congestionControl(congestionControl: CongestionControl) = - apply { this.congestionControl = congestionControl } - - /** Change the [Priority] of the written data. */ - fun priority(priority: Priority) = apply { this.priority = priority } - - /** - * Performs a DELETE operation on the specified [keyExpr]. - * - * A successful [Result] only states the Delete request was properly sent through the network, it doesn't mean it - * was properly executed remotely. - */ - fun res(): Result = runCatching { - val delete = Delete( - this.keyExpr, Value.empty(), this.congestionControl, this.priority, SampleKind.DELETE - ) - session.resolveDelete(keyExpr, delete) - } - } -} +internal class Delete ( + val keyExpr: KeyExpr, val qos: QoS, val attachment: ZBytes? +) diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt index 791233c68..024230338 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Publisher.kt @@ -18,110 +18,78 @@ import io.zenoh.* import io.zenoh.exceptions.SessionException import io.zenoh.jni.JNIPublisher import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.SampleKind import io.zenoh.prelude.Priority import io.zenoh.prelude.CongestionControl -import io.zenoh.sample.Attachment +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes import io.zenoh.value.Value /** + * # Publisher + * * A Zenoh Publisher. * * A publisher is automatically dropped when using it with the 'try-with-resources' statement (i.e. 'use' in Kotlin). * The session from which it was declared will also keep a reference to it and undeclare it once the session is closed. * - * In order to declare a publisher, [Session.declarePublisher] must be called, which returns a [Publisher.Builder] from - * which we can specify the [Priority], and the [CongestionControl]. - * - * Example: - * ``` + * Example of a publisher declaration: + * ```kotlin * val keyExpr = "demo/kotlin/greeting" - * Session.open().onSuccess { + * Session.open(Config.default()).onSuccess { * it.use { session -> * session * .declarePublisher(keyExpr) - * .priority(Priority.REALTIME) - * .congestionControl(CongestionControl.DROP) - * .res() * .onSuccess { pub -> - * pub.use { - * var i = 0 - * while (true) { - * pub.put("Hello for the ${i}th time!").res() - * Thread.sleep(1000) - * i++ - * } + * var i = 0 + * while (true) { + * pub.put("Hello for the ${i}th time!") + * Thread.sleep(1000) + * i++ * } * } * } * } * ``` * - * The publisher configuration parameters can be later changed using the setter functions. + * ## Lifespan + * + * Internally, the [Session] from which the [Publisher] was declared keeps a reference to it, therefore keeping it alive + * until the session is closed. For the cases where we want to stop the publisher earlier, it's necessary + * to keep a reference to it in order to undeclare it later. * * @property keyExpr The key expression the publisher will be associated to. + * @property qos [QoS] configuration of the publisher. * @property jniPublisher Delegate class handling the communication with the native code. - * @property congestionControl The congestion control policy. - * @property priority The priority policy. * @constructor Create empty Publisher with the default configuration. + * @see Session.declarePublisher */ class Publisher internal constructor( val keyExpr: KeyExpr, + val qos: QoS, private var jniPublisher: JNIPublisher?, - private var congestionControl: CongestionControl, - private var priority: Priority ) : SessionDeclaration, AutoCloseable { companion object { private val InvalidPublisherResult = Result.failure(SessionException("Publisher is not valid.")) } + val congestionControl = qos.congestionControl + val priority = qos.priority + val express = qos.express + /** Performs a PUT operation on the specified [keyExpr] with the specified [value]. */ - fun put(value: Value) = Put(jniPublisher, value) + fun put(value: Value, attachment: ZBytes? = null) = jniPublisher?.put(value, attachment) ?: InvalidPublisherResult + /** Performs a PUT operation on the specified [keyExpr] with the specified string [value]. */ - fun put(value: String) = Put(jniPublisher, Value(value)) + fun put(value: String, attachment: ZBytes? = null) = jniPublisher?.put(Value(value), attachment) ?: InvalidPublisherResult /** * Performs a DELETE operation on the specified [keyExpr] - * - * @return A [Resolvable] operation. */ - fun delete() = Delete(jniPublisher) - - /** Get congestion control policy. */ - fun getCongestionControl(): CongestionControl { - return congestionControl - } + fun delete(attachment: ZBytes? = null) = jniPublisher?.delete(attachment) ?: InvalidPublisherResult - /** - * Set the congestion control policy of the publisher. - * - * This function is not thread safe. - * - * @param congestionControl: The [CongestionControl] policy. - */ - fun setCongestionControl(congestionControl: CongestionControl) { - jniPublisher?.setCongestionControl(congestionControl)?.onSuccess { this.congestionControl = congestionControl } - } - - /** Get priority policy. */ - fun getPriority(): Priority { - return priority - } - - /** - * Set the priority policy of the publisher. - * - * This function is not thread safe. - * - * @param priority: The [Priority] policy. - */ - fun setPriority(priority: Priority) { - jniPublisher?.setPriority(priority)?.onSuccess { this.priority = priority } - } - - override fun isValid(): Boolean { + fun isValid(): Boolean { return jniPublisher != null } @@ -133,62 +101,4 @@ class Publisher internal constructor( jniPublisher?.close() jniPublisher = null } - - protected fun finalize() { - jniPublisher?.close() - } - - class Put internal constructor( - private var jniPublisher: JNIPublisher?, - val value: Value, - var attachment: Attachment? = null - ) : Resolvable { - - fun withAttachment(attachment: Attachment) = apply { this.attachment = attachment } - - override fun res(): Result = run { - jniPublisher?.put(value, attachment) ?: InvalidPublisherResult - } - } - - class Delete internal constructor( - private var jniPublisher: JNIPublisher?, - var attachment: Attachment? = null - ) : Resolvable { - - fun withAttachment(attachment: Attachment) = apply { this.attachment = attachment } - - override fun res(): Result = run { - jniPublisher?.delete(attachment) ?: InvalidPublisherResult - } - } - - /** - * Publisher Builder. - * - * @property session The [Session] from which the publisher is declared. - * @property keyExpr The key expression the publisher will be associated to. - * @property congestionControl The congestion control policy, defaults to [CongestionControl.DROP]. - * @property priority The priority policy, defaults to [Priority.DATA] - * @constructor Create empty Builder. - */ - class Builder internal constructor( - val session: Session, - val keyExpr: KeyExpr, - var congestionControl: CongestionControl = CongestionControl.DROP, - var priority: Priority = Priority.DATA, - ) { - - /** Change the `congestion_control` to apply when routing the data. */ - fun congestionControl(congestionControl: CongestionControl) = - apply { this.congestionControl = congestionControl } - - /** Change the priority of the written data. */ - fun priority(priority: Priority) = apply { this.priority = priority } - - fun res(): Result { - return session.run { resolvePublisher(this@Builder) } - } - } } - diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt index a54c9f5bd..93c7340a7 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/publication/Put.kt @@ -14,113 +14,22 @@ package io.zenoh.publication -import io.zenoh.Resolvable -import io.zenoh.Session import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.Encoding -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.CongestionControl -import io.zenoh.prelude.Priority -import io.zenoh.sample.Attachment +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes import io.zenoh.value.Value /** * Put operation. * - * A put puts a [io.zenoh.sample.Sample] into the specified key expression. - * - * Example: - * ```kotlin - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> - * session.put(keyExpr, "Hello") - * .congestionControl(CongestionControl.BLOCK) - * .priority(Priority.REALTIME) - * .kind(SampleKind.PUT) - * .res() - * .onSuccess { println("Put 'Hello' on $keyExpr.") } - * }} - * } - * ``` - * - * This class is an open class for the sake of the [Delete] operation, which is a special case of [Put] operation. - * * @property keyExpr The [KeyExpr] to which the put operation will be performed. * @property value The [Value] to put. - * @property congestionControl The [CongestionControl] to be applied when routing the data. - * @property priority The [Priority] of zenoh messages. - * @property kind The [SampleKind] of the sample (put or delete). - * @property attachment An optional user [Attachment]. + * @property qos The [QoS] configuration. + * @property attachment An optional user attachment. */ -open class Put protected constructor( +internal data class Put ( val keyExpr: KeyExpr, val value: Value, - val congestionControl: CongestionControl, - val priority: Priority, - val kind: SampleKind, - val attachment: Attachment? -) { - - companion object { - - /** - * Creates a bew [Builder] associated to the specified [session] and [keyExpr]. - * - * @param session The [Session] from which the put will be performed. - * @param keyExpr The [KeyExpr] upon which the put will be performed. - * @param value The [Value] to put. - * @return A [Put] operation [Builder]. - */ - fun newBuilder(session: Session, keyExpr: KeyExpr, value: Value): Builder { - return Builder(session, keyExpr, value) - } - } - - /** - * Builder to construct a [Put] operation. - * - * @property session The [Session] from which the put operation will be performed. - * @property keyExpr The [KeyExpr] upon which the put operation will be performed. - * @property value The [Value] to put. - * @property congestionControl The [CongestionControl] to be applied when routing the data, - * defaults to [CongestionControl.DROP] - * @property priority The [Priority] of zenoh messages, defaults to [Priority.DATA]. - * @property kind The [SampleKind] of the sample (put or delete), defaults to [SampleKind.PUT]. - * @property attachment Optional user [Attachment]. - * @constructor Create a [Put] builder. - */ - class Builder internal constructor( - private val session: Session, - private val keyExpr: KeyExpr, - private var value: Value, - private var congestionControl: CongestionControl = CongestionControl.DROP, - private var priority: Priority = Priority.DATA, - private var kind: SampleKind = SampleKind.PUT, - private var attachment: Attachment? = null - ): Resolvable { - - /** Change the [Encoding] of the written data. */ - fun encoding(encoding: Encoding) = apply { - this.value = Value(value.payload, encoding) - } - - /** Change the [CongestionControl] to apply when routing the data. */ - fun congestionControl(congestionControl: CongestionControl) = - apply { this.congestionControl = congestionControl } - - /** Change the [Priority] of the written data. */ - fun priority(priority: Priority) = apply { this.priority = priority } - - /** Change the [SampleKind] of the sample. If set to [SampleKind.DELETE], performs a delete operation. */ - fun kind(kind: SampleKind) = apply { this.kind = kind } - - /** Set an attachment to the put operation. */ - fun withAttachment(attachment: Attachment) = apply { this.attachment = attachment } - - /** Resolves the put operation, returning a [Result]. */ - override fun res(): Result = runCatching { - val put = Put(keyExpr, value, congestionControl, priority, kind, attachment) - session.run { resolvePut(keyExpr, put) } - } - } -} + val qos: QoS, + val attachment: ZBytes? +) diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/ConsolidationMode.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/ConsolidationMode.kt index e430358a9..a95fa0d33 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/ConsolidationMode.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/ConsolidationMode.kt @@ -16,6 +16,9 @@ package io.zenoh.query /** The kind of consolidation. */ enum class ConsolidationMode { + /** Apply automatic consolidation based on queryable's preferences. */ + AUTO, + /** No consolidation applied: multiple samples may be received for the same key-timestamp.*/ NONE, @@ -32,4 +35,8 @@ enum class ConsolidationMode { /** Holds back samples to only send the set of samples that had the highest timestamp for their key. */ LATEST; + + companion object { + fun default() = AUTO + } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt deleted file mode 100644 index b0316ac77..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Get.kt +++ /dev/null @@ -1,196 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.query - -import io.zenoh.handlers.Callback -import io.zenoh.Session -import io.zenoh.handlers.ChannelHandler -import io.zenoh.handlers.Handler -import io.zenoh.sample.Attachment -import io.zenoh.selector.Selector -import io.zenoh.value.Value -import kotlinx.coroutines.channels.Channel -import java.time.Duration - -/** - * Get to query data from the matching queryables in the system. - * - * Example with a [Callback]: - * ``` - * println("Opening Session") - * Session.open().onSuccess { session -> session.use { - * "demo/kotlin/example".intoSelector().onSuccess { selector -> - * session.get(selector) - * .consolidation(ConsolidationMode.NONE) - * .target(QueryTarget.BEST_MATCHING) - * .withValue("Get value example") - * .with { reply -> println("Received reply $reply") } - * .timeout(Duration.ofMillis(1000)) - * .res() - * .onSuccess {...} - * } - * } - * } - * ``` - * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. - */ -class Get private constructor() { - - companion object { - /** - * Creates a bew [Builder] associated to the specified [session] and [keyExpr]. - * - * @param session The [Session] from which the query will be triggered. - * @param selector The [Selector] with which the query will be performed. - * @return A [Builder] with a default [ChannelHandler] to handle any incoming [Reply]. - */ - fun newBuilder(session: Session, selector: Selector): Builder> { - return Builder(session, selector, handler = ChannelHandler(Channel())) - } - } - - /** - * Builder to construct a [Get]. - * - * Either a [Handler] or a [Callback] must be specified. Note neither of them are stackable and are mutually exclusive, - * meaning that it is not possible to specify multiple callbacks and/or handlers, the builder only considers the - * last one specified. - * - * @param R The receiver type of the [Handler] implementation, defaults to [Unit] when no handler is specified. - * @property session The [Session] from which the query will be performed. - * @property selector The [Selector] with which the get query will be performed. - * @constructor Creates a Builder. This constructor is internal and should not be called directly. Instead, this - * builder should be obtained through the [Session] after calling [Session.get]. - */ - class Builder internal constructor( - private val session: Session, - private val selector: Selector, - private var callback: Callback? = null, - private var handler: Handler? = null, - ) { - - private var timeout = Duration.ofMillis(10000) - private var target: QueryTarget = QueryTarget.BEST_MATCHING - private var consolidation: ConsolidationMode = ConsolidationMode.NONE - private var value: Value? = null - private var attachment: Attachment? = null - private var onClose: (() -> Unit)? = null - - private constructor(other: Builder<*>, handler: Handler?) : this(other.session, other.selector) { - this.handler = handler - copyParams(other) - } - - private constructor(other: Builder<*>, callback: Callback?) : this(other.session, other.selector) { - this.callback = callback - copyParams(other) - } - - private fun copyParams(other: Builder<*>) { - this.timeout = other.timeout - this.target = other.target - this.consolidation = other.consolidation - this.value = other.value - this.attachment = other.attachment - this.onClose = other.onClose - } - - /** Specify the [QueryTarget]. */ - fun target(target: QueryTarget): Builder { - this.target = target - return this - } - - /** Specify the [ConsolidationMode]. */ - fun consolidation(consolidation: ConsolidationMode): Builder { - this.consolidation = consolidation - return this - } - - /** Specify the timeout. */ - fun timeout(timeout: Duration): Builder { - this.timeout = timeout - return this - } - - /** - * Specify a string value. A [Value] is generated with the provided message, therefore - * this method is equivalent to calling `withValue(Value(message))`. - */ - fun withValue(message: String): Builder { - this.value = Value(message) - return this - } - - /** Specify a [Value]. */ - fun withValue(value: Value): Builder { - this.value = value - return this - } - - /** Specify an [Attachment]. */ - fun withAttachment(attachment: Attachment): Builder { - this.attachment = attachment - return this - } - - /** - * Specify an action to be invoked when the Get operation is over. - * - * Zenoh will trigger ths specified action once no more replies are to be expected. - */ - fun onClose(action: () -> Unit): Builder { - this.onClose = action - return this - } - - /** Specify a [Callback]. Overrides any previously specified callback or handler. */ - fun with(callback: Callback): Builder = Builder(this, callback) - - /** Specify a [Handler]. Overrides any previously specified callback or handler. */ - fun with(handler: Handler): Builder = Builder(this, handler) - - /** Specify a [Channel]. Overrides any previously specified callback or handler. */ - fun with(channel: Channel): Builder> = Builder(this, ChannelHandler(channel)) - - /** - * Resolve the builder triggering the query. - * - * @return A [Result] with the receiver [R] from the specified [Handler] (if specified). - */ - fun res(): Result = runCatching { - require(callback != null || handler != null) { "Either a callback or a handler must be provided." } - val resolvedCallback = callback ?: Callback { t: Reply -> handler?.handle(t) } - val resolvedOnClose = fun() { - onClose?.invoke() - handler?.onClose() - } - return session.run { - resolveGet( - selector, - resolvedCallback, - resolvedOnClose, - handler?.receiver(), - timeout, - target, - consolidation, - value, - attachment - ) - } - } - } -} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt index 97c5c7a21..00c39661d 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/query/Reply.kt @@ -14,79 +14,51 @@ package io.zenoh.query -import io.zenoh.Resolvable import io.zenoh.ZenohType import io.zenoh.sample.Sample -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.QoS import io.zenoh.value.Value import io.zenoh.keyexpr.KeyExpr -import io.zenoh.sample.Attachment +import io.zenoh.prelude.QoS +import io.zenoh.protocol.ZBytes +import io.zenoh.protocol.ZenohID import io.zenoh.queryable.Query import org.apache.commons.net.ntp.TimeStamp /** - * Class to represent a Zenoh Reply to a [Get] operation and to a remote [Query]. - * - * A reply can be either successful ([Success]) or an error ([Error]), both having different information. For instance, - * the successful reply will contain a [Sample] while the error reply will only contain a [Value] with the error information. + * Class to represent a Zenoh Reply to a get query and to a remote [Query]. * - * Replies can either be automatically created when receiving a remote reply after performing a [Get] (in which case the - * [replierId] shows the id of the replier) or created through the builders while answering to a remote [Query] (in that - * case the replier ID is automatically added by Zenoh). - * - * Generating a reply only makes sense within the context of a [Query], therefore builders below are meant to only - * be accessible from [Query.reply]. + * A reply can be either successful ([Success]), an error ([Error]) or a delete request ([Delete]), both having different + * information. + * For instance, the successful reply will contain a [Sample] while the error reply will only contain a [Value] + * with the error information. * * Example: * ```kotlin - * session.declareQueryable(keyExpr).with { query -> - * query.reply(keyExpr) - * .success(Value("Hello")) - * .withTimeStamp(TimeStamp(Date.from(Instant.now()))) - * .res() - * }.res() - * ... + * Session.open(config).onSuccess { session -> + * session.use { + * key.intoKeyExpr().onSuccess { keyExpr -> + * session.declareQueryable(keyExpr, Channel()).onSuccess { queryable -> + * runBlocking { + * for (query in queryable.receiver) { + * val valueInfo = query.value?.let { value -> " with value '$value'" } ?: "" + * println(">> [Queryable] Received Query '${query.selector}' $valueInfo") + * query.replySuccess( + * keyExpr, + * value = Value("Example value"), + * timestamp = TimeStamp.getCurrentTime() + * ).getOrThrow() + * } + * } + * } + * } + * } + * } * ``` * - * **IMPORTANT: Error replies are not yet fully supported by Zenoh, but the code for the error replies below has been - * added for the sake of future compatibility.** - * - * @property replierId: unique ID identifying the replier. + * @property replierId: unique ID identifying the replier, may be null in case the network cannot provide it + * (@see https://github.com/eclipse-zenoh/zenoh/issues/709#issuecomment-2202763630). */ -abstract class Reply private constructor(val replierId: String) : ZenohType { - - /** - * Builder to construct a [Reply]. - * - * This builder allows you to construct [Success] replies. **Error replies are not yet enabled since they are not yet - * supported on Zenoh.** - * - * @property query The received [Query] to reply to. - * @property keyExpr The [KeyExpr] from the queryable, which is at least an intersection of the query's key expression. - * @constructor Create empty Builder - */ - class Builder internal constructor(val query: Query, val keyExpr: KeyExpr) { - - /** - * Returns a [Success.Builder] with the provided [value]. - * - * @param value The [Value] of the reply. - */ - fun success(value: Value) = Success.Builder(query, keyExpr, value) - - /** - * Returns a [Success.Builder] with a [Value] containing the provided [message]. - * - * It is equivalent to calling `success(Value(message))`. - * - * @param message A string message for the reply. - */ - fun success(message: String) = success(Value(message)) - -// TODO: uncomment line below when Zenoh enables Error replies. -// fun error(value: Value) = Error.Builder(query, value) - } +sealed class Reply private constructor(open val replierId: ZenohID?) : ZenohType { /** * A successful [Reply]. @@ -97,100 +69,45 @@ abstract class Reply private constructor(val replierId: String) : ZenohType { * * @param replierId The replierId of the remotely generated reply. */ - class Success internal constructor(replierId: String, val sample: Sample) : Reply(replierId) { - - /** - * Builder for the [Success] reply. - * - * @property query The [Query] to reply to. - * @property keyExpr The [KeyExpr] of the queryable. - * @property value The [Value] with the reply information. - */ - class Builder internal constructor(val query: Query, val keyExpr: KeyExpr, val value: Value): Resolvable { - - private var kind = SampleKind.PUT - private var timeStamp: TimeStamp? = null - private var attachment: Attachment? = null - - /** - * Sets the [SampleKind] of the replied [Sample]. - */ - fun withKind(kind: SampleKind) = apply { this.kind = kind } - - /** - * Sets the [TimeStamp] of the replied [Sample]. - */ - fun withTimeStamp(timeStamp: TimeStamp) = apply { this.timeStamp = timeStamp } - - /** - * Appends an [Attachment] to the reply. - */ - fun withAttachment(attachment: Attachment) = apply { this.attachment = attachment } - - /** - * Constructs the reply sample with the provided parameters and triggers the reply to the query. - */ - override fun res(): Result { - val sample = Sample(keyExpr, value, kind, timeStamp, QoS.default(), attachment) - return query.reply(Success("", sample)).res() - } - } + data class Success internal constructor(override val replierId: ZenohID? = null, val sample: Sample) : Reply(replierId) { override fun toString(): String { return "Success(sample=$sample)" } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Success) return false - - return sample == other.sample - } - - override fun hashCode(): Int { - return sample.hashCode() - } } /** * An Error reply. * - * @property error: value with the error information. - * @constructor The constructor is private since reply instances are created through JNI when receiving a reply to a query. - * + * @property error: value with the error information.* * @param replierId: unique ID identifying the replier. */ - class Error internal constructor(replierId: String, val error: Value) : Reply(replierId) { - - /** - * Builder for the [Error] reply. - * - * @property query The [Query] to reply to. - * @property value The [Value] with the reply information. - */ - class Builder internal constructor(val query: Query, val value: Value): Resolvable { - - /** - * Triggers the error reply. - */ - override fun res(): Result { - return query.reply(Error("", value)).res() - } - } + data class Error internal constructor(override val replierId: ZenohID? = null, val error: Value) : Reply(replierId) { override fun toString(): String { return "Error(error=$error)" } + } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Error) return false - - return error == other.error - } + /** + * A Delete reply. + * + * @property replierId Unique ID identifying the replier. + * @property keyExpr Key expression to reply to. This parameter must not be necessarily the same + * as the key expression from the Query, however it must intersect with the query key. + * @property attachment Optional attachment for the delete reply. + * @property qos QoS for the reply. + */ + data class Delete internal constructor( + override val replierId: ZenohID? = null, + val keyExpr: KeyExpr, + val timestamp: TimeStamp?, + val attachment: ZBytes?, + val qos: QoS + ) : Reply(replierId) { - override fun hashCode(): Int { - return error.hashCode() + override fun toString(): String { + return "Delete(keyexpr=$keyExpr)" } } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt index 54570d6ab..28af57d7a 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Query.kt @@ -14,15 +14,18 @@ package io.zenoh.queryable -import io.zenoh.Resolvable import io.zenoh.ZenohType import io.zenoh.selector.Selector -import io.zenoh.value.Value import io.zenoh.exceptions.SessionException import io.zenoh.jni.JNIQuery import io.zenoh.keyexpr.KeyExpr -import io.zenoh.query.Reply -import io.zenoh.sample.Attachment +import io.zenoh.prelude.Encoding +import io.zenoh.prelude.QoS +import io.zenoh.prelude.SampleKind +import io.zenoh.protocol.ZBytes +import io.zenoh.sample.Sample +import io.zenoh.value.Value +import org.apache.commons.net.ntp.TimeStamp /** * Represents a Zenoh Query in Kotlin. @@ -41,47 +44,92 @@ class Query internal constructor( val keyExpr: KeyExpr, val selector: Selector, val value: Value?, - val attachment: Attachment?, + val attachment: ZBytes?, private var jniQuery: JNIQuery? ) : AutoCloseable, ZenohType { /** Shortcut to the [selector]'s parameters. */ val parameters = selector.parameters + /** Payload of the query. */ + val payload: ZBytes? = value?.payload + + /** Encoding of the payload. */ + val encoding: Encoding? = value?.encoding + /** - * Reply to the specified key expression. + * Reply success to the remote [Query]. + * + * A query can not be replied more than once. After the reply is performed, the query is considered + * to be no more valid and further attempts to reply to it will fail. * * @param keyExpr Key expression to reply to. This parameter must not be necessarily the same * as the key expression from the Query, however it must intersect with the query key. - * @return a [Reply.Builder] + * @param value The [Value] with the reply information. + * @param qos The [QoS] for the reply. + * @param timestamp Optional timestamp for the reply. + * @param attachment Optional attachment for the reply. */ - fun reply(keyExpr: KeyExpr) = Reply.Builder(this, keyExpr) - - override fun close() { - jniQuery?.apply { - this.close() + fun replySuccess( + keyExpr: KeyExpr, + value: Value, + qos: QoS = QoS.default(), + timestamp: TimeStamp? = null, + attachment: ZBytes? = null + ): Result { + val sample = Sample(keyExpr, value, SampleKind.PUT, timestamp, qos, attachment) + return jniQuery?.let { + val result = it.replySuccess(sample) jniQuery = null - } + result + } ?: Result.failure(SessionException("Query is invalid")) } - protected fun finalize() { - close() + /** + * Reply error to the remote [Query]. + * + * A query can not be replied more than once. After the reply is performed, the query is considered + * to be no more valid and further attempts to reply to it will fail. + * + * @param error [Value] with the error information. + */ + fun replyError(error: Value): Result { + return jniQuery?.let { + val result = it.replyError(error) + jniQuery = null + result + } ?: Result.failure(SessionException("Query is invalid")) } /** - * Perform a reply operation to the remote [Query]. + * Perform a delete reply operation to the remote [Query]. + * + * A query can not be replied more than once. After the reply is performed, the query is considered + * to be no more valid and further attempts to reply to it will fail. * - * @param reply The [Reply] to the Query. - * @return A [Resolvable] that returns a [Result] with the status of the reply operation. + * @param keyExpr Key expression to reply to. This parameter must not be necessarily the same + * as the key expression from the Query, however it must intersect with the query key. + * @param qos The [QoS] for the reply. + * @param timestamp Optional timestamp for the reply. + * @param attachment Optional attachment for the reply. */ - internal fun reply(reply: Reply): Resolvable = Resolvable { + fun replyDelete( + keyExpr: KeyExpr, + qos: QoS = QoS.default(), + timestamp: TimeStamp? = null, + attachment: ZBytes? = null + ): Result { + return jniQuery?.let { + val result = it.replyDelete(keyExpr, timestamp, attachment, qos) + jniQuery = null + result + } ?: Result.failure(SessionException("Query is invalid")) + } + + override fun close() { jniQuery?.apply { - reply as Reply.Success // Since error replies are not yet supported, we assume a reply is a Success reply. - val result = replySuccess(reply.sample) this.close() jniQuery = null - return@Resolvable result } - return@Resolvable Result.failure(SessionException("Query is invalid")) } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt index ef48f43ff..1e43fce9e 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/queryable/Queryable.kt @@ -15,24 +15,24 @@ package io.zenoh.queryable import io.zenoh.* -import io.zenoh.handlers.Callback -import io.zenoh.handlers.ChannelHandler import io.zenoh.handlers.Handler import io.zenoh.jni.JNIQueryable import io.zenoh.keyexpr.KeyExpr import kotlinx.coroutines.channels.Channel /** + * # Queryable + * * A queryable that allows to perform multiple queries on the specified [KeyExpr]. * * Its main purpose is to keep the queryable active as long as it exists. * * Example using the default [Channel] handler: * ```kotlin - * Session.open().onSuccess { session -> session.use { + * Session.open(Config.default()).onSuccess { session -> session.use { * "demo/kotlin/greeting".intoKeyExpr().onSuccess { keyExpr -> * println("Declaring Queryable") - * session.declareQueryable(keyExpr).res().onSuccess { queryable -> + * session.declareQueryable(keyExpr).wait().onSuccess { queryable -> * queryable.use { * it.receiver?.let { receiverChannel -> * runBlocking { @@ -44,7 +44,7 @@ import kotlinx.coroutines.channels.Channel * .success("Hello!") * .withKind(SampleKind.PUT) * .withTimeStamp(TimeStamp.getCurrentTime()) - * .res() + * .wait() * .onSuccess { println("Replied hello.") } * .onFailure { println(it) } * } @@ -56,19 +56,23 @@ import kotlinx.coroutines.channels.Channel * } * }} * ``` + * ## Lifespan + * + * Internally, the [Session] from which the [Queryable] was declared keeps a reference to it, therefore keeping it alive + * until the session is closed. For the cases where we want to stop the queryable earlier, it's necessary + * to keep a reference to it in order to undeclare it later. * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, [R] will be [Unit]. + * @param R Receiver type of the [Handler] implementation. * @property keyExpr The [KeyExpr] to which the subscriber is associated. * @property receiver Optional [R] that is provided when specifying a [Handler] for the subscriber. * @property jniQueryable Delegate object in charge of communicating with the underlying native code. - * @constructor Internal constructor. Instances of Queryable must be created through the [Builder] obtained after - * calling [Session.declareQueryable] or alternatively through [newBuilder]. + * @see Session.declareQueryable */ class Queryable internal constructor( - val keyExpr: KeyExpr, val receiver: R?, private var jniQueryable: JNIQueryable? + val keyExpr: KeyExpr, val receiver: R, private var jniQueryable: JNIQueryable? ) : AutoCloseable, SessionDeclaration { - override fun isValid(): Boolean { + fun isValid(): Boolean { return jniQueryable != null } @@ -80,93 +84,4 @@ class Queryable internal constructor( override fun close() { undeclare() } - - protected fun finalize() { - jniQueryable?.close() - } - - companion object { - - /** - * Creates a new [Builder] associated to the specified [session] and [keyExpr]. - * - * @param session The [Session] from which the queryable will be declared. - * @param keyExpr The [KeyExpr] associated to the queryable. - * @return An empty [Builder] with a default [ChannelHandler] to handle the incoming samples. - */ - fun newBuilder(session: Session, keyExpr: KeyExpr): Builder> { - return Builder(session, keyExpr, handler = ChannelHandler(Channel())) - } - } - - /** - * Builder to construct a [Queryable]. - * - * Either a [Handler] or a [Callback] must be specified. Note neither of them are stackable and are mutually exclusive, - * meaning that it is not possible to specify multiple callbacks and/or handlers, the builder only considers the - * last one specified. - * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. - * @property session [Session] to which the [Queryable] will be bound to. - * @property keyExpr The [KeyExpr] to which the queryable is associated. - * @property callback Optional callback that will be triggered upon receiving a [Query]. - * @property handler Optional handler to receive the incoming queries. - * @constructor Creates a Builder. This constructor is internal and should not be called directly. Instead, this - * builder should be obtained through the [Session] after calling [Session.declareQueryable]. - */ - class Builder internal constructor( - private val session: Session, - private val keyExpr: KeyExpr, - private var callback: Callback? = null, - private var handler: Handler? = null - ): Resolvable> { - private var complete: Boolean = false - private var onClose: (() -> Unit)? = null - - private constructor(other: Builder<*>, handler: Handler?) : this(other.session, other.keyExpr) { - this.handler = handler - this.complete = other.complete - this.onClose = other.onClose - } - - private constructor(other: Builder<*>, callback: Callback?) : this(other.session, other.keyExpr) { - this.callback = callback - this.complete = other.complete - this.onClose = other.onClose - } - - /** Change queryable completeness. */ - fun complete(complete: Boolean) = apply { this.complete = complete } - - /** Specify an action to be invoked when the [Queryable] is undeclared. */ - fun onClose(action: () -> Unit): Builder { - this.onClose = action - return this - } - - /** Specify a [Callback]. Overrides any previously specified callback or handler. */ - fun with(callback: Callback): Builder = Builder(this, callback) - - /** Specify a [Handler]. Overrides any previously specified callback or handler. */ - fun with(handler: Handler): Builder = Builder(this, handler) - - /** Specify a [Channel]. Overrides any previously specified callback or handler. */ - fun with(channel: Channel): Builder> = Builder(this, ChannelHandler(channel)) - - /** - * Resolve the builder, creating a [Queryable] with the provided parameters. - * - * @return A [Result] with the newly created [Queryable]. - */ - override fun res(): Result> = runCatching { - require(callback != null || handler != null) { "Either a callback or a handler must be provided." } - val resolvedCallback = callback ?: Callback { t: Query -> handler?.handle(t) } - val resolvedOnClose = fun() { - handler?.onClose() - onClose?.invoke() - } - return session.run { resolveQueryable(keyExpr, resolvedCallback, resolvedOnClose, handler?.receiver(), complete) } - } - } } - diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Attachment.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Attachment.kt deleted file mode 100644 index e0e115f58..000000000 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Attachment.kt +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) 2023 ZettaScale Technology -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License 2.0 which is available at -// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// -// Contributors: -// ZettaScale Zenoh Team, -// - -package io.zenoh.sample - -/** - * Attachment - * - * An attachment consists of a list of non-unique ordered key value pairs, where keys are UTF-8 Strings and the values are bytes. - * Inserting at the same key multiple times leads to both values being transmitted for that key. - * - * Attachments can be added to a message sent through Zenoh while performing puts, queries and replies. - * - * Using attachments will result in performance loss. - * - * @property values - * @constructor Create empty Attachment - */ -class Attachment internal constructor(val values: List>) { - - class Builder { - - private val values: MutableList> = mutableListOf() - - fun add(key: ByteArray, value: ByteArray) = apply { - values.add(key to value) - } - - fun add(key: String, value: ByteArray) = apply { - values.add(key.toByteArray() to value) - } - - fun add(key: String, value: String) = apply { - values.add(key.toByteArray() to value.toByteArray()) - } - - fun addAll(elements: Collection>) = apply { - values.addAll(elements) - } - - fun addAll(elements: Iterable>) = apply { - values.addAll(elements) - } - - fun res(): Attachment { - return Attachment(values) - } - } -} diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt index 1eec3a22b..a69c098e3 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/sample/Sample.kt @@ -18,6 +18,7 @@ import io.zenoh.ZenohType import io.zenoh.prelude.SampleKind import io.zenoh.prelude.QoS import io.zenoh.keyexpr.KeyExpr +import io.zenoh.protocol.ZBytes import io.zenoh.value.Value import org.apache.commons.net.ntp.TimeStamp @@ -32,37 +33,13 @@ import org.apache.commons.net.ntp.TimeStamp * @property kind The [SampleKind] of the sample. * @property timestamp Optional [TimeStamp]. * @property qos The Quality of Service settings used to deliver the sample. - * @property attachment Optional [Attachment]. + * @property attachment Optional attachment. */ -class Sample( +data class Sample( val keyExpr: KeyExpr, val value: Value, val kind: SampleKind, val timestamp: TimeStamp?, val qos: QoS, - val attachment: Attachment? = null -): ZenohType { - override fun toString(): String { - return if (kind == SampleKind.DELETE) "$kind($keyExpr)" else "$kind($keyExpr: $value)" - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Sample - - if (keyExpr != other.keyExpr) return false - if (value != other.value) return false - if (kind != other.kind) return false - return timestamp == other.timestamp - } - - override fun hashCode(): Int { - var result = keyExpr.hashCode() - result = 31 * result + value.hashCode() - result = 31 * result + kind.hashCode() - result = 31 * result + (timestamp?.hashCode() ?: 0) - return result - } -} + val attachment: ZBytes? = null +): ZenohType diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt index ff637cb73..78d71b11a 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/selector/Selector.kt @@ -28,23 +28,28 @@ import java.net.URLDecoder * @property keyExpr The [KeyExpr] of the selector. * @property parameters The parameters of the selector. */ -class Selector(val keyExpr: KeyExpr, val parameters: String = "") : AutoCloseable { +class Selector(val keyExpr: KeyExpr, val parameters: String? = null) : AutoCloseable { - /** Extracts the selector [parameters]' name-value pairs into a map, returning an error in case of duplicated parameters. */ - fun parametersStringMap(): Result> = runCatching { - parameters.split('&').fold(mapOf()) { parametersMap, parameter -> - val keyValuePair = parameter.split('=') - val key = keyValuePair[0] - if (parametersMap.containsKey(key)) { - throw IllegalArgumentException("Duplicated parameter `$key` detected.") - } - val value = keyValuePair.getOrNull(1)?.let { URLDecoder.decode(it, Charsets.UTF_8.name()) } ?: "" - parametersMap + (key to value) + /** + * If the [parameters] argument is defined, this function extracts its name-value pairs into a map, + * returning an error in case of duplicated parameters. + */ + fun parametersStringMap(): Result>? { + return parameters?.let { + it.split('&').fold>(mapOf()) { parametersMap, parameter -> + val keyValuePair = parameter.split('=') + val key = keyValuePair[0] + if (parametersMap.containsKey(key)) { + throw IllegalArgumentException("Duplicated parameter `$key` detected.") + } + val value = keyValuePair.getOrNull(1)?.let { URLDecoder.decode(it, Charsets.UTF_8.name()) } ?: "" + parametersMap + (key to value) + }.let { map -> Result.success(map) } } } override fun toString(): String { - return if (parameters.isEmpty()) "$keyExpr" else "$keyExpr?$parameters" + return parameters?.let { "$keyExpr?$parameters" } ?: keyExpr.toString() } /** Closes the selector's [KeyExpr]. */ diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt index bc47f36b7..d675758eb 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Reliability.kt @@ -32,7 +32,7 @@ enum class Reliability { * * Informs the network that this subscriber wishes for all publications to reliably reach it. * - * Note that if a publisher puts a sample with the [io.zenoh.publication.CongestionControl.DROP] option, + * Note that if a publisher puts a sample with the [io.zenoh.prelude.CongestionControl.DROP] option, * this reliability requirement may be infringed to prevent slow readers from blocking the network. */ RELIABLE, diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt index 65d5858bd..74da746fb 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/subscriber/Subscriber.kt @@ -15,59 +15,41 @@ package io.zenoh.subscriber import io.zenoh.* -import io.zenoh.handlers.Callback -import io.zenoh.handlers.ChannelHandler import io.zenoh.handlers.Handler -import io.zenoh.subscriber.Subscriber.Builder import io.zenoh.jni.JNISubscriber import io.zenoh.keyexpr.KeyExpr -import io.zenoh.sample.Sample -import kotlinx.coroutines.channels.Channel /** - * A subscriber that allows listening to updates on a key expression and reacting to changes. - * - * Its main purpose is to keep the subscription active as long as it exists. + * # Subscriber * - * Example using the default [Channel] handler: + * A subscriber that allows listening to updates on a key expression and reacting to changes. * + * Simple example using a callback to handle the received samples: * ```kotlin - * Session.open().onSuccess { session -> - * session.use { - * "demo/kotlin/sub".intoKeyExpr().onSuccess { keyExpr -> - * session.declareSubscriber(keyExpr) - * .bestEffort() - * .res() - * .onSuccess { subscriber -> - * subscriber.use { - * println("Declared subscriber on $keyExpr.") - * runBlocking { - * val receiver = subscriber.receiver!! - * val iterator = receiver.iterator() - * while (iterator.hasNext()) { - * val sample = iterator.next() - * println(sample) - * } - * } - * } - * } - * } - * } - * } + * val session = Session.open(Config.default()).getOrThrow() + * val keyexpr = "a/b/c".intoKeyExpr().getOrThrow() + * session.declareSubscriber(keyexpr, callback = { sample -> + * println(">> [Subscriber] Received $sample") + * }) * ``` * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. + * ## Lifespan + * + * Internally, the [Session] from which the [Subscriber] was declared keeps a reference to it, therefore keeping it alive + * until the session is closed. For the cases where we want to stop the subscriber earlier, it's necessary + * to keep a reference to it in order to undeclare it later. + * + * @param R Receiver type of the [Handler] implementation. * @property keyExpr The [KeyExpr] to which the subscriber is associated. * @property receiver Optional [R] that is provided when specifying a [Handler] for the subscriber. * @property jniSubscriber Delegate object in charge of communicating with the underlying native code. - * @constructor Internal constructor. Instances of Subscriber must be created through the [Builder] obtained after - * calling [Session.declareSubscriber] or alternatively through [newBuilder]. + * @see Session.declareSubscriber */ class Subscriber internal constructor( - val keyExpr: KeyExpr, val receiver: R?, private var jniSubscriber: JNISubscriber? + val keyExpr: KeyExpr, val receiver: R, private var jniSubscriber: JNISubscriber? ) : AutoCloseable, SessionDeclaration { - override fun isValid(): Boolean { + fun isValid(): Boolean { return jniSubscriber != null } @@ -79,106 +61,4 @@ class Subscriber internal constructor( override fun close() { undeclare() } - - protected fun finalize() { - jniSubscriber?.close() - } - - companion object { - - /** - * Creates a new [Builder] associated to the specified [session] and [keyExpr]. - * - * @param session The [Session] from which the subscriber will be declared. - * @param keyExpr The [KeyExpr] associated to the subscriber. - * @return An empty [Builder] with a default [ChannelHandler] to handle the incoming samples. - */ - fun newBuilder(session: Session, keyExpr: KeyExpr): Builder> { - return Builder(session, keyExpr, handler = ChannelHandler(Channel())) - } - } - - /** - * Builder to construct a [Subscriber]. - * - * Either a [Handler] or a [Callback] must be specified. Note neither of them are stackable and are mutually exclusive, - * meaning that it is not possible to specify multiple callbacks and/or handlers, the builder only considers the - * last one specified. - * - * @param R Receiver type of the [Handler] implementation. If no handler is provided to the builder, R will be [Unit]. - * @property session [Session] to which the [Subscriber] will be bound to. - * @property keyExpr The [KeyExpr] to which the subscriber is associated. - * @constructor Creates a Builder. This constructor is internal and should not be called directly. Instead, this - * builder should be obtained through the [Session] after calling [Session.declareSubscriber]. - */ - class Builder internal constructor( - private val session: Session, - private val keyExpr: KeyExpr, - private var callback: Callback? = null, - private var handler: Handler? = null - ): Resolvable> { - - private var reliability: Reliability = Reliability.BEST_EFFORT - private var onClose: (() -> Unit)? = null - - private constructor(other: Builder<*>, handler: Handler?): this(other.session, other.keyExpr) { - this.handler = handler - copyParams(other) - } - - private constructor(other: Builder<*>, callback: Callback?) : this(other.session, other.keyExpr) { - this.callback = callback - copyParams(other) - } - - private fun copyParams(other: Builder<*>) { - this.reliability = other.reliability - this.onClose = other.onClose - } - - /** Sets the [Reliability]. */ - fun reliability(reliability: Reliability): Builder = apply { - this.reliability = reliability - } - - /** Sets the reliability to [Reliability.RELIABLE]. */ - fun reliable(): Builder = apply { - this.reliability = Reliability.RELIABLE - } - - /** Sets the reliability to [Reliability.BEST_EFFORT]. */ - fun bestEffort(): Builder = apply { - this.reliability = Reliability.BEST_EFFORT - } - - /** Specify an action to be invoked when the [Subscriber] is undeclared. */ - fun onClose(action: () -> Unit): Builder { - this.onClose = action - return this - } - - /** Specify a [Callback]. Overrides any previously specified callback or handler. */ - fun with(callback: Callback): Builder = Builder(this, callback) - - /** Specify a [Handler]. Overrides any previously specified callback or handler. */ - fun with(handler: Handler): Builder = Builder(this, handler) - - /** Specify a [Channel]. Overrides any previously specified callback or handler. */ - fun with(channel: Channel): Builder> = Builder(this, ChannelHandler(channel)) - - /** - * Resolve the builder, creating a [Subscriber] with the provided parameters. - * - * @return A [Result] with the newly created [Subscriber]. - */ - override fun res(): Result> = runCatching { - require(callback != null || handler != null) { "Either a callback or a handler must be provided." } - val resolvedCallback = callback ?: Callback { t: Sample -> handler?.handle(t) } - val resolvedOnClose = fun() { - handler?.onClose() - onClose?.invoke() - } - return session.run { resolveSubscriber(keyExpr, resolvedCallback, resolvedOnClose, handler?.receiver(), reliability) } - } - } } diff --git a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt index d88c12728..0b2168dd1 100644 --- a/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt +++ b/zenoh-kotlin/src/commonMain/kotlin/io/zenoh/value/Value.kt @@ -15,7 +15,9 @@ package io.zenoh.value import io.zenoh.prelude.Encoding -import io.zenoh.prelude.KnownEncoding +import io.zenoh.protocol.Serializable +import io.zenoh.protocol.ZBytes +import io.zenoh.protocol.into /** * A Zenoh value. @@ -25,29 +27,66 @@ import io.zenoh.prelude.KnownEncoding * @property payload The payload of this Value. * @property encoding An encoding description indicating how the associated payload is encoded. */ -class Value(val payload: ByteArray, val encoding: Encoding) { +class Value(val payload: ZBytes, val encoding: Encoding) { /** - * Constructs a value with the provided message, using [KnownEncoding.TEXT_PLAIN] for encoding. + * Constructs a value with the provided message, using [Encoding.ID.TEXT_PLAIN] for encoding. */ - constructor(message: String): this(message.toByteArray(), Encoding(KnownEncoding.TEXT_PLAIN)) + constructor(message: String): this(message.toByteArray().into(), Encoding(Encoding.ID.TEXT_PLAIN)) /** * Constructs a value with the provided message and encoding. */ - constructor(message: String, encoding: Encoding): this(message.toByteArray(), encoding) + constructor(message: String, encoding: Encoding): this(message.toByteArray().into(), encoding) + + /** + * Constructs a value with the provided payload and encoding. + */ + constructor(payload: ByteArray, encoding: Encoding): this(payload.into(), encoding) + + /** + * Constructs a value with the provided payload and encoding. + */ + constructor(payload: Serializable, encoding: Encoding): this(payload.into(), encoding) + + /** + * Constructs a value with the provided message + * + * @param message The message for the value. + * @param encoding The [Encoding.ID] + * @param schema Optional [Encoding.schema] + */ + constructor(message: String, encoding: Encoding.ID, schema: String? = null): this(message.toByteArray().into(), Encoding(encoding, schema)) + + + /** + * Constructs a value with the provided [payload] + * + * @param payload The payload of the value. + * @param encoding The [Encoding.ID] + * @param schema Optional [Encoding.schema] + */ + constructor(payload: ByteArray, encoding: Encoding.ID, schema: String? = null): this(payload.into(), Encoding(encoding, schema)) + + /** + * Constructs a value with the provided [payload] + * + * @param payload The payload of the value. + * @param encoding The [Encoding.ID] + * @param schema Optional [Encoding.schema] + */ + constructor(payload: Serializable, encoding: Encoding.ID, schema: String? = null): this(payload.into(), Encoding(encoding, schema)) + companion object { /** Return an empty value. */ fun empty(): Value { - return Value(ByteArray(0), Encoding(KnownEncoding.APP_OCTET_STREAM)) + return Value(ByteArray(0), Encoding(Encoding.ID.ZENOH_BYTES)) } } - override fun toString(): String { - return payload.decodeToString() - } + override fun toString(): String = payload.toString() override fun equals(other: Any?): Boolean { if (this === other) return true @@ -55,12 +94,13 @@ class Value(val payload: ByteArray, val encoding: Encoding) { other as Value - if (!payload.contentEquals(other.payload)) return false + if (payload != other.payload) return false + return encoding == other.encoding } override fun hashCode(): Int { - var result = payload.contentHashCode() + var result = payload.bytes.hashCode() result = 31 * result + encoding.hashCode() return result } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ConfigTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ConfigTest.kt new file mode 100644 index 000000000..07c91afad --- /dev/null +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ConfigTest.kt @@ -0,0 +1,191 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// +package io.zenoh + +import io.zenoh.keyexpr.intoKeyExpr +import io.zenoh.sample.Sample +import io.zenoh.value.Value +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class ConfigTest { + companion object { + val TEST_KEY_EXP = "example/testing/keyexpr".intoKeyExpr().getOrThrow() + } + + private val json5ClientConfig = Config.from( + config = """ + { + mode: "peer", + connect: { + endpoints: ["tcp/localhost:7450"], + }, + scouting: { + multicast: { + enabled: false, + } + } + } + """.trimIndent(), format = Config.Format.JSON5 + ) + + private val json5ServerConfig = Config.from( + config = """ + { + mode: "peer", + listen: { + endpoints: ["tcp/localhost:7450"], + }, + scouting: { + multicast: { + enabled: false, + } + } + } + """.trimIndent(), format = Config.Format.JSON5 + ) + + + private val jsonClientConfig = Config.from( + config = """ + { + "mode": "peer", + "connect": { + "endpoints": ["tcp/localhost:7450"] + }, + "scouting": { + "multicast": { + "enabled": false + } + } + } + """.trimIndent(), format = Config.Format.JSON + ) + + + private val jsonServerConfig = Config.from( + config = """ + { + "mode": "peer", + "listen": { + "endpoints": ["tcp/localhost:7450"] + }, + "scouting": { + "multicast": { + "enabled": false + } + } + } + """.trimIndent(), format = Config.Format.JSON + ) + + + private val yamlClientConfig = Config.from( + config = """ + mode: peer + connect: + endpoints: + - tcp/localhost:7450 + scouting: + multicast: + enabled: false + """.trimIndent(), format = Config.Format.YAML + ) + + + private val yamlServerConfig = Config.from( + config = """ + mode: peer + listen: + endpoints: + - tcp/localhost:7450 + scouting: + multicast: + enabled: false + """.trimIndent(), format = Config.Format.YAML + ) + + + private fun runSessionTest(clientConfig: Config, serverConfig: Config) { + runBlocking { + val sessionClient = Session.open(clientConfig).getOrThrow() + val sessionServer = Session.open(serverConfig).getOrThrow() + + var receivedSample: Sample? = null + val subscriber = sessionClient.declareSubscriber(TEST_KEY_EXP, callback = { sample -> + receivedSample = sample + }).getOrThrow() + + val value = Value("example message") + sessionClient.put(TEST_KEY_EXP, value).getOrThrow() + + delay(1000) + + subscriber.close() + sessionClient.close() + sessionServer.close() + + assertNotNull(receivedSample) + assertEquals(receivedSample!!.value, value) + } + } + + @Test + fun `test config with JSON5`() = runSessionTest(json5ClientConfig, json5ServerConfig) + + @Test + fun `test config loads from JSON string`() = runSessionTest(jsonClientConfig, jsonServerConfig) + + @Test + fun `test config loads from YAML string`() = runSessionTest(yamlClientConfig, yamlServerConfig) + + @Test + fun `test config loads from JsonElement`() { + val clientConfigJson = Json.parseToJsonElement( + """ + { + "mode": "peer", + "connect": { + "endpoints": ["tcp/localhost:7450"] + }, + "scouting": { + "multicast": { + "enabled": false + } + } + } + """.trimIndent() + ) + val serverConfigJson = Json.parseToJsonElement( + """ + { + "mode": "peer", + "listen": { + "endpoints": ["tcp/localhost:7450"] + }, + "scouting": { + "multicast": { + "enabled": false + } + } + } + """.trimIndent() + ) + runSessionTest(Config.from(clientConfigJson), Config.from(serverConfigJson)) + } +} diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt index 223008c71..67f345e4d 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/DeleteTest.kt @@ -25,11 +25,11 @@ class DeleteTest { @Test fun delete_isProperlyReceivedBySubscriber() { - val session = Session.open().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() var receivedSample: Sample? = null val keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - session.delete(keyExpr).res() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + session.delete(keyExpr) subscriber.close() session.close() assertNotNull(receivedSample) diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt new file mode 100644 index 000000000..5a5deb10c --- /dev/null +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/EncodingTest.kt @@ -0,0 +1,169 @@ +package io.zenoh + +import io.zenoh.keyexpr.intoKeyExpr +import io.zenoh.prelude.Encoding +import io.zenoh.query.Reply +import io.zenoh.sample.Sample +import io.zenoh.selector.intoSelector +import io.zenoh.value.Value +import kotlin.test.* + +class EncodingTest { + + @Test + fun encoding_subscriberTest() { + val session = Session.open(Config.default()).getOrThrow() + val keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() + + // Testing non null schema + var receivedSample: Sample? = null + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> + receivedSample = sample + }).getOrThrow() + var value = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) + session.put(keyExpr, value) + Thread.sleep(200) + + assertNotNull(receivedSample) + assertEquals(Encoding.ID.TEXT_CSV, receivedSample!!.value.encoding.id) + assertEquals("test_schema", receivedSample!!.value.encoding.schema) + + // Testing null schema + receivedSample = null + value = Value("test2", Encoding(Encoding.ID.ZENOH_STRING, null)) + session.put(keyExpr, value) + Thread.sleep(200) + + assertNotNull(receivedSample) + assertEquals(Encoding.ID.ZENOH_STRING, receivedSample!!.value.encoding.id) + assertNull(receivedSample!!.value.encoding.schema) + + subscriber.close() + session.close() + } + + @Test + fun encoding_replySuccessTest() { + val session = Session.open(Config.default()).getOrThrow() + val keyExpr = "example/testing/**".intoKeyExpr().getOrThrow() + val test1 = "example/testing/reply_success".intoSelector().getOrThrow() + val test2 = "example/testing/reply_success_with_schema".intoSelector().getOrThrow() + + val testValueA = Value("test", Encoding(Encoding.ID.TEXT_CSV, null)) + val testValueB = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) + + val queryable = session.declareQueryable(keyExpr, callback = { query -> + when (query.keyExpr) { + test1.keyExpr -> query.replySuccess(query.keyExpr, value = testValueA) + test2.keyExpr -> query.replySuccess(query.keyExpr, value = testValueB) + } + }).getOrThrow() + + // Testing with null schema on a reply success scenario. + var receivedSample: Sample? = null + session.get(test1, callback = { reply -> + assertTrue(reply is Reply.Success) + receivedSample = reply.sample + }).getOrThrow() + Thread.sleep(200) + + assertNotNull(receivedSample) + assertEquals(Encoding.ID.TEXT_CSV, receivedSample!!.value.encoding.id) + assertNull(receivedSample!!.value.encoding.schema) + + // Testing with non-null schema on a reply success scenario. + receivedSample = null + session.get(test2, callback = { reply -> + assertTrue(reply is Reply.Success) + receivedSample = reply.sample + }).getOrThrow() + Thread.sleep(200) + + assertNotNull(receivedSample) + assertEquals(Encoding.ID.TEXT_CSV, receivedSample!!.value.encoding.id) + assertEquals("test_schema", receivedSample!!.value.encoding.schema) + + queryable.close() + session.close() + } + + @Test + fun encoding_replyErrorTest() { + val session = Session.open(Config.default()).getOrThrow() + val keyExpr = "example/testing/**".intoKeyExpr().getOrThrow() + + val test1 = "example/testing/reply_error".intoSelector().getOrThrow() + val test2 = "example/testing/reply_error_with_schema".intoSelector().getOrThrow() + + val testValueA = Value("test", Encoding(Encoding.ID.TEXT_CSV, null)) + val testValueB = Value("test", Encoding(Encoding.ID.TEXT_CSV, "test_schema")) + + val queryable = session.declareQueryable(keyExpr, callback = { query -> + when (query.keyExpr) { + test1.keyExpr -> query.replyError(testValueA) + test2.keyExpr -> query.replyError(testValueB) + } + }).getOrThrow() + + // Testing with null schema on a reply error scenario. + var errorValue: Value? = null + session.get(test1, callback = { reply -> + assertTrue(reply is Reply.Error) + errorValue = reply.error + }).getOrThrow() + Thread.sleep(200) + + assertNotNull(errorValue) + assertEquals(Encoding.ID.TEXT_CSV, errorValue!!.encoding.id) + assertNull(errorValue!!.encoding.schema) + + // Testing with non-null schema on a reply error scenario. + errorValue = null + session.get(test2, callback = { reply -> + assertTrue(reply is Reply.Error) + errorValue = reply.error + }).getOrThrow() + Thread.sleep(200) + + assertNotNull(errorValue) + assertEquals(Encoding.ID.TEXT_CSV, errorValue!!.encoding.id) + assertEquals("test_schema", errorValue!!.encoding.schema) + + queryable.close() + session.close() + } + + @Test + fun encoding_queryTest() { + val session = Session.open(Config.default()).getOrThrow() + val selector = "example/testing/keyexpr".intoSelector().getOrThrow() + val encodingA = Encoding(Encoding.ID.TEXT_CSV, null) + val encodingB = Encoding(Encoding.ID.TEXT_CSV, "test_schema") + + var receivedEncoding: Encoding? = null + val queryable = session.declareQueryable(selector.keyExpr, callback = { query -> + receivedEncoding = query.encoding + query.close() + }).getOrThrow() + + // Testing with null schema + session.get(selector, callback = {}, value = Value("test", encodingA)) + Thread.sleep(200) + + assertNotNull(receivedEncoding) + assertEquals(Encoding.ID.TEXT_CSV, receivedEncoding!!.id) + assertNull(receivedEncoding!!.schema) + + // Testing non-null schema + receivedEncoding = null + session.get(selector, callback = {}, value = Value("test", encodingB)) + Thread.sleep(200) + + assertNotNull(receivedEncoding) + assertEquals(Encoding.ID.TEXT_CSV, receivedEncoding!!.id) + assertEquals("test_schema", receivedEncoding!!.schema) + + queryable.close() + session.close() + } +} diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt index 6de31aa15..51f9f1e71 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/GetTest.kt @@ -15,13 +15,11 @@ package io.zenoh import io.zenoh.handlers.Handler -import io.zenoh.keyexpr.KeyExpr -import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.QoS import io.zenoh.query.Reply import io.zenoh.queryable.Queryable import io.zenoh.selector.Selector +import io.zenoh.selector.intoSelector import io.zenoh.value.Value import org.apache.commons.net.ntp.TimeStamp import java.time.Duration @@ -37,46 +35,43 @@ class GetTest { } private lateinit var session: Session - private lateinit var keyExpr: KeyExpr + private lateinit var selector: Selector private lateinit var queryable: Queryable @BeforeTest fun setUp() { - session = Session.open().getOrThrow() - keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() - queryable = session.declareQueryable(keyExpr).with { query -> - query.reply(query.keyExpr) - .success(value) - .withTimeStamp(timestamp) - .withKind(kind) - .res() - }.res().getOrThrow() + session = Session.open(Config.default()).getOrThrow() + selector = "example/testing/keyexpr".intoSelector().getOrThrow() + queryable = session.declareQueryable(selector.keyExpr, callback = { query -> + query.replySuccess(query.keyExpr, value, timestamp = timestamp) + }).getOrThrow() } @AfterTest fun tearDown() { session.close() - keyExpr.close() + selector.close() queryable.close() } @Test fun get_runsWithCallback() { var reply: Reply? = null - session.get(keyExpr).with { reply = it }.timeout(Duration.ofMillis(1000)).res() + session.get(selector, callback = { + reply = it + }, timeout = Duration.ofMillis(1000)) assertTrue(reply is Reply.Success) val sample = (reply as Reply.Success).sample assertEquals(value, sample.value) assertEquals(kind, sample.kind) - assertEquals(keyExpr, sample.keyExpr) + assertEquals(selector.keyExpr, sample.keyExpr) assertEquals(timestamp, sample.timestamp) } @Test fun get_runsWithHandler() { - val receiver: ArrayList = session.get(keyExpr).with(TestHandler()) - .timeout(Duration.ofMillis(1000)).res().getOrThrow()!! + val receiver: ArrayList = session.get(selector, handler = TestHandler(), timeout = Duration.ofMillis(1000)).getOrThrow() for (reply in receiver) { reply as Reply.Success @@ -89,17 +84,17 @@ class GetTest { @Test fun getWithSelectorParamsTest() { - var receivedParams = String() - var receivedParamsMap = mapOf() - val queryable = session.declareQueryable(keyExpr).with { it.use { query -> + var receivedParams: String? = null + var receivedParamsMap : Map? = null + val queryable = session.declareQueryable(selector.keyExpr, callback = { query -> receivedParams = query.parameters - receivedParamsMap = query.selector.parametersStringMap().getOrThrow() - }}.res().getOrThrow() + receivedParamsMap = query.selector.parametersStringMap()?.getOrThrow() + }).getOrThrow() val params = "arg1=val1&arg2=val2&arg3" val paramsMap = mapOf("arg1" to "val1", "arg2" to "val2", "arg3" to "") - val selector = Selector(keyExpr, params) - session.get(selector).with {}.timeout(Duration.ofMillis(1000)).res() + val selectorWithParams = Selector(selector.keyExpr, params) + session.get(selectorWithParams, callback = {}, timeout = Duration.ofMillis(1000)) queryable.close() diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt index e43e35703..2d2eeafb6 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/KeyExprTest.kt @@ -21,10 +21,6 @@ import kotlin.test.* class KeyExprTest { - init { - Zenoh.load() - } - @Test fun creation_TryFromTest() { // A couple of examples of valid and invalid key expressions. @@ -100,8 +96,8 @@ class KeyExprTest { @Test fun sessionDeclarationTest() { - val session = Session.open().getOrThrow() - val keyExpr = session.declareKeyExpr("a/b/c").res().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() + val keyExpr = session.declareKeyExpr("a/b/c").getOrThrow() assertEquals("a/b/c", keyExpr.toString()) session.close() keyExpr.close() @@ -109,21 +105,21 @@ class KeyExprTest { @Test fun sessionUnDeclarationTest() { - val session = Session.open().getOrThrow() - val keyExpr = session.declareKeyExpr("a/b/c").res().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() + val keyExpr = session.declareKeyExpr("a/b/c").getOrThrow() assertEquals("a/b/c", keyExpr.toString()) - val undeclare1 = session.undeclare(keyExpr).res() + val undeclare1 = session.undeclare(keyExpr) assertTrue(undeclare1.isSuccess) // Undeclaring twice a key expression shall fail. - val undeclare2 = session.undeclare(keyExpr).res() + val undeclare2 = session.undeclare(keyExpr) assertTrue(undeclare2.isFailure) assertTrue(undeclare2.exceptionOrNull() is SessionException) // Undeclaring a key expr that was not declared through a session. val keyExpr2 = "x/y/z".intoKeyExpr().getOrThrow() - val undeclare3 = session.undeclare(keyExpr2).res() + val undeclare3 = session.undeclare(keyExpr2) assertTrue(undeclare3.isFailure) assertTrue(undeclare3.exceptionOrNull() is SessionException) @@ -131,14 +127,4 @@ class KeyExprTest { keyExpr.close() keyExpr2.close() } - - @Test - fun keyExprIsValidAfterClosingSession() { - val session = Session.open().getOrThrow() - val keyExpr = session.declareKeyExpr("a/b/c").res().getOrThrow() - session.close() - - assertTrue(keyExpr.isValid()) - assertFalse(keyExpr.toString().isEmpty()) // An operation such as toString that goes through JNI is still valid. - } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt index 5e43909b2..b28361d04 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PublisherTest.kt @@ -15,11 +15,9 @@ package io.zenoh import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.KnownEncoding -import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.Encoding +import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.QoS import io.zenoh.publication.Publisher import io.zenoh.sample.Sample import io.zenoh.subscriber.Subscriber @@ -36,12 +34,12 @@ class PublisherTest { @BeforeTest fun setUp() { - session = Session.open().getOrThrow() + session = Session.open(Config.default()).getOrThrow() keyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() - publisher = session.declarePublisher(keyExpr).res().getOrThrow() - subscriber = session.declareSubscriber(keyExpr).with { sample -> + publisher = session.declarePublisher(keyExpr).getOrThrow() + subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSamples.add(sample) - }.res().getOrThrow() + }).getOrThrow() receivedSamples = ArrayList() } @@ -57,12 +55,12 @@ class PublisherTest { fun putTest() { val testValues = arrayListOf( - Value("Test 1".encodeToByteArray(), Encoding(KnownEncoding.TEXT_PLAIN)), - Value("Test 2".encodeToByteArray(), Encoding(KnownEncoding.TEXT_JSON)), - Value("Test 3".encodeToByteArray(), Encoding(KnownEncoding.TEXT_CSV)) + Value("Test 1", Encoding(Encoding.ID.TEXT_PLAIN)), + Value("Test 2", Encoding(Encoding.ID.TEXT_JSON)), + Value("Test 3", Encoding(Encoding.ID.TEXT_CSV)) ) - testValues.forEach() { value -> publisher.put(value).res() } + testValues.forEach() { value -> publisher.put(value) } assertEquals(receivedSamples.size, testValues.size) for ((index, sample) in receivedSamples.withIndex()) { @@ -72,7 +70,7 @@ class PublisherTest { @Test fun deleteTest() { - publisher.delete().res() + publisher.delete() assertEquals(1, receivedSamples.size) assertEquals(SampleKind.DELETE, receivedSamples[0].kind) } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt index 53b595e9d..ef0cd05ed 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/PutTest.kt @@ -14,7 +14,6 @@ package io.zenoh -import io.zenoh.prelude.KnownEncoding import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.Encoding import io.zenoh.sample.Sample @@ -32,12 +31,12 @@ class PutTest { @Test fun putTest() { - val session = Session.open().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() var receivedSample: Sample? = null val keyExpr = TEST_KEY_EXP.intoKeyExpr().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - val value = Value(TEST_PAYLOAD.toByteArray(), Encoding(KnownEncoding.TEXT_PLAIN)) - session.put(keyExpr, value).res() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + val value = Value(TEST_PAYLOAD, Encoding(Encoding.ID.TEXT_PLAIN)) + session.put(keyExpr, value) subscriber.close() session.close() assertNotNull(receivedSample) diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt index d4f4ecad2..7f4e32354 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/QueryableTest.kt @@ -17,8 +17,9 @@ package io.zenoh import io.zenoh.handlers.Handler import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.prelude.SampleKind -import io.zenoh.prelude.QoS +import io.zenoh.prelude.* +import io.zenoh.prelude.Encoding.ID.ZENOH_STRING +import io.zenoh.protocol.into import io.zenoh.query.Reply import io.zenoh.queryable.Query import io.zenoh.sample.Sample @@ -45,7 +46,7 @@ class QueryableTest { @BeforeTest fun setUp() { - session = Session.open().getOrThrow() + session = Session.open(Config.default()).getOrThrow() testKeyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() } @@ -59,16 +60,20 @@ class QueryableTest { @Test fun queryable_runsWithCallback() = runBlocking { val sample = Sample( - testKeyExpr, Value(testPayload), SampleKind.PUT, TimeStamp(Date.from(Instant.now())), QoS.default() + testKeyExpr, + Value(testPayload), + SampleKind.PUT, + TimeStamp(Date.from(Instant.now())), + QoS() ) - val queryable = session.declareQueryable(testKeyExpr).with { query -> - query.reply(testKeyExpr).success(sample.value).withTimeStamp(sample.timestamp!!).res() - }.res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replySuccess(testKeyExpr, value = sample.value, timestamp = sample.timestamp) + }).getOrThrow() var reply: Reply? = null val delay = Duration.ofMillis(1000) withTimeout(delay) { - session.get(testKeyExpr).with { reply = it }.timeout(delay).res() + session.get(testKeyExpr.intoSelector(), callback = { reply = it }, timeout = delay) } assertTrue(reply is Reply.Success) @@ -80,14 +85,14 @@ class QueryableTest { @Test fun queryable_runsWithHandler() = runBlocking { val handler = QueryHandler() - val queryable = session.declareQueryable(testKeyExpr).with(handler).res().getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, handler = handler).getOrThrow() delay(500) val receivedReplies = ArrayList() - session.get(testKeyExpr).with { reply: Reply -> + session.get(testKeyExpr.intoSelector(), callback = { reply: Reply -> receivedReplies.add(reply) - }.res() + }) delay(500) @@ -97,47 +102,114 @@ class QueryableTest { } @Test - fun queryableBuilder_channelHandlerIsTheDefaultHandler() = runBlocking { - val queryable = session.declareQueryable(testKeyExpr).res().getOrThrow() - assertTrue(queryable.receiver is Channel) + fun queryTest() = runBlocking { + var receivedQuery: Query? = null + val queryable = + session.declareQueryable(testKeyExpr, callback = { query -> receivedQuery = query }).getOrThrow() + + session.get(testKeyExpr.intoSelector(), callback = {}) + + delay(100) + assertNotNull(receivedQuery) + assertNull(receivedQuery!!.payload) + assertNull(receivedQuery!!.encoding) + assertNull(receivedQuery!!.attachment) + + receivedQuery = null + val payload = "Test value" + val attachment = "Attachment".into() + session.get(testKeyExpr.intoSelector(), callback = {}, value = Value(payload, ZENOH_STRING), attachment = attachment) + + delay(100) + assertNotNull(receivedQuery) + assertEquals(payload, receivedQuery!!.payload!!.bytes.decodeToString()) + assertEquals(ZENOH_STRING, receivedQuery!!.encoding!!.id) + assertEquals(attachment, receivedQuery!!.attachment) + queryable.close() } @Test - fun queryTest() = runBlocking { - var receivedQuery: Query? = null - val queryable = session.declareQueryable(testKeyExpr).with { query -> receivedQuery = query }.res().getOrThrow() - - session.get(testKeyExpr).res() + fun queryReplySuccessTest() { + val message = "Test message" + val timestamp = TimeStamp.getCurrentTime() + val qos = QoS(priority = Priority.DATA_HIGH, express = true, congestionControl = CongestionControl.DROP) + val priority = Priority.DATA_HIGH + val express = true + val congestionControl = CongestionControl.DROP + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replySuccess(testKeyExpr, value = Value(message), timestamp = timestamp, qos = qos) + }).getOrThrow() + + var receivedReply: Reply? = null + session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) - delay(1000) queryable.close() - assertNotNull(receivedQuery) - assertNull(receivedQuery!!.value) + + assertTrue(receivedReply is Reply.Success) + val reply = receivedReply as Reply.Success + assertEquals(message, reply.sample.value.payload.bytes.decodeToString()) + assertEquals(timestamp, reply.sample.timestamp) + assertEquals(priority, reply.sample.qos.priority) + assertEquals(express, reply.sample.qos.express) + assertEquals(congestionControl, reply.sample.qos.congestionControl) } @Test - fun queryWithValueTest() = runBlocking { - var receivedQuery: Query? = null - val queryable = session.declareQueryable(testKeyExpr).with { query -> receivedQuery = query }.res().getOrThrow() + fun queryReplyErrorTest() { + val message = "Error message" + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replyError(error = Value(message)) + }).getOrThrow() - session.get(testKeyExpr).withValue("Test value").res() + var receivedReply: Reply? = null + session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) - delay(1000) + Thread.sleep(1000) queryable.close() - assertNotNull(receivedQuery) - assertEquals(Value("Test value"), receivedQuery!!.value) + + assertNotNull(receivedReply) + assertTrue(receivedReply is Reply.Error) + val reply = receivedReply as Reply.Error + assertEquals(message, reply.error.payload.bytes.decodeToString()) + } + + @Test + fun queryReplyDeleteTest() { + val timestamp = TimeStamp.getCurrentTime() + val priority = Priority.DATA_HIGH + val express = true + val congestionControl = CongestionControl.DROP + val qos = QoS(priority = Priority.DATA_HIGH, express = true, congestionControl = CongestionControl.DROP) + + val queryable = session.declareQueryable(testKeyExpr, callback = { query -> + query.replyDelete(testKeyExpr, timestamp = timestamp, qos = qos) + }).getOrThrow() + var receivedReply: Reply? = null + session.get(testKeyExpr.intoSelector(), callback = { receivedReply = it }, timeout = Duration.ofMillis(10)) + + queryable.close() + + assertNotNull(receivedReply) + assertTrue(receivedReply is Reply.Delete) + val reply = receivedReply as Reply.Delete + assertEquals(timestamp, reply.timestamp) + assertEquals(priority, reply.qos.priority) + assertEquals(express, reply.qos.express) + assertEquals(congestionControl, reply.qos.congestionControl) } @OptIn(DelicateCoroutinesApi::class) @Test fun onCloseTest() = runBlocking { var onCloseWasCalled = false - val queryable = session.declareQueryable(testKeyExpr).onClose { onCloseWasCalled = true }.res().getOrThrow() + val channel = Channel() + val queryable = + session.declareQueryable(testKeyExpr, channel = channel, onClose = { onCloseWasCalled = true }).getOrThrow() queryable.undeclare() assertTrue(onCloseWasCalled) - assertTrue(queryable.receiver!!.isClosedForReceive) + assertTrue(queryable.receiver.isClosedForReceive) } } @@ -162,9 +234,13 @@ private class QueryHandler : Handler { val payload = "Hello queryable $counter!" counter++ val sample = Sample( - query.keyExpr, Value(payload), SampleKind.PUT, TimeStamp(Date.from(Instant.now())), QoS.default() + query.keyExpr, + Value(payload), + SampleKind.PUT, + TimeStamp(Date.from(Instant.now())), + QoS() ) performedReplies.add(sample) - query.reply(query.keyExpr).success(sample.value).withTimeStamp(sample.timestamp!!).res() + query.replySuccess(query.keyExpr, value = sample.value, timestamp = sample.timestamp) } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SelectorTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SelectorTest.kt index 0a579563c..87b1a2d29 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SelectorTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SelectorTest.kt @@ -9,10 +9,6 @@ import kotlin.test.assertFailsWith class SelectorTest { - init { - Zenoh.load() - } - @Test fun selectorFromStringTest() { "a/b/c?arg1=val1".intoSelector().getOrThrow().use { selector: Selector -> diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt index 55a4652e9..47909d473 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SessionTest.kt @@ -17,7 +17,6 @@ package io.zenoh import io.zenoh.exceptions.SessionException import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr -import io.zenoh.sample.Sample import kotlinx.coroutines.runBlocking import java.nio.file.Path import kotlin.test.* @@ -37,7 +36,7 @@ class SessionTest { @Test fun sessionStartCloseTest() { - val session = Session.open().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() assertTrue(session.isOpen()) session.close() assertFalse(session.isOpen()) @@ -45,16 +44,16 @@ class SessionTest { @Test fun sessionOpeningFailure() { - val invalidConfig = Config.from(Path.of("invalid")) + val invalidConfig = Config.from(path = Path.of("invalid")) assertFailsWith { Session.open(invalidConfig).getOrThrow() } } @Test fun sessionClose_succeedsDespiteNotFreeingAllDeclarations() { - val session = Session.open().getOrThrow() - val queryable = session.declareQueryable(testKeyExpr).with {}.res().getOrThrow() - val subscriber = session.declareSubscriber(testKeyExpr).with {}.res().getOrThrow() - val publisher = session.declarePublisher(testKeyExpr).res().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() + val queryable = session.declareQueryable(testKeyExpr, callback = {}).getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() + val publisher = session.declarePublisher(testKeyExpr).getOrThrow() session.close() queryable.close() @@ -63,32 +62,26 @@ class SessionTest { } @Test - fun sessionClose_declarationsAreAliveAfterClosingSessionTest() = runBlocking { - val session = Session.open().getOrThrow() - var receivedSample: Sample? = null + fun sessionClose_declarationsAreUndeclaredAfterClosingSessionTest() = runBlocking { + val session = Session.open(Config.default()).getOrThrow() - val publisher = session.declarePublisher(testKeyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(testKeyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(testKeyExpr).getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() session.close() - assertTrue(publisher.isValid()) - assertTrue(subscriber.isValid()) + assertFalse(publisher.isValid()) + assertFalse(subscriber.isValid()) - publisher.put("Test").res() - assertNotNull(receivedSample) - assertEquals("Test", receivedSample!!.value.payload.decodeToString()) - - subscriber.close() - publisher.close() + assertTrue(publisher.put("Test").isFailure) } @Test fun sessionClose_newDeclarationsReturnNullAfterClosingSession() { - val session = Session.open().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() session.close() - assertFailsWith { session.declarePublisher(testKeyExpr).res().getOrThrow() } - assertFailsWith { session.declareSubscriber(testKeyExpr).with {}.res().getOrThrow() } - assertFailsWith { session.declareQueryable(testKeyExpr).with {}.res().getOrThrow() } + assertFailsWith { session.declarePublisher(testKeyExpr).getOrThrow() } + assertFailsWith { session.declareSubscriber(testKeyExpr, callback = {}).getOrThrow() } + assertFailsWith { session.declareQueryable(testKeyExpr, callback = {}).getOrThrow() } } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt index 7f52d3c5c..83ac61443 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/SubscriberTest.kt @@ -16,13 +16,13 @@ package io.zenoh import io.zenoh.handlers.Handler import io.zenoh.keyexpr.KeyExpr -import io.zenoh.prelude.KnownEncoding import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.Encoding import io.zenoh.sample.Sample import io.zenoh.value.Value import io.zenoh.prelude.CongestionControl import io.zenoh.prelude.Priority +import io.zenoh.prelude.QoS import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.runBlocking @@ -34,13 +34,13 @@ import kotlin.test.* class SubscriberTest { companion object { - val TEST_PRIORITY = Priority.DATA_HIGH; - val TEST_CONGESTION_CONTROL = CongestionControl.BLOCK; + val TEST_PRIORITY = Priority.DATA_HIGH + val TEST_CONGESTION_CONTROL = CongestionControl.BLOCK val testValues = arrayListOf( - Value("Test 1".encodeToByteArray(), Encoding(KnownEncoding.TEXT_PLAIN)), - Value("Test 2".encodeToByteArray(), Encoding(KnownEncoding.TEXT_JSON)), - Value("Test 3".encodeToByteArray(), Encoding(KnownEncoding.TEXT_CSV)) + Value("Test 1", Encoding(Encoding.ID.TEXT_PLAIN)), + Value("Test 2", Encoding(Encoding.ID.TEXT_JSON)), + Value("Test 3", Encoding(Encoding.ID.TEXT_CSV)) ) } @@ -49,7 +49,7 @@ class SubscriberTest { @BeforeTest fun setUp() { - session = Session.open().getOrThrow() + session = Session.open(Config.default()).getOrThrow() testKeyExpr = "example/testing/keyexpr".intoKeyExpr().getOrThrow() } @@ -63,20 +63,17 @@ class SubscriberTest { fun subscriber_runsWithCallback() { val receivedSamples = ArrayList() val subscriber = - session.declareSubscriber(testKeyExpr).with { sample -> receivedSamples.add(sample) }.res().getOrThrow() + session.declareSubscriber(testKeyExpr, callback = { sample -> receivedSamples.add(sample)}).getOrThrow() testValues.forEach { value -> - session.put(testKeyExpr, value) - .priority(TEST_PRIORITY) - .congestionControl(TEST_CONGESTION_CONTROL) - .res() + session.put(testKeyExpr, value, qos = QoS(priority = TEST_PRIORITY, congestionControl = TEST_CONGESTION_CONTROL)) } assertEquals(receivedSamples.size, testValues.size) receivedSamples.zip(testValues).forEach { (sample, value) -> assertEquals(sample.value, value) - assertEquals(sample.qos.priority(), TEST_PRIORITY) - assertEquals(sample.qos.congestionControl(), TEST_CONGESTION_CONTROL) + assertEquals(sample.qos.priority, TEST_PRIORITY) + assertEquals(sample.qos.congestionControl, TEST_CONGESTION_CONTROL) } subscriber.close() @@ -85,41 +82,31 @@ class SubscriberTest { @Test fun subscriber_runsWithHandler() { val handler = QueueHandler() - val subscriber = session.declareSubscriber(testKeyExpr).with(handler).res().getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, handler = handler).getOrThrow() - testValues.forEach { value -> - session.put(testKeyExpr, value) - .priority(TEST_PRIORITY) - .congestionControl(TEST_CONGESTION_CONTROL) - .res() + testValues.forEach { value -> + session.put(testKeyExpr, value, qos = QoS(priority = TEST_PRIORITY, congestionControl = TEST_CONGESTION_CONTROL)) } assertEquals(handler.queue.size, testValues.size) handler.queue.zip(testValues).forEach { (sample, value) -> assertEquals(sample.value, value) - assertEquals(sample.qos.priority(), TEST_PRIORITY) - assertEquals(sample.qos.congestionControl(), TEST_CONGESTION_CONTROL) + assertEquals(sample.qos.priority, TEST_PRIORITY) + assertEquals(sample.qos.congestionControl, TEST_CONGESTION_CONTROL) } subscriber.close() } - @Test - fun subscriberBuilder_channelHandlerIsTheDefaultHandler() { - val subscriber = session.declareSubscriber(testKeyExpr).res().getOrThrow() - assertTrue(subscriber.receiver is Channel) - subscriber.close() - } - @Test fun subscriber_isDeclaredWithNonDeclaredKeyExpression() { // Declaring a subscriber with an undeclared key expression and verifying it properly receives samples. val keyExpr = KeyExpr("example/**") - val session = Session.open().getOrThrow() + val session = Session.open(Config.default()).getOrThrow() val receivedSamples = ArrayList() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSamples.add(sample) }.res().getOrThrow() - testValues.forEach { value -> session.put(testKeyExpr, value).res() } + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSamples.add(sample) }).getOrThrow() + testValues.forEach { value -> session.put(testKeyExpr, value) } subscriber.close() assertEquals(receivedSamples.size, testValues.size) @@ -133,11 +120,11 @@ class SubscriberTest { @Test fun onCloseTest() = runBlocking { var onCloseWasCalled = false - val subscriber = session.declareSubscriber(testKeyExpr).onClose { onCloseWasCalled = true }.res().getOrThrow() + val subscriber = session.declareSubscriber(testKeyExpr, channel = Channel(), onClose = { onCloseWasCalled = true }).getOrThrow() subscriber.undeclare() assertTrue(onCloseWasCalled) - assertTrue(subscriber.receiver!!.isClosedForReceive) + assertTrue(subscriber.receiver.isClosedForReceive) } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt index 8adf5422b..dc527d9c3 100644 --- a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/UserAttachmentTest.kt @@ -14,16 +14,11 @@ package io.zenoh -import io.zenoh.jni.decodeAttachment -import io.zenoh.jni.encodeAttachment -import io.zenoh.jni.toByteArray -import io.zenoh.jni.toInt import io.zenoh.keyexpr.KeyExpr import io.zenoh.keyexpr.intoKeyExpr import io.zenoh.prelude.Encoding -import io.zenoh.prelude.KnownEncoding +import io.zenoh.protocol.ZBytes import io.zenoh.query.Reply -import io.zenoh.sample.Attachment import io.zenoh.sample.Sample import io.zenoh.value.Value import java.time.Duration @@ -35,18 +30,15 @@ class UserAttachmentTest { private lateinit var keyExpr: KeyExpr companion object { - val value = Value("test", Encoding(KnownEncoding.TEXT_PLAIN)) + val value = Value("test", Encoding(Encoding.ID.TEXT_PLAIN)) const val keyExprString = "example/testing/attachment" - val attachmentPairs = arrayListOf( - "key1" to "value1", "key2" to "value2", "key3" to "value3", "repeatedKey" to "value1", "repeatedKey" to "value2" - ) - val attachment = - Attachment(attachmentPairs.map { it.first.encodeToByteArray() to it.second.encodeToByteArray() }) + const val attachment = "mock_attachment" + val attachmentZBytes = ZBytes.from(attachment) } @BeforeTest fun setup() { - session = Session.open().getOrThrow() + session = Session.open(Config.default()).getOrThrow() keyExpr = keyExprString.intoKeyExpr().getOrThrow() } @@ -56,53 +48,46 @@ class UserAttachmentTest { keyExpr.close() } - private fun assertAttachmentOk(attachment: Attachment?) { - assertNotNull(attachment) - val receivedPairs = attachment.values - assertEquals(attachmentPairs.size, receivedPairs.size) - for ((index, receivedPair) in receivedPairs.withIndex()) { - assertEquals(attachmentPairs[index].first, receivedPair.first.decodeToString()) - assertEquals(attachmentPairs[index].second, receivedPair.second.decodeToString()) - } - } - @Test fun putWithAttachmentTest() { var receivedSample: Sample? = null - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() - session.put(keyExpr, value).withAttachment(attachment).res() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() + session.put(keyExpr, value, attachment = attachmentZBytes) subscriber.close() assertNotNull(receivedSample) { - assertEquals(value, it.value) - assertAttachmentOk(it.attachment) + val receivedAttachment = it.attachment!! + assertEquals(attachment, receivedAttachment.toString()) } } @Test fun publisherPutWithAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample - }.res().getOrThrow() + }).getOrThrow() - publisher.put("test").withAttachment(attachment).res() + publisher.put("test", attachment = attachmentZBytes) publisher.close() subscriber.close() - assertAttachmentOk(receivedSample!!.attachment!!) + assertNotNull(receivedSample) { + val receivedAttachment = it.attachment!! + assertEquals(attachment, receivedAttachment.deserialize().getOrNull()) + } } @Test fun publisherPutWithoutAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - publisher.put("test").res() + publisher.put("test") publisher.close() subscriber.close() @@ -115,24 +100,27 @@ class UserAttachmentTest { @Test fun publisherDeleteWithAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - publisher.delete().withAttachment(attachment).res() + publisher.delete(attachment = attachmentZBytes) publisher.close() subscriber.close() - assertAttachmentOk(receivedSample?.attachment) + assertNotNull(receivedSample) { + val receivedAttachment = it.attachment!! + assertEquals(attachment, receivedAttachment.toString()) + } } @Test fun publisherDeleteWithoutAttachmentTest() { var receivedSample: Sample? = null - val publisher = session.declarePublisher(keyExpr).res().getOrThrow() - val subscriber = session.declareSubscriber(keyExpr).with { sample -> receivedSample = sample }.res().getOrThrow() + val publisher = session.declarePublisher(keyExpr).getOrThrow() + val subscriber = session.declareSubscriber(keyExpr, callback = { sample -> receivedSample = sample }).getOrThrow() - publisher.delete().res() + publisher.delete() publisher.close() subscriber.close() @@ -144,47 +132,52 @@ class UserAttachmentTest { @Test fun queryWithAttachmentTest() { - var receivedAttachment: Attachment? = null - val queryable = session.declareQueryable(keyExpr).with { query -> + var receivedAttachment: ZBytes? = null + val queryable = session.declareQueryable(keyExpr, callback = { query -> receivedAttachment = query.attachment - query.reply(keyExpr).success("test").res() - }.res().getOrThrow() + query.replySuccess(keyExpr, value = Value("test")) + }).getOrThrow() - session.get(keyExpr).with {}.withAttachment(attachment).timeout(Duration.ofMillis(1000)).res().getOrThrow() + session.get(keyExpr.intoSelector(), callback = {}, attachment = attachmentZBytes, timeout = Duration.ofMillis(1000)).getOrThrow() queryable.close() - assertAttachmentOk(receivedAttachment) + assertNotNull(receivedAttachment) { + assertEquals(attachmentZBytes, it) + } } @Test fun queryReplyWithAttachmentTest() { - var receivedAttachment: Attachment? = null - val queryable = session.declareQueryable(keyExpr).with { query -> - query.reply(keyExpr).success("test").withAttachment(attachment).res() - }.res().getOrThrow() - - session.get(keyExpr).with { reply -> - if (reply is Reply.Success) { - receivedAttachment = reply.sample.attachment + var reply: Reply? = null + val queryable = session.declareQueryable(keyExpr, callback = { query -> + query.replySuccess(keyExpr, value = Value("test"), attachment = attachmentZBytes) + }).getOrThrow() + + session.get(keyExpr.intoSelector(), callback = { + if (it is Reply.Success) { + reply = it } - }.timeout(Duration.ofMillis(1000)).res().getOrThrow() + }, timeout = Duration.ofMillis(1000)).getOrThrow() queryable.close() - assertAttachmentOk(receivedAttachment) + assertNotNull(reply) { + val receivedAttachment = (it as Reply.Success).sample.attachment!! + assertEquals(attachment, receivedAttachment.toString()) + } } @Test fun queryReplyWithoutAttachmentTest() { var reply: Reply? = null - val queryable = session.declareQueryable(keyExpr).with { query -> - query.reply(keyExpr).success("test").res() - }.res().getOrThrow() + val queryable = session.declareQueryable(keyExpr, callback = { query -> + query.replySuccess(keyExpr, value = Value("test")) + }).getOrThrow() - session.get(keyExpr).with { + session.get(keyExpr.intoSelector(), callback = { reply = it - }.timeout(Duration.ofMillis(1000)).res().getOrThrow() + }, timeout = Duration.ofMillis(1000)).getOrThrow() queryable.close() @@ -193,23 +186,4 @@ class UserAttachmentTest { assertNull(it.sample.attachment) } } - - @Test - fun encodeAndDecodeNumbersTest() { - val numbers: List = arrayListOf(0, 1, -1, 12345, -12345, 123567, 123456789, -123456789) - - numbers.forEach { number -> - val bytes = number.toByteArray() - val decodedNumber: Int = bytes.toInt() - assertEquals(number, decodedNumber) - } - } - - @Test - fun encodeAndDecodeAttachmentTest() { - val encodedAttachment = encodeAttachment(attachment) - val decodedAttachment = decodeAttachment(encodedAttachment) - - assertAttachmentOk(decodedAttachment) - } } diff --git a/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt new file mode 100644 index 000000000..6320cd5ce --- /dev/null +++ b/zenoh-kotlin/src/commonTest/kotlin/io/zenoh/ZBytesTest.kt @@ -0,0 +1,591 @@ +// +// Copyright (c) 2023 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +package io.zenoh + +import io.zenoh.protocol.Deserializable +import io.zenoh.protocol.Serializable +import io.zenoh.protocol.ZBytes +import io.zenoh.protocol.into +import org.junit.jupiter.api.Assertions.assertArrayEquals + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.reflect.KClass +import kotlin.reflect.typeOf +import kotlin.test.Test +import kotlin.test.assertTrue + +data class SimpleTestCase( + val originalItem: T, + val clazz: KClass +) + +data class ListTestCase( + val originalList: List, + val itemclazz: KClass +) + +data class MapTestCase( + val originalMap: Map, + val keyclazz: KClass, + val valueclazz: KClass, +) + +class ZBytesTests { + + companion object { + @JvmStatic + fun simpleTestCases(): List> { + return listOf( + SimpleTestCase(1.toByte(), Byte::class), + SimpleTestCase(1.toShort(), Short::class), + SimpleTestCase(1, Int::class), + SimpleTestCase(1L, Long::class), + SimpleTestCase(1.0f, Float::class), + SimpleTestCase(1.0, Double::class), + SimpleTestCase("value1", String::class), + SimpleTestCase(byteArrayOf(1, 2, 3), ByteArray::class), + SimpleTestCase(MyZBytes("foo"), MyZBytes::class) + ) + } + + @JvmStatic + fun listTestCases(): List> { + return listOf( + // Byte Lists + ListTestCase(listOf(1.toByte(), 2.toByte(), 3.toByte()), Byte::class), + // Short Lists + ListTestCase(listOf(1.toShort(), 2.toShort(), 3.toShort()), Short::class), + // Int Lists + ListTestCase(listOf(1, 2, 3), Int::class), + // Long Lists + ListTestCase(listOf(1L, 2L, 3L), Long::class), + // Float Lists + ListTestCase(listOf(1.0f, 2.0f, 3.0f), Float::class), + // Double Lists + ListTestCase(listOf(1.0, 2.0, 3.0), Double::class), + // String Lists + ListTestCase(listOf("value1", "value2", "value3"), String::class), + // ByteArray Lists + ListTestCase(listOf(byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6)), ByteArray::class), + // MyZBytes Lists + ListTestCase(listOf(MyZBytes("foo"), MyZBytes("bar")), MyZBytes::class) + ) + } + + @JvmStatic + fun mapTestCases(): List> { + return listOf( + // Byte Keys + MapTestCase(mapOf(1.toByte() to "value1", 2.toByte() to "value2"), Byte::class, String::class), + MapTestCase(mapOf(1.toByte() to 1.toByte(), 2.toByte() to 2.toByte()), Byte::class, Byte::class), + MapTestCase(mapOf(1.toByte() to 1.toShort(), 2.toByte() to 2.toShort()), Byte::class, Short::class), + MapTestCase(mapOf(1.toByte() to 1, 2.toByte() to 2), Byte::class, Int::class), + MapTestCase(mapOf(1.toByte() to 1L, 2.toByte() to 2L), Byte::class, Long::class), + MapTestCase(mapOf(1.toByte() to 1.0f, 2.toByte() to 2.0f), Byte::class, Float::class), + MapTestCase(mapOf(1.toByte() to 1.0, 2.toByte() to 2.0), Byte::class, Double::class), + MapTestCase(mapOf(1.toByte() to byteArrayOf(1, 2, 3), 2.toByte() to byteArrayOf(4, 5, 6)), Byte::class, ByteArray::class), + MapTestCase(mapOf(1.toByte() to MyZBytes("foo"), 2.toByte() to MyZBytes("bar")), Byte::class, MyZBytes::class), + + // Short Keys + MapTestCase(mapOf(1.toShort() to "value1", 2.toShort() to "value2"), Short::class, String::class), + MapTestCase(mapOf(1.toShort() to 1.toByte(), 2.toShort() to 2.toByte()), Short::class, Byte::class), + MapTestCase(mapOf(1.toShort() to 1.toShort(), 2.toShort() to 2.toShort()), Short::class, Short::class), + MapTestCase(mapOf(1.toShort() to 1, 2.toShort() to 2), Short::class, Int::class), + MapTestCase(mapOf(1.toShort() to 1L, 2.toShort() to 2L), Short::class, Long::class), + MapTestCase(mapOf(1.toShort() to 1.0f, 2.toShort() to 2.0f), Short::class, Float::class), + MapTestCase(mapOf(1.toShort() to 1.0, 2.toShort() to 2.0), Short::class, Double::class), + MapTestCase(mapOf(1.toShort() to byteArrayOf(1, 2, 3), 2.toShort() to byteArrayOf(4, 5, 6)), Short::class, ByteArray::class), + MapTestCase(mapOf(1.toShort() to MyZBytes("foo"), 2.toShort() to MyZBytes("bar")), Short::class, MyZBytes::class), + + // Int Keys + MapTestCase(mapOf(1 to "value1", 2 to "value2"), Int::class, String::class), + MapTestCase(mapOf(1 to 1.toByte(), 2 to 2.toByte()), Int::class, Byte::class), + MapTestCase(mapOf(1 to 1.toShort(), 2 to 2.toShort()), Int::class, Short::class), + MapTestCase(mapOf(1 to 1, 2 to 2), Int::class, Int::class), + MapTestCase(mapOf(1 to 1L, 2 to 2L), Int::class, Long::class), + MapTestCase(mapOf(1 to 1.0f, 2 to 2.0f), Int::class, Float::class), + MapTestCase(mapOf(1 to 1.0, 2 to 2.0), Int::class, Double::class), + MapTestCase(mapOf(1 to byteArrayOf(1, 2, 3), 2 to byteArrayOf(4, 5, 6)), Int::class, ByteArray::class), + MapTestCase(mapOf(1 to MyZBytes("foo"), 2 to MyZBytes("bar")), Int::class, MyZBytes::class), + + // Long Keys + MapTestCase(mapOf(1L to "value1", 2L to "value2"), Long::class, String::class), + MapTestCase(mapOf(1L to 1.toByte(), 2L to 2.toByte()), Long::class, Byte::class), + MapTestCase(mapOf(1L to 1.toShort(), 2L to 2.toShort()), Long::class, Short::class), + MapTestCase(mapOf(1L to 1, 2L to 2), Long::class, Int::class), + MapTestCase(mapOf(1L to 1L, 2L to 2L), Long::class, Long::class), + MapTestCase(mapOf(1L to 1.0f, 2L to 2.0f), Long::class, Float::class), + MapTestCase(mapOf(1L to 1.0, 2L to 2.0), Long::class, Double::class), + MapTestCase(mapOf(1L to byteArrayOf(1, 2, 3), 2L to byteArrayOf(4, 5, 6)), Long::class, ByteArray::class), + MapTestCase(mapOf(1L to MyZBytes("foo"), 2L to MyZBytes("bar")), Long::class, MyZBytes::class), + + // Float Keys + MapTestCase(mapOf(1.0f to "value1", 2.0f to "value2"), Float::class, String::class), + MapTestCase(mapOf(1.0f to 1.toByte(), 2.0f to 2.toByte()), Float::class, Byte::class), + MapTestCase(mapOf(1.0f to 1.toShort(), 2.0f to 2.toShort()), Float::class, Short::class), + MapTestCase(mapOf(1.0f to 1, 2.0f to 2), Float::class, Int::class), + MapTestCase(mapOf(1.0f to 1L, 2.0f to 2L), Float::class, Long::class), + MapTestCase(mapOf(1.0f to 1.0f, 2.0f to 2.0f), Float::class, Float::class), + MapTestCase(mapOf(1.0f to 1.0, 2.0f to 2.0), Float::class, Double::class), + MapTestCase(mapOf(1.0f to byteArrayOf(1, 2, 3), 2.0f to byteArrayOf(4, 5, 6)), Float::class, ByteArray::class), + MapTestCase(mapOf(1.0f to MyZBytes("foo"), 2.0f to MyZBytes("bar")), Float::class, MyZBytes::class), + + // Double Keys + MapTestCase(mapOf(1.0 to "value1", 2.0 to "value2"), Double::class, String::class), + MapTestCase(mapOf(1.0 to 1.toByte(), 2.0 to 2.toByte()), Double::class, Byte::class), + MapTestCase(mapOf(1.0 to 1.toShort(), 2.0 to 2.toShort()), Double::class, Short::class), + MapTestCase(mapOf(1.0 to 1, 2.0 to 2), Double::class, Int::class), + MapTestCase(mapOf(1.0 to 1L, 2.0 to 2L), Double::class, Long::class), + MapTestCase(mapOf(1.0 to 1.0f, 2.0 to 2.0f), Double::class, Float::class), + MapTestCase(mapOf(1.0 to 1.0, 2.0 to 2.0), Double::class, Double::class), + MapTestCase(mapOf(1.0 to byteArrayOf(1, 2, 3), 2.0 to byteArrayOf(4, 5, 6)), Double::class, ByteArray::class), + MapTestCase(mapOf(1.0 to MyZBytes("foo"), 2.0 to MyZBytes("bar")), Double::class, MyZBytes::class), + + // String Keys + MapTestCase(mapOf("key1" to "value1", "key2" to "value2"), String::class, String::class), + MapTestCase(mapOf("key1" to 1.toByte(), "key2" to 2.toByte()), String::class, Byte::class), + MapTestCase(mapOf("key1" to 1.toShort(), "key2" to 2.toShort()), String::class, Short::class), + MapTestCase(mapOf("key1" to 1, "key2" to 2), String::class, Int::class), + MapTestCase(mapOf("key1" to 1L, "key2" to 2L), String::class, Long::class), + MapTestCase(mapOf("key1" to 1.0f, "key2" to 2.0f), String::class, Float::class), + MapTestCase(mapOf("key1" to 1.0, "key2" to 2.0), String::class, Double::class), + MapTestCase(mapOf("key1" to byteArrayOf(1, 2, 3), "key2" to byteArrayOf(4, 5, 6)), String::class, ByteArray::class), + MapTestCase(mapOf("key1" to MyZBytes("foo"), "key2" to MyZBytes("bar")), String::class, MyZBytes::class), + + // ByteArray Keys + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to "value1", byteArrayOf(4, 5, 6) to "value2"), ByteArray::class, String::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.toByte(), byteArrayOf(4, 5, 6) to 2.toByte()), ByteArray::class, Byte::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.toShort(), byteArrayOf(4, 5, 6) to 2.toShort()), ByteArray::class, Short::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1, byteArrayOf(4, 5, 6) to 2), ByteArray::class, Int::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1L, byteArrayOf(4, 5, 6) to 2L), ByteArray::class, Long::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.0f, byteArrayOf(4, 5, 6) to 2.0f), ByteArray::class, Float::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to 1.0, byteArrayOf(4, 5, 6) to 2.0), ByteArray::class, Double::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to byteArrayOf(1, 2, 3), byteArrayOf(4, 5, 6) to byteArrayOf(4, 5, 6)), ByteArray::class, ByteArray::class), + MapTestCase(mapOf(byteArrayOf(1, 2, 3) to MyZBytes("foo"), byteArrayOf(4, 5, 6) to MyZBytes("bar")), ByteArray::class, MyZBytes::class), + + // MyZBytes (Serializable and Deserializable) Keys + MapTestCase(mapOf(MyZBytes("foo") to "value1", MyZBytes("bar") to "value2"), MyZBytes::class, String::class), + MapTestCase(mapOf(MyZBytes("foo") to 1.toByte(), MyZBytes("bar") to 2.toByte()), MyZBytes::class, Byte::class), + MapTestCase(mapOf(MyZBytes("foo") to 1.toShort(), MyZBytes("bar") to 2.toShort()), MyZBytes::class, Short::class), + MapTestCase(mapOf(MyZBytes("foo") to 1, MyZBytes("bar") to 2), MyZBytes::class, Int::class), + MapTestCase(mapOf(MyZBytes("foo") to 1L, MyZBytes("bar") to 2L), MyZBytes::class, Long::class), + MapTestCase(mapOf(MyZBytes("foo") to 1.0f, MyZBytes("bar") to 2.0f), MyZBytes::class, Float::class), + MapTestCase(mapOf(MyZBytes("foo") to 1.0, MyZBytes("bar") to 2.0), MyZBytes::class, Double::class), + MapTestCase(mapOf(MyZBytes("foo") to byteArrayOf(1, 2, 3), MyZBytes("bar") to byteArrayOf(4, 5, 6)), MyZBytes::class, ByteArray::class), + MapTestCase(mapOf(MyZBytes("foo") to MyZBytes("foo"), MyZBytes("bar") to MyZBytes("bar")), MyZBytes::class, MyZBytes::class) + ) + } + } + + @ParameterizedTest + @MethodSource("simpleTestCases") + fun serializationAndDeserialization_simpleTest(testCase: SimpleTestCase) { + val originalItem = testCase.originalItem + val clazz = testCase.clazz + + val bytes = ZBytes.serialize(originalItem, clazz = clazz).getOrThrow() + val deserializedItem = bytes.deserialize(clazz = clazz).getOrThrow() + + if (originalItem is ByteArray) { + assertArrayEquals(originalItem, deserializedItem as ByteArray) + } else { + assertEquals(originalItem, deserializedItem) + } + } + + @ParameterizedTest + @MethodSource("listTestCases") + fun serializationAndDeserialization_listTest(testCase: ListTestCase) { + val originalList = testCase.originalList + val itemClass = testCase.itemclazz + + val bytes = ZBytes.serialize(originalList).getOrThrow() + + val deserializedList = bytes.deserialize(clazz = List::class, arg1clazz = itemClass).getOrThrow() + + if (originalList.isNotEmpty() && originalList[0] is ByteArray) { + originalList.forEachIndexed { index, value -> + assertArrayEquals(value as ByteArray, deserializedList[index] as ByteArray) + } + } else { + assertEquals(originalList, deserializedList) + } + } + + @ParameterizedTest + @MethodSource("mapTestCases") + fun serializationAndDeserialization_mapTest(testCase: MapTestCase) { + val originalMap = testCase.originalMap + val keyClass = testCase.keyclazz + val valueClass = testCase.valueclazz + + val bytes = ZBytes.serialize(originalMap).getOrThrow() + + val deserializedMap = bytes.deserialize( + clazz = Map::class, + arg1clazz = keyClass, + arg2clazz = valueClass + ).getOrThrow() + + if (keyClass == ByteArray::class && valueClass != ByteArray::class) { + val map1 = originalMap.map { (k, v) -> (k as ByteArray).toList() to v }.toMap() + val map2 = originalMap.map { (k, v) -> (k as ByteArray).toList() to v }.toMap() + assertEquals(map1, map2) + return + } + + if (keyClass != ByteArray::class && valueClass == ByteArray::class) { + val map1 = originalMap.map { (k, v) -> k to (v as ByteArray).toList() }.toMap() + val map2 = originalMap.map { (k, v) -> k to (v as ByteArray).toList() }.toMap() + assertEquals(map1, map2) + return + } + + if (keyClass == ByteArray::class && valueClass == ByteArray::class) { + val map1 = originalMap.map { (k, v) -> (k as ByteArray).toList() to (v as ByteArray).toList() }.toMap() + val map2 = originalMap.map { (k, v) -> (k as ByteArray).toList() to (v as ByteArray).toList() }.toMap() + assertEquals(map1, map2) + return + } + + assertEquals(originalMap, deserializedMap) + } + + @Test + fun deserializationWithMapOfDeserializationFunctionsTest() { + val stringMap = mapOf("key1" to "value1", "key2" to "value2") + val zbytesMap = stringMap.map { (k, v) -> k.into() to v.into() }.toMap() + val zbytesListOfPairs = stringMap.map { (k, v) -> k.into() to v.into() } + val intMap = mapOf(1 to 10, 2 to 20, 3 to 30) + val zbytesList = listOf(1.into(), 2.into(), 3.into()) + + val serializedBytes = serializeZBytesMap(zbytesMap) + + val customDeserializers = mapOf( + typeOf>() to ::deserializeIntoZBytesMap, + typeOf>() to ::deserializeIntoStringMap, + typeOf>() to ::deserializeIntoIntMap, + typeOf>() to ::deserializeIntoZBytesList, + typeOf>>() to ::deserializeIntoListOfPairs, + ) + + val deserializedMap = serializedBytes.deserialize>(customDeserializers).getOrThrow() + assertEquals(zbytesMap, deserializedMap) + + val deserializedMap2 = serializedBytes.deserialize>(customDeserializers).getOrThrow() + assertEquals(stringMap, deserializedMap2) + + val intMapBytes = serializeIntoIntMap(intMap) + val deserializedMap3 = intMapBytes.deserialize>(customDeserializers).getOrThrow() + assertEquals(intMap, deserializedMap3) + + val serializedZBytesList = serializeZBytesList(zbytesList) + val deserializedList = serializedZBytesList.deserialize>(customDeserializers).getOrThrow() + assertEquals(zbytesList, deserializedList) + + val serializedZBytesPairList = serializeZBytesMap(zbytesListOfPairs.toMap()) + val deserializedZBytesPairList = + serializedZBytesPairList.deserialize>>(customDeserializers).getOrThrow() + assertEquals(zbytesListOfPairs, deserializedZBytesPairList) + } + + /** + * A series of tests to verify the correct functioning of the [ZBytes.deserialize] function. + * + * The [ZBytes.deserialize] function with reification can not be tested in a parametrized fashion because + * it uses reified parameters which causes the testing framework (designed for Java) to fail to properly + * set up the tests. + */ + @Test + fun serializationAndDeserializationWithReification() { + /*********************************************** + * Standard serialization and deserialization. * + ***********************************************/ + + /** Numeric: byte, short, int, float, double */ + val intInput = 1234 + var payload = ZBytes.from(intInput) + val intOutput = payload.deserialize().getOrThrow() + assertEquals(intInput, intOutput) + + // Another example with float + val floatInput = 3.1415f + payload = ZBytes.from(floatInput) + val floatOutput = payload.deserialize().getOrThrow() + assertEquals(floatInput, floatOutput) + + /** String serialization and deserialization. */ + val stringInput = "example" + payload = ZBytes.from(stringInput) + val stringOutput = payload.deserialize().getOrThrow() + assertEquals(stringInput, stringOutput) + + /** ByteArray serialization and deserialization. */ + val byteArrayInput = "example".toByteArray() + payload = ZBytes.from(byteArrayInput) // Equivalent to `byteArrayInput.into()` + val byteArrayOutput = payload.deserialize().getOrThrow() + assertTrue(byteArrayInput.contentEquals(byteArrayOutput)) + + val inputList = listOf("sample1", "sample2", "sample3") + payload = ZBytes.serialize(inputList).getOrThrow() + val outputList = payload.deserialize>().getOrThrow() + assertEquals(inputList, outputList) + + val inputListZBytes = inputList.map { value -> value.into() } + payload = ZBytes.serialize(inputListZBytes).getOrThrow() + val outputListZBytes = payload.deserialize>().getOrThrow() + assertEquals(inputListZBytes, outputListZBytes) + + val inputListByteArray = inputList.map { value -> value.toByteArray() } + payload = ZBytes.serialize(inputListByteArray).getOrThrow() + val outputListByteArray = payload.deserialize>().getOrThrow() + assertTrue(compareByteArrayLists(inputListByteArray, outputListByteArray)) + + val inputMap = mapOf("key1" to "value1", "key2" to "value2", "key3" to "value3") + payload = ZBytes.serialize(inputMap).getOrThrow() + val outputMap = payload.deserialize>().getOrThrow() + assertEquals(inputMap, outputMap) + + val combinedInputMap = mapOf("key1" to ZBytes.from("zbytes1"), "key2" to ZBytes.from("zbytes2")) + payload = ZBytes.serialize(combinedInputMap).getOrThrow() + val combinedOutputMap = payload.deserialize>().getOrThrow() + assertEquals(combinedInputMap, combinedOutputMap) + + /********************************************* + * Custom serialization and deserialization. * + *********************************************/ + + val inputMyZBytes = MyZBytes("example") + payload = ZBytes.serialize(inputMyZBytes).getOrThrow() + val outputMyZBytes = payload.deserialize().getOrThrow() + assertEquals(inputMyZBytes, outputMyZBytes) + + /** List of MyZBytes. */ + val inputListMyZBytes = inputList.map { value -> MyZBytes(value) } + payload = ZBytes.serialize>(inputListMyZBytes).getOrThrow() + val outputListMyZBytes = payload.deserialize>().getOrThrow() + assertEquals(inputListMyZBytes, outputListMyZBytes) + + /** Map of MyZBytes. */ + val inputMapMyZBytes = inputMap.map { (k, v) -> MyZBytes(k) to MyZBytes(v)}.toMap() + payload = ZBytes.serialize>(inputMapMyZBytes).getOrThrow() + val outputMapMyZBytes = payload.deserialize>().getOrThrow() + assertEquals(inputMapMyZBytes, outputMapMyZBytes) + + val combinedMap = mapOf(MyZBytes("foo") to 1, MyZBytes("bar") to 2) + payload = ZBytes.serialize>(combinedMap).getOrThrow() + val combinedOutput = payload.deserialize>().getOrThrow() + assertEquals(combinedMap, combinedOutput) + + /** + * Providing a map of deserializers. + */ + val fooMap = mapOf(Foo("foo1") to Foo("bar1"), Foo("foo2") to Foo("bar2")) + val fooMapSerialized = ZBytes.from(serializeFooMap(fooMap)) + val deserializersMap = mapOf(typeOf>() to ::deserializeFooMap) + val deserializedFooMap = fooMapSerialized.deserialize>(deserializersMap).getOrThrow() + assertEquals(fooMap, deserializedFooMap) + } + + /***************** + * Testing utils * + *****************/ + + /** + * Custom class for the tests. The purpose of this class is to test + * the proper functioning of the serialization and deserialization for + * a class implementing the [Serializable] and the [Deserializable] interface. + */ + class MyZBytes(val content: String) : Serializable, Deserializable { + + override fun into(): ZBytes = content.into() + + companion object : Deserializable.From { + override fun from(zbytes: ZBytes): MyZBytes { + return MyZBytes(zbytes.toString()) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MyZBytes + + return content == other.content + } + + override fun hashCode(): Int { + return content.hashCode() + } + } + + /** Example class for the deserialization map examples. */ + class Foo(val content: String) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Foo + + return content == other.content + } + + override fun hashCode(): Int { + return content.hashCode() + } + } + + private fun compareByteArrayLists(list1: List, list2: List): Boolean { + if (list1.size != list2.size) { + return false + } + for (i in list1.indices) { + if (!list1[i].contentEquals(list2[i])) { + return false + } + } + return true + } + + + /********************************************************************************** + * Serializers and deserializers for testing the functionality of deserialization * + * with deserializer functions. * + **********************************************************************************/ + + private fun serializeFooMap(testMap: Map): ByteArray { + return testMap.map { + val key = it.key.content.toByteArray() + val keyLength = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(key.size).array() + val value = it.value.content.toByteArray() + val valueLength = + ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(value.size).array() + keyLength + key + valueLength + value + }.reduce { acc, bytes -> acc + bytes } + } + + private fun deserializeFooMap(serializedMap: ZBytes): Map { + var idx = 0 + var sliceSize: Int + val bytes = serializedMap.toByteArray() + val decodedMap = mutableMapOf() + while (idx < bytes.size) { + sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) + .order(ByteOrder.LITTLE_ENDIAN).int + idx += Int.SIZE_BYTES + + val key = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + sliceSize = ByteBuffer.wrap(bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))).order( + ByteOrder.LITTLE_ENDIAN + ).int + idx += Int.SIZE_BYTES + + val value = bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + decodedMap[Foo(key.decodeToString())] = Foo(value.decodeToString()) + } + return decodedMap + } + + private fun serializeZBytesMap(testMap: Map): ZBytes { + return testMap.map { + val key = it.key.bytes + val keyLength = ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(key.size).array() + val value = it.value.bytes + val valueLength = + ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(value.size).array() + keyLength + key + valueLength + value + }.reduce { acc, bytes -> acc + bytes }.into() + } + + private fun deserializeIntoZBytesMap(serializedMap: ZBytes): Map { + var idx = 0 + var sliceSize: Int + val decodedMap = mutableMapOf() + while (idx < serializedMap.bytes.size) { + sliceSize = ByteBuffer.wrap(serializedMap.bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) + .order(ByteOrder.LITTLE_ENDIAN).int + idx += Int.SIZE_BYTES + + val key = serializedMap.bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + sliceSize = ByteBuffer.wrap(serializedMap.bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))).order( + ByteOrder.LITTLE_ENDIAN + ).int + idx += Int.SIZE_BYTES + + val value = serializedMap.bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + decodedMap[key.into()] = value.into() + } + return decodedMap + } + + private fun serializeIntoIntMap(intMap: Map): ZBytes { + val zBytesMap = intMap.map { (k, v) -> k.into() to v.into() }.toMap() + return serializeZBytesMap(zBytesMap) + } + + private fun deserializeIntoStringMap(serializerMap: ZBytes): Map { + return deserializeIntoZBytesMap(serializerMap).map { (k, v) -> k.toString() to v.toString() }.toMap() + } + + private fun deserializeIntoIntMap(serializerMap: ZBytes): Map { + return deserializeIntoZBytesMap(serializerMap).map { (k, v) -> + k.deserialize().getOrThrow() to v.deserialize().getOrThrow() + }.toMap() + } + + private fun serializeZBytesList(list: List): ZBytes { + return list.map { + val item = it.bytes + val itemLength = + ByteBuffer.allocate(Int.SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN).putInt(item.size).array() + itemLength + item + }.reduce { acc, bytes -> acc + bytes }.into() + } + + private fun deserializeIntoZBytesList(serializedList: ZBytes): List { + var idx = 0 + var sliceSize: Int + val decodedList = mutableListOf() + while (idx < serializedList.bytes.size) { + sliceSize = ByteBuffer.wrap(serializedList.bytes.sliceArray(IntRange(idx, idx + Int.SIZE_BYTES - 1))) + .order(ByteOrder.LITTLE_ENDIAN).int + idx += Int.SIZE_BYTES + + val item = serializedList.bytes.sliceArray(IntRange(idx, idx + sliceSize - 1)) + idx += sliceSize + + decodedList.add(item.into()) + } + return decodedList + } + + private fun deserializeIntoListOfPairs(serializedList: ZBytes): List> { + return deserializeIntoZBytesMap(serializedList).map { (k, v) -> k to v } + } +} \ No newline at end of file diff --git a/zenoh-kotlin/src/jvmMain/kotlin/io/zenoh/Zenoh.kt b/zenoh-kotlin/src/jvmMain/kotlin/io/zenoh/Zenoh.kt index d6b7dd9b4..7f1c08816 100644 --- a/zenoh-kotlin/src/jvmMain/kotlin/io/zenoh/Zenoh.kt +++ b/zenoh-kotlin/src/jvmMain/kotlin/io/zenoh/Zenoh.kt @@ -15,155 +15,146 @@ package io.zenoh import java.io.File +import java.io.FileInputStream import java.io.FileOutputStream import java.io.InputStream -import java.io.FileInputStream import java.util.zip.ZipInputStream /** * Static singleton class to load the Zenoh native library once and only once, as well as the logger in function of the * log level configuration. */ -internal actual class Zenoh private actual constructor() { - - actual companion object { - private const val ZENOH_LIB_NAME = "zenoh_jni" - private const val ZENOH_LOGS_PROPERTY = "zenoh.logger" +internal actual object ZenohLoad { + private const val ZENOH_LIB_NAME = "zenoh_jni" + private const val ZENOH_LOGS_PROPERTY = "zenoh.logger" - private var instance: Zenoh? = null + init { + // Try first to load the local native library for cases in which the module was built locally, + // otherwise try to load from the JAR. + if (tryLoadingLocalLibrary().isFailure) { + val target = determineTarget().getOrThrow() + tryLoadingLibraryFromJarPackage(target).getOrThrow() + } - actual fun load() { - instance ?: Zenoh().also { instance = it } + val logLevel = System.getProperty(ZENOH_LOGS_PROPERTY) + if (logLevel != null) { + Logger.start(logLevel) } + } - /** - * Determine target - * - * Determines the [Target] corresponding to the machine on top of which the native code will run. - * - * @return A result with the target. - */ - private fun determineTarget(): Result = runCatching { - val osName = System.getProperty("os.name").lowercase() - val osArch = System.getProperty("os.arch") - - val target = when { - osName.contains("win") -> when { - osArch.contains("x86_64") || osArch.contains("amd64") -> Target.WINDOWS_X86_64_MSVC - else -> throw UnsupportedOperationException("Unsupported architecture: $osArch") - } - - osName.contains("mac") -> when { - osArch.contains("x86_64") || osArch.contains("amd64") -> Target.APPLE_X86_64 - osArch.contains("aarch64") -> Target.APPLE_AARCH64 - else -> throw UnsupportedOperationException("Unsupported architecture: $osArch") - } - - osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> when { - osArch.contains("x86_64") || osArch.contains("amd64") -> Target.LINUX_X86_64 - osArch.contains("aarch64") -> Target.LINUX_AARCH64 - else -> throw UnsupportedOperationException("Unsupported architecture: $osArch") - } - - else -> throw UnsupportedOperationException("Unsupported platform: $osName") + /** + * Determine target + * + * Determines the [Target] corresponding to the machine on top of which the native code will run. + * + * @return A result with the target. + */ + private fun determineTarget(): Result = runCatching { + val osName = System.getProperty("os.name").lowercase() + val osArch = System.getProperty("os.arch") + + val target = when { + osName.contains("win") -> when { + osArch.contains("x86_64") || osArch.contains("amd64") -> Target.WINDOWS_X86_64_MSVC + else -> throw UnsupportedOperationException("Unsupported architecture: $osArch") } - return Result.success(target) - } - /** - * Unzip library. - * - * The Zenoh libraries are stored within the JAR as compressed ZIP files. - * The location of the zipped files is expected to be under target/target.zip. - * It is expected that the zip file only contains the compressed library. - * - * The uncompressed library will be stored temporarily and deleted on exit. - * - * @param compressedLib Input stream pointing to the compressed library. - * @return A result with the uncompressed library file. - */ - private fun unzipLibrary(compressedLib: InputStream): Result = runCatching { - val zipInputStream = ZipInputStream(compressedLib) - val buffer = ByteArray(1024) - val zipEntry = zipInputStream.nextEntry - - val library = File.createTempFile(zipEntry!!.name, ".tmp") - library.deleteOnExit() - - val parent = library.parentFile - if (!parent.exists()) { - parent.mkdirs() + osName.contains("mac") -> when { + osArch.contains("x86_64") || osArch.contains("amd64") -> Target.APPLE_X86_64 + osArch.contains("aarch64") -> Target.APPLE_AARCH64 + else -> throw UnsupportedOperationException("Unsupported architecture: $osArch") } - val fileOutputStream = FileOutputStream(library) - var len: Int - while (zipInputStream.read(buffer).also { len = it } > 0) { - fileOutputStream.write(buffer, 0, len) + osName.contains("nix") || osName.contains("nux") || osName.contains("aix") -> when { + osArch.contains("x86_64") || osArch.contains("amd64") -> Target.LINUX_X86_64 + osArch.contains("aarch64") -> Target.LINUX_AARCH64 + else -> throw UnsupportedOperationException("Unsupported architecture: $osArch") } - fileOutputStream.close() - zipInputStream.closeEntry() - zipInputStream.close() - return Result.success(library) + else -> throw UnsupportedOperationException("Unsupported platform: $osName") } + return Result.success(target) + } - private fun loadLibraryAsInputStream(target: Target): Result = runCatching { - val libUrl = ClassLoader.getSystemClassLoader().getResourceAsStream("$target/$target.zip")!! - val uncompressedLibFile = unzipLibrary(libUrl) - return Result.success(FileInputStream(uncompressedLibFile.getOrThrow())) + /** + * Unzip library. + * + * The Zenoh libraries are stored within the JAR as compressed ZIP files. + * The location of the zipped files is expected to be under target/target.zip. + * It is expected that the zip file only contains the compressed library. + * + * The uncompressed library will be stored temporarily and deleted on exit. + * + * @param compressedLib Input stream pointing to the compressed library. + * @return A result with the uncompressed library file. + */ + private fun unzipLibrary(compressedLib: InputStream): Result = runCatching { + val zipInputStream = ZipInputStream(compressedLib) + val buffer = ByteArray(1024) + val zipEntry = zipInputStream.nextEntry + + val library = File.createTempFile(zipEntry!!.name, ".tmp") + library.deleteOnExit() + + val parent = library.parentFile + if (!parent.exists()) { + parent.mkdirs() } - @Suppress("UnsafeDynamicallyLoadedCode") - private fun loadZenohJNI(inputStream: InputStream) { - val tempLib = File.createTempFile("tempLib", ".tmp") - tempLib.deleteOnExit() + val fileOutputStream = FileOutputStream(library) + var len: Int + while (zipInputStream.read(buffer).also { len = it } > 0) { + fileOutputStream.write(buffer, 0, len) + } + fileOutputStream.close() - FileOutputStream(tempLib).use { output -> - inputStream.copyTo(output) - } + zipInputStream.closeEntry() + zipInputStream.close() + return Result.success(library) + } - System.load(tempLib.absolutePath) - } + private fun loadLibraryAsInputStream(target: Target): Result = runCatching { + val libUrl = ClassLoader.getSystemClassLoader().getResourceAsStream("$target/$target.zip")!! + val uncompressedLibFile = unzipLibrary(libUrl) + return Result.success(FileInputStream(uncompressedLibFile.getOrThrow())) + } - /** - * Load library from jar package. - * - * Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar. - * - * @param target - */ - private fun tryLoadingLibraryFromJarPackage(target: Target): Result = runCatching { - val lib: Result = loadLibraryAsInputStream(target) - lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") } - } + @Suppress("UnsafeDynamicallyLoadedCode") + private fun loadZenohJNI(inputStream: InputStream) { + val tempLib = File.createTempFile("tempLib", ".tmp") + tempLib.deleteOnExit() - /** - * Try loading local library. - * - * This function aims to load the default library that is usually included when building the zenoh kotlin library - * locally. - */ - private fun tryLoadingLocalLibrary(): Result = runCatching { - val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME) - if (lib != null) { - loadZenohJNI(lib) - } else { - throw Exception("Unable to load local Zenoh JNI.") - } + FileOutputStream(tempLib).use { output -> + inputStream.copyTo(output) } + + System.load(tempLib.absolutePath) } - init { - // Try first to load the local native library for cases in which the module was built locally, - // otherwise try to load from the JAR. - if (tryLoadingLocalLibrary().isFailure) { - val target = determineTarget().getOrThrow() - tryLoadingLibraryFromJarPackage(target).getOrThrow() - } + /** + * Load library from jar package. + * + * Attempts to load the library corresponding to the `target` specified from the zenoh kotlin jar. + * + * @param target + */ + private fun tryLoadingLibraryFromJarPackage(target: Target): Result = runCatching { + val lib: Result = loadLibraryAsInputStream(target) + lib.onSuccess { loadZenohJNI(it) }.onFailure { throw Exception("Unable to load Zenoh JNI: $it") } + } - val logLevel = System.getProperty(ZENOH_LOGS_PROPERTY) - if (logLevel != null) { - Logger.start(logLevel) + /** + * Try loading local library. + * + * This function aims to load the default library that is usually included when building the zenoh kotlin library + * locally. + */ + private fun tryLoadingLocalLibrary(): Result = runCatching { + val lib = ClassLoader.getSystemClassLoader().findLibraryStream(ZENOH_LIB_NAME) + if (lib != null) { + loadZenohJNI(lib) + } else { + throw Exception("Unable to load local Zenoh JNI.") } } }