diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28a2fab3..ef67473a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,14 +31,14 @@ jobs: uses: actions/checkout@v4 with: repository: ZettaScaleLabs/zenoh-flat-jni - ref: c82656b2f41093d04921ead50516219ae74c7e96 + ref: 9bc155b3272c8c905bf95614cb7eb7e6d656c72f path: zenoh-flat-jni - name: Check out zenoh-flat uses: actions/checkout@v4 with: repository: ZettaScaleLabs/zenoh-flat - ref: 7b151d7808583ed15884fada2fd40d5db387c982 + ref: 6d22091d75912be1c14330b0f78fa1d4bf258ea3 path: zenoh-flat - name: Use prebindgen from GitHub main diff --git a/ZENOH_FLAT_TRANSITION.md b/ZENOH_FLAT_TRANSITION.md index e03f18d5..492d969a 100644 --- a/ZENOH_FLAT_TRANSITION.md +++ b/ZENOH_FLAT_TRANSITION.md @@ -25,7 +25,8 @@ zenoh (Rust) | PR | Scope | Status | | --- | --- | --- | -| [#481](https://github.com/eclipse-zenoh/zenoh-java/pull/481) | Use receiver-style zenoh-flat-jni bindings (generated Session/Query/Publisher methods, split key-expr overloads, de-prefixed callback names); +61–83% subscriber throughput | CI green | +| [#481](https://github.com/eclipse-zenoh/zenoh-java/pull/481) | Use receiver-style zenoh-flat-jni bindings (generated Session/Query/Publisher methods, split key-expr overloads, de-prefixed callback names); +61–83% subscriber throughput | merged | +| [#484](https://github.com/eclipse-zenoh/zenoh-java/pull/484) | `Encoding`/`ZenohId.toString` become pure JVM values (fixes a per-message native leak); the conversion logic lives in the **shared tier** (`EncodingCodec`/`zidString` in zenoh-flat-jni, correspondence-tested against native, reusable by zenoh-kotlin) with only a thin facade here; publisher **default encoding set natively at declare** (plain puts cross no encoding data). Pairs with [zenoh-flat-jni#4](https://github.com/ZettaScaleLabs/zenoh-flat-jni/pull/4), [zenoh-flat#3](https://github.com/ZettaScaleLabs/zenoh-flat/pull/3) (merged), [prebindgen#80](https://github.com/milyin/prebindgen/pull/80) (merged) | in progress | Companion PRs in the upstream repos are coordinated per constituent PR (e.g. [ZettaScaleLabs/zenoh-flat-jni#3](https://github.com/ZettaScaleLabs/zenoh-flat-jni/pull/3) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/FlatCallbacks.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/FlatCallbacks.kt index 63d401e3..4e35a7cf 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/FlatCallbacks.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/FlatCallbacks.kt @@ -39,14 +39,14 @@ import io.zenoh.scouting.Hello internal fun sampleCallbackOf( f: (Sample) -> Unit ): io.zenoh.jni.sample.SampleCallback = - io.zenoh.jni.sample.SampleCallback { keH, payloadH, encH, encId, kindInt, ntp64, express, prioInt, ccInt, attachH, reliabilityInt, sourceZid, sourceEid, sourceSn -> - f(Sample.fromParts(keH, payloadH, encH, encId, kindInt, ntp64, express, prioInt, ccInt, attachH, reliabilityInt, sourceZid, sourceEid, sourceSn)) + io.zenoh.jni.sample.SampleCallback { keH, payloadH, encId, encSchema, kindInt, ntp64, express, prioInt, ccInt, attachH, reliabilityInt, sourceZid, sourceEid, sourceSn -> + f(Sample.fromParts(keH, payloadH, encId, encSchema, kindInt, ntp64, express, prioInt, ccInt, attachH, reliabilityInt, sourceZid, sourceEid, sourceSn)) } internal fun queryCallbackOf( f: (Query) -> Unit ): io.zenoh.jni.query.QueryCallback = - io.zenoh.jni.query.QueryCallback { keH, parameters, payloadH, encH, encId, attachH, acceptsReplies, zq -> + io.zenoh.jni.query.QueryCallback { keH, parameters, payloadH, encId, encSchema, attachH, acceptsReplies, zq -> // The decomposed leaves — including the cloned `keH` key-expr handle and // the owned `zq` query handle — are folded into the SDK [Query]. Unlike // the decomposed read-only types (Sample/Hello), the query OWNS `zq` and @@ -54,25 +54,25 @@ internal fun queryCallbackOf( // on a channel by a queue handler) and replied to later. The native query // is dropped when it is replied to (see [Query.reply]) or when [Query] is // closed — that drop is what finalizes the querier's get. - f(Query.fromParts(keH, parameters, payloadH, encH, encId, attachH, acceptsReplies, zq)) + f(Query.fromParts(keH, parameters, payloadH, encId, encSchema, attachH, acceptsReplies, zq)) } internal fun replyCallbackOf( f: (Reply) -> Unit ): io.zenoh.jni.query.ReplyCallback = - io.zenoh.jni.query.ReplyCallback { zid, eid, isOk, keH, payloadH, encH, encId, kindInt, ntp64, express, prioInt, ccInt, attachH, reliabilityInt, sourceZid, sourceEid, sourceSn, errPayloadH, errEncH, errEncId -> + io.zenoh.jni.query.ReplyCallback { zid, eid, isOk, keH, payloadH, encId, encSchema, kindInt, ntp64, express, prioInt, ccInt, attachH, reliabilityInt, sourceZid, sourceEid, sourceSn, errPayloadH, errEncId, errEncSchema -> val replierId = zid?.let { EntityGlobalId(ZenohId(it), eid.toUInt()) } f( if (isOk) { Reply.Success( replierId, - Sample.fromParts(keH!!, payloadH!!, encH!!, encId!!, kindInt!!, ntp64, express!!, prioInt!!, ccInt!!, attachH, reliabilityInt!!, sourceZid, sourceEid!!, sourceSn!!) + Sample.fromParts(keH!!, payloadH!!, encId!!, encSchema, kindInt!!, ntp64, express!!, prioInt!!, ccInt!!, attachH, reliabilityInt!!, sourceZid, sourceEid!!, sourceSn!!) ) } else { Reply.Error( replierId, ZBytes.fromHandle(errPayloadH!!), - errEncH?.let { Encoding.fromParts(it, errEncId!!) } ?: Encoding.defaultEncoding() + errEncId?.let { Encoding(it, errEncSchema) } ?: Encoding.defaultEncoding() ) } ) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt index 10473468..846dec59 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/Session.kt @@ -587,8 +587,14 @@ class Session private constructor(private val config: Config) : AutoCloseable { internal fun resolvePublisher(keyExpr: KeyExpr, options: PublisherOptions): Publisher { val zSession = zSession ?: throw sessionClosedException val publisher = run { + // The publisher's default encoding is set NATIVELY here, once — + // plain puts then cross no encoding data at all (see Publisher). + val enc = options.encoding val zPublisher = zSession.declarePublisher( keyExpr.cloneFlat(), + true, + enc.id, + enc.schema, options.congestionControl.jni, options.priority.jni, options.express, @@ -722,7 +728,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { val zSession = zSession ?: throw sessionClosedException return run { val sel = selector.into() - val enc = options.encoding ?: Encoding.defaultEncoding() + val enc = options.encoding zSession.get( sel.keyExpr.flat, sel.parameters?.toString(), @@ -734,9 +740,9 @@ class Session private constructor(private val config: Config) : AutoCloseable { options.qos.priority.jni, options.qos.express, options.payload?.into()?.bytes, - true, - enc.idForWire(), - enc.schemaForWire(), + enc != null, + enc?.id ?: 0, + enc?.schema, options.attachment?.into()?.bytes, replyCallbackOf { handler.handle(it) }, { handler.onClose() }, @@ -755,7 +761,7 @@ class Session private constructor(private val config: Config) : AutoCloseable { val zSession = zSession ?: throw sessionClosedException run { val sel = selector.into() - val enc = options.encoding ?: Encoding.defaultEncoding() + val enc = options.encoding zSession.get( sel.keyExpr.flat, sel.parameters?.toString(), @@ -767,9 +773,9 @@ class Session private constructor(private val config: Config) : AutoCloseable { options.qos.priority.jni, options.qos.express, options.payload?.into()?.bytes, - true, - enc.idForWire(), - enc.schemaForWire(), + enc != null, + enc?.id ?: 0, + enc?.schema, options.attachment?.into()?.bytes, replyCallbackOf { callback.run(it) }, { }, @@ -782,13 +788,13 @@ class Session private constructor(private val config: Config) : AutoCloseable { internal fun resolvePut(keyExpr: KeyExpr, payload: IntoZBytes, putOptions: PutOptions) { val zSession = zSession ?: return run { - val enc = putOptions.encoding ?: Encoding.defaultEncoding() + val enc = putOptions.encoding zSession.put( keyExpr.flat, payload.into().bytes, - true, - enc.idForWire(), - enc.schemaForWire(), + enc != null, + enc?.id ?: 0, + enc?.schema, putOptions.congestionControl.jni, putOptions.priority.jni, putOptions.express, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt index affd91ed..dfa0e19c 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/bytes/Encoding.kt @@ -14,114 +14,7 @@ package io.zenoh.bytes -import io.zenoh.exceptions.throwZError0 -import io.zenoh.jni.bytes.ENCODING_APPLICATION_CBOR -import io.zenoh.jni.bytes.ENCODING_APPLICATION_CBOR_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_CDR -import io.zenoh.jni.bytes.ENCODING_APPLICATION_CDR_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_COAP_PAYLOAD -import io.zenoh.jni.bytes.ENCODING_APPLICATION_COAP_PAYLOAD_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JAVA_SERIALIZED_OBJECT -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JAVA_SERIALIZED_OBJECT_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSON -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSONPATH -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSONPATH_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSON_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSON_PATCH_JSON -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSON_PATCH_JSON_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSON_SEQ -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JSON_SEQ_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JWT -import io.zenoh.jni.bytes.ENCODING_APPLICATION_JWT_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_MP4 -import io.zenoh.jni.bytes.ENCODING_APPLICATION_MP4_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_OCTET_STREAM -import io.zenoh.jni.bytes.ENCODING_APPLICATION_OCTET_STREAM_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_OPENMETRICS_TEXT -import io.zenoh.jni.bytes.ENCODING_APPLICATION_OPENMETRICS_TEXT_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_PROTOBUF -import io.zenoh.jni.bytes.ENCODING_APPLICATION_PROTOBUF_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_PYTHON_SERIALIZED_OBJECT -import io.zenoh.jni.bytes.ENCODING_APPLICATION_PYTHON_SERIALIZED_OBJECT_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_SOAP_XML -import io.zenoh.jni.bytes.ENCODING_APPLICATION_SOAP_XML_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_SQL -import io.zenoh.jni.bytes.ENCODING_APPLICATION_SQL_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_XML -import io.zenoh.jni.bytes.ENCODING_APPLICATION_XML_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_X_WWW_FORM_URLENCODED -import io.zenoh.jni.bytes.ENCODING_APPLICATION_X_WWW_FORM_URLENCODED_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_YAML -import io.zenoh.jni.bytes.ENCODING_APPLICATION_YAML_ID -import io.zenoh.jni.bytes.ENCODING_APPLICATION_YANG -import io.zenoh.jni.bytes.ENCODING_APPLICATION_YANG_ID -import io.zenoh.jni.bytes.ENCODING_AUDIO_AAC -import io.zenoh.jni.bytes.ENCODING_AUDIO_AAC_ID -import io.zenoh.jni.bytes.ENCODING_AUDIO_FLAC -import io.zenoh.jni.bytes.ENCODING_AUDIO_FLAC_ID -import io.zenoh.jni.bytes.ENCODING_AUDIO_MP4 -import io.zenoh.jni.bytes.ENCODING_AUDIO_MP4_ID -import io.zenoh.jni.bytes.ENCODING_AUDIO_OGG -import io.zenoh.jni.bytes.ENCODING_AUDIO_OGG_ID -import io.zenoh.jni.bytes.ENCODING_AUDIO_VORBIS -import io.zenoh.jni.bytes.ENCODING_AUDIO_VORBIS_ID -import io.zenoh.jni.bytes.ENCODING_IMAGE_BMP -import io.zenoh.jni.bytes.ENCODING_IMAGE_BMP_ID -import io.zenoh.jni.bytes.ENCODING_IMAGE_GIF -import io.zenoh.jni.bytes.ENCODING_IMAGE_GIF_ID -import io.zenoh.jni.bytes.ENCODING_IMAGE_JPEG -import io.zenoh.jni.bytes.ENCODING_IMAGE_JPEG_ID -import io.zenoh.jni.bytes.ENCODING_IMAGE_PNG -import io.zenoh.jni.bytes.ENCODING_IMAGE_PNG_ID -import io.zenoh.jni.bytes.ENCODING_IMAGE_WEBP -import io.zenoh.jni.bytes.ENCODING_IMAGE_WEBP_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_CSS -import io.zenoh.jni.bytes.ENCODING_TEXT_CSS_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_CSV -import io.zenoh.jni.bytes.ENCODING_TEXT_CSV_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_HTML -import io.zenoh.jni.bytes.ENCODING_TEXT_HTML_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_JAVASCRIPT -import io.zenoh.jni.bytes.ENCODING_TEXT_JAVASCRIPT_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_JSON -import io.zenoh.jni.bytes.ENCODING_TEXT_JSON5 -import io.zenoh.jni.bytes.ENCODING_TEXT_JSON5_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_JSON_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_MARKDOWN -import io.zenoh.jni.bytes.ENCODING_TEXT_MARKDOWN_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_PLAIN -import io.zenoh.jni.bytes.ENCODING_TEXT_PLAIN_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_XML -import io.zenoh.jni.bytes.ENCODING_TEXT_XML_ID -import io.zenoh.jni.bytes.ENCODING_TEXT_YAML -import io.zenoh.jni.bytes.ENCODING_TEXT_YAML_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_H261 -import io.zenoh.jni.bytes.ENCODING_VIDEO_H261_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_H263 -import io.zenoh.jni.bytes.ENCODING_VIDEO_H263_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_H264 -import io.zenoh.jni.bytes.ENCODING_VIDEO_H264_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_H265 -import io.zenoh.jni.bytes.ENCODING_VIDEO_H265_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_H266 -import io.zenoh.jni.bytes.ENCODING_VIDEO_H266_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_MP4 -import io.zenoh.jni.bytes.ENCODING_VIDEO_MP4_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_OGG -import io.zenoh.jni.bytes.ENCODING_VIDEO_OGG_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_RAW -import io.zenoh.jni.bytes.ENCODING_VIDEO_RAW_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_VP8 -import io.zenoh.jni.bytes.ENCODING_VIDEO_VP8_ID -import io.zenoh.jni.bytes.ENCODING_VIDEO_VP9 -import io.zenoh.jni.bytes.ENCODING_VIDEO_VP9_ID -import io.zenoh.jni.bytes.ENCODING_ZENOH_BYTES -import io.zenoh.jni.bytes.ENCODING_ZENOH_BYTES_ID -import io.zenoh.jni.bytes.ENCODING_ZENOH_SERIALIZED -import io.zenoh.jni.bytes.ENCODING_ZENOH_SERIALIZED_ID -import io.zenoh.jni.bytes.ENCODING_ZENOH_STRING -import io.zenoh.jni.bytes.ENCODING_ZENOH_STRING_ID -import io.zenoh.jni.bytes.Encoding as JniEncoding +import io.zenoh.jni.bytes.* /** * Default encoding values used by Zenoh. @@ -133,157 +26,72 @@ import io.zenoh.jni.bytes.Encoding as JniEncoding * * A set of associated constants are provided to cover the most common encodings for user convenience. * - * Internally the encoding is kept as its canonical textual form ([repr], e.g. - * `"text/plain"` or `"text/plain;utf-8"`); a native `ZEncoding` handle is built - * on demand for each native crossing (the raw `z_*` API takes the encoding by - * reference, so the transient handle is closed by the caller after the call). + * An encoding **is** its decomposed pair `(id, schema)` — Zenoh's own + * representation. This class is a plain immutable value; all conversion logic + * (the id↔name table and Zenoh's parse/render rules) lives in the shared + * bindings tier ([EncodingCodec]), verified against the native implementation + * by `EncodingCorrespondenceTest`. No native handle, no native calls, nothing + * to close. */ -class Encoding private constructor( - private var reprLazy: String?, - internal val id: Int?, - private val handle: JniEncoding?, +class Encoding internal constructor( + internal val id: Int, + internal val schema: String? = null, ) { - internal constructor(repr: String) : this(repr, null, null) - - private var schemaLazy: String? = null - private var schemaKnown: Boolean = false - private var idCached: Int = id ?: 0 - private var idKnown: Boolean = id != null - - /** - * Ensure the lossless decomposed form `(id, schema)` is cached. Zenoh's - * encoding IS `(id, schema)`, so this loses nothing. A handle-backed - * (received) Encoding already knows its [id]; its schema is read LAZILY - * through the handle on first need. A predefined constant is born fully - * decomposed (repr + id from the generated consts, schema known-absent) and - * never enters this path. A repr-primary Encoding built by [from] derives - * BOTH once from a transient handle built off [repr], then frees it — - * caching pure JVM values and retaining no native handle. The cache is - * reused across every native crossing, so a reused - * encoding (the normal case — a publisher publishes one data type) never - * re-parses its string per call. - */ - private fun ensureDecomposed() { - if (idKnown && schemaKnown) return - synchronized(this) { - if (idKnown && schemaKnown) return - if (handle != null) { - if (!schemaKnown) { - schemaLazy = handle.getSchema(throwZError0) - schemaKnown = true - } - } else { - val h = JniEncoding.newFromString(repr, throwZError0) - try { - if (!idKnown) { - idCached = h.getId(throwZError0) - idKnown = true - } - if (!schemaKnown) { - schemaLazy = h.getSchema(throwZError0) - schemaKnown = true - } - } finally { - h.close() - } - } - } - } - - /** - * Encoding id for the OUTBOUND native crossing. Cached once (see - * [ensureDecomposed]) and reused across every put/get/reply — the wire - * carries this cheap primitive instead of a freshly parsed string. - */ - internal fun idForWire(): Int { - ensureDecomposed() - return idCached - } - - /** Optional schema for the OUTBOUND native crossing. Cached once. */ - internal fun schemaForWire(): String? { - ensureDecomposed() - return schemaLazy - } - - /** - * Canonical display string. A handle-backed (received) Encoding - * materializes it LAZILY on first use (toString/equals) via the native - * accessor — received encodings are usually only forwarded or compared by - * id, so the common path never builds the string (forward-extraction rule). - */ - internal val repr: String - get() = reprLazy ?: synchronized(this) { - reprLazy - ?: handle!!.toStr(throwZError0) - .also { reprLazy = it } - } - companion object { - /** - * A predefined constant: its canonical string and wire id come from - * the generated `ENCODING_*` consts (single source of truth in Rust), - * and a predefined encoding never carries a schema — the decomposed - * `(id, schema)` form is fully known up front, so these constants - * never touch native for decomposition (see [ensureDecomposed]). - */ - private fun predefined(repr: String, id: Int): Encoding = - Encoding(repr, id, null).apply { schemaKnown = true } - - @JvmField val ZENOH_BYTES = predefined(ENCODING_ZENOH_BYTES, ENCODING_ZENOH_BYTES_ID) - @JvmField val ZENOH_STRING = predefined(ENCODING_ZENOH_STRING, ENCODING_ZENOH_STRING_ID) - @JvmField val ZENOH_SERIALIZED = predefined(ENCODING_ZENOH_SERIALIZED, ENCODING_ZENOH_SERIALIZED_ID) - @JvmField val APPLICATION_OCTET_STREAM = predefined(ENCODING_APPLICATION_OCTET_STREAM, ENCODING_APPLICATION_OCTET_STREAM_ID) - @JvmField val TEXT_PLAIN = predefined(ENCODING_TEXT_PLAIN, ENCODING_TEXT_PLAIN_ID) - @JvmField val APPLICATION_JSON = predefined(ENCODING_APPLICATION_JSON, ENCODING_APPLICATION_JSON_ID) - @JvmField val TEXT_JSON = predefined(ENCODING_TEXT_JSON, ENCODING_TEXT_JSON_ID) - @JvmField val APPLICATION_CDR = predefined(ENCODING_APPLICATION_CDR, ENCODING_APPLICATION_CDR_ID) - @JvmField val APPLICATION_CBOR = predefined(ENCODING_APPLICATION_CBOR, ENCODING_APPLICATION_CBOR_ID) - @JvmField val APPLICATION_YAML = predefined(ENCODING_APPLICATION_YAML, ENCODING_APPLICATION_YAML_ID) - @JvmField val TEXT_YAML = predefined(ENCODING_TEXT_YAML, ENCODING_TEXT_YAML_ID) - @JvmField val TEXT_JSON5 = predefined(ENCODING_TEXT_JSON5, ENCODING_TEXT_JSON5_ID) - @JvmField val APPLICATION_PYTHON_SERIALIZED_OBJECT = predefined(ENCODING_APPLICATION_PYTHON_SERIALIZED_OBJECT, ENCODING_APPLICATION_PYTHON_SERIALIZED_OBJECT_ID) - @JvmField val APPLICATION_PROTOBUF = predefined(ENCODING_APPLICATION_PROTOBUF, ENCODING_APPLICATION_PROTOBUF_ID) - @JvmField val APPLICATION_JAVA_SERIALIZED_OBJECT = predefined(ENCODING_APPLICATION_JAVA_SERIALIZED_OBJECT, ENCODING_APPLICATION_JAVA_SERIALIZED_OBJECT_ID) - @JvmField val APPLICATION_OPENMETRICS_TEXT = predefined(ENCODING_APPLICATION_OPENMETRICS_TEXT, ENCODING_APPLICATION_OPENMETRICS_TEXT_ID) - @JvmField val IMAGE_PNG = predefined(ENCODING_IMAGE_PNG, ENCODING_IMAGE_PNG_ID) - @JvmField val IMAGE_JPEG = predefined(ENCODING_IMAGE_JPEG, ENCODING_IMAGE_JPEG_ID) - @JvmField val IMAGE_GIF = predefined(ENCODING_IMAGE_GIF, ENCODING_IMAGE_GIF_ID) - @JvmField val IMAGE_BMP = predefined(ENCODING_IMAGE_BMP, ENCODING_IMAGE_BMP_ID) - @JvmField val IMAGE_WEBP = predefined(ENCODING_IMAGE_WEBP, ENCODING_IMAGE_WEBP_ID) - @JvmField val APPLICATION_XML = predefined(ENCODING_APPLICATION_XML, ENCODING_APPLICATION_XML_ID) - @JvmField val APPLICATION_X_WWW_FORM_URLENCODED = predefined(ENCODING_APPLICATION_X_WWW_FORM_URLENCODED, ENCODING_APPLICATION_X_WWW_FORM_URLENCODED_ID) - @JvmField val TEXT_HTML = predefined(ENCODING_TEXT_HTML, ENCODING_TEXT_HTML_ID) - @JvmField val TEXT_XML = predefined(ENCODING_TEXT_XML, ENCODING_TEXT_XML_ID) - @JvmField val TEXT_CSS = predefined(ENCODING_TEXT_CSS, ENCODING_TEXT_CSS_ID) - @JvmField val TEXT_JAVASCRIPT = predefined(ENCODING_TEXT_JAVASCRIPT, ENCODING_TEXT_JAVASCRIPT_ID) - @JvmField val TEXT_MARKDOWN = predefined(ENCODING_TEXT_MARKDOWN, ENCODING_TEXT_MARKDOWN_ID) - @JvmField val TEXT_CSV = predefined(ENCODING_TEXT_CSV, ENCODING_TEXT_CSV_ID) - @JvmField val APPLICATION_SQL = predefined(ENCODING_APPLICATION_SQL, ENCODING_APPLICATION_SQL_ID) - @JvmField val APPLICATION_COAP_PAYLOAD = predefined(ENCODING_APPLICATION_COAP_PAYLOAD, ENCODING_APPLICATION_COAP_PAYLOAD_ID) - @JvmField val APPLICATION_JSON_PATCH_JSON = predefined(ENCODING_APPLICATION_JSON_PATCH_JSON, ENCODING_APPLICATION_JSON_PATCH_JSON_ID) - @JvmField val APPLICATION_JSON_SEQ = predefined(ENCODING_APPLICATION_JSON_SEQ, ENCODING_APPLICATION_JSON_SEQ_ID) - @JvmField val APPLICATION_JSONPATH = predefined(ENCODING_APPLICATION_JSONPATH, ENCODING_APPLICATION_JSONPATH_ID) - @JvmField val APPLICATION_JWT = predefined(ENCODING_APPLICATION_JWT, ENCODING_APPLICATION_JWT_ID) - @JvmField val APPLICATION_MP4 = predefined(ENCODING_APPLICATION_MP4, ENCODING_APPLICATION_MP4_ID) - @JvmField val APPLICATION_SOAP_XML = predefined(ENCODING_APPLICATION_SOAP_XML, ENCODING_APPLICATION_SOAP_XML_ID) - @JvmField val APPLICATION_YANG = predefined(ENCODING_APPLICATION_YANG, ENCODING_APPLICATION_YANG_ID) - @JvmField val AUDIO_AAC = predefined(ENCODING_AUDIO_AAC, ENCODING_AUDIO_AAC_ID) - @JvmField val AUDIO_FLAC = predefined(ENCODING_AUDIO_FLAC, ENCODING_AUDIO_FLAC_ID) - @JvmField val AUDIO_MP4 = predefined(ENCODING_AUDIO_MP4, ENCODING_AUDIO_MP4_ID) - @JvmField val AUDIO_OGG = predefined(ENCODING_AUDIO_OGG, ENCODING_AUDIO_OGG_ID) - @JvmField val AUDIO_VORBIS = predefined(ENCODING_AUDIO_VORBIS, ENCODING_AUDIO_VORBIS_ID) - @JvmField val VIDEO_H261 = predefined(ENCODING_VIDEO_H261, ENCODING_VIDEO_H261_ID) - @JvmField val VIDEO_H263 = predefined(ENCODING_VIDEO_H263, ENCODING_VIDEO_H263_ID) - @JvmField val VIDEO_H264 = predefined(ENCODING_VIDEO_H264, ENCODING_VIDEO_H264_ID) - @JvmField val VIDEO_H265 = predefined(ENCODING_VIDEO_H265, ENCODING_VIDEO_H265_ID) - @JvmField val VIDEO_H266 = predefined(ENCODING_VIDEO_H266, ENCODING_VIDEO_H266_ID) - @JvmField val VIDEO_MP4 = predefined(ENCODING_VIDEO_MP4, ENCODING_VIDEO_MP4_ID) - @JvmField val VIDEO_OGG = predefined(ENCODING_VIDEO_OGG, ENCODING_VIDEO_OGG_ID) - @JvmField val VIDEO_RAW = predefined(ENCODING_VIDEO_RAW, ENCODING_VIDEO_RAW_ID) - @JvmField val VIDEO_VP8 = predefined(ENCODING_VIDEO_VP8, ENCODING_VIDEO_VP8_ID) - @JvmField val VIDEO_VP9 = predefined(ENCODING_VIDEO_VP9, ENCODING_VIDEO_VP9_ID) + @JvmField val ZENOH_BYTES = Encoding(ENCODING_ZENOH_BYTES_ID) + @JvmField val ZENOH_STRING = Encoding(ENCODING_ZENOH_STRING_ID) + @JvmField val ZENOH_SERIALIZED = Encoding(ENCODING_ZENOH_SERIALIZED_ID) + @JvmField val APPLICATION_OCTET_STREAM = Encoding(ENCODING_APPLICATION_OCTET_STREAM_ID) + @JvmField val TEXT_PLAIN = Encoding(ENCODING_TEXT_PLAIN_ID) + @JvmField val APPLICATION_JSON = Encoding(ENCODING_APPLICATION_JSON_ID) + @JvmField val TEXT_JSON = Encoding(ENCODING_TEXT_JSON_ID) + @JvmField val APPLICATION_CDR = Encoding(ENCODING_APPLICATION_CDR_ID) + @JvmField val APPLICATION_CBOR = Encoding(ENCODING_APPLICATION_CBOR_ID) + @JvmField val APPLICATION_YAML = Encoding(ENCODING_APPLICATION_YAML_ID) + @JvmField val TEXT_YAML = Encoding(ENCODING_TEXT_YAML_ID) + @JvmField val TEXT_JSON5 = Encoding(ENCODING_TEXT_JSON5_ID) + @JvmField val APPLICATION_PYTHON_SERIALIZED_OBJECT = Encoding(ENCODING_APPLICATION_PYTHON_SERIALIZED_OBJECT_ID) + @JvmField val APPLICATION_PROTOBUF = Encoding(ENCODING_APPLICATION_PROTOBUF_ID) + @JvmField val APPLICATION_JAVA_SERIALIZED_OBJECT = Encoding(ENCODING_APPLICATION_JAVA_SERIALIZED_OBJECT_ID) + @JvmField val APPLICATION_OPENMETRICS_TEXT = Encoding(ENCODING_APPLICATION_OPENMETRICS_TEXT_ID) + @JvmField val IMAGE_PNG = Encoding(ENCODING_IMAGE_PNG_ID) + @JvmField val IMAGE_JPEG = Encoding(ENCODING_IMAGE_JPEG_ID) + @JvmField val IMAGE_GIF = Encoding(ENCODING_IMAGE_GIF_ID) + @JvmField val IMAGE_BMP = Encoding(ENCODING_IMAGE_BMP_ID) + @JvmField val IMAGE_WEBP = Encoding(ENCODING_IMAGE_WEBP_ID) + @JvmField val APPLICATION_XML = Encoding(ENCODING_APPLICATION_XML_ID) + @JvmField val APPLICATION_X_WWW_FORM_URLENCODED = Encoding(ENCODING_APPLICATION_X_WWW_FORM_URLENCODED_ID) + @JvmField val TEXT_HTML = Encoding(ENCODING_TEXT_HTML_ID) + @JvmField val TEXT_XML = Encoding(ENCODING_TEXT_XML_ID) + @JvmField val TEXT_CSS = Encoding(ENCODING_TEXT_CSS_ID) + @JvmField val TEXT_JAVASCRIPT = Encoding(ENCODING_TEXT_JAVASCRIPT_ID) + @JvmField val TEXT_MARKDOWN = Encoding(ENCODING_TEXT_MARKDOWN_ID) + @JvmField val TEXT_CSV = Encoding(ENCODING_TEXT_CSV_ID) + @JvmField val APPLICATION_SQL = Encoding(ENCODING_APPLICATION_SQL_ID) + @JvmField val APPLICATION_COAP_PAYLOAD = Encoding(ENCODING_APPLICATION_COAP_PAYLOAD_ID) + @JvmField val APPLICATION_JSON_PATCH_JSON = Encoding(ENCODING_APPLICATION_JSON_PATCH_JSON_ID) + @JvmField val APPLICATION_JSON_SEQ = Encoding(ENCODING_APPLICATION_JSON_SEQ_ID) + @JvmField val APPLICATION_JSONPATH = Encoding(ENCODING_APPLICATION_JSONPATH_ID) + @JvmField val APPLICATION_JWT = Encoding(ENCODING_APPLICATION_JWT_ID) + @JvmField val APPLICATION_MP4 = Encoding(ENCODING_APPLICATION_MP4_ID) + @JvmField val APPLICATION_SOAP_XML = Encoding(ENCODING_APPLICATION_SOAP_XML_ID) + @JvmField val APPLICATION_YANG = Encoding(ENCODING_APPLICATION_YANG_ID) + @JvmField val AUDIO_AAC = Encoding(ENCODING_AUDIO_AAC_ID) + @JvmField val AUDIO_FLAC = Encoding(ENCODING_AUDIO_FLAC_ID) + @JvmField val AUDIO_MP4 = Encoding(ENCODING_AUDIO_MP4_ID) + @JvmField val AUDIO_OGG = Encoding(ENCODING_AUDIO_OGG_ID) + @JvmField val AUDIO_VORBIS = Encoding(ENCODING_AUDIO_VORBIS_ID) + @JvmField val VIDEO_H261 = Encoding(ENCODING_VIDEO_H261_ID) + @JvmField val VIDEO_H263 = Encoding(ENCODING_VIDEO_H263_ID) + @JvmField val VIDEO_H264 = Encoding(ENCODING_VIDEO_H264_ID) + @JvmField val VIDEO_H265 = Encoding(ENCODING_VIDEO_H265_ID) + @JvmField val VIDEO_H266 = Encoding(ENCODING_VIDEO_H266_ID) + @JvmField val VIDEO_MP4 = Encoding(ENCODING_VIDEO_MP4_ID) + @JvmField val VIDEO_OGG = Encoding(ENCODING_VIDEO_OGG_ID) + @JvmField val VIDEO_RAW = Encoding(ENCODING_VIDEO_RAW_ID) + @JvmField val VIDEO_VP8 = Encoding(ENCODING_VIDEO_VP8_ID) + @JvmField val VIDEO_VP9 = Encoding(ENCODING_VIDEO_VP9_ID) /** The default [Encoding] is [ZENOH_BYTES]. */ @JvmStatic fun defaultEncoding() = ZENOH_BYTES @@ -293,61 +101,30 @@ class Encoding private constructor( * `"my_encoding"`). Well-known names resolve to their canonical id; * everything else is preserved as a custom encoding. */ - @JvmStatic fun from(s: String): Encoding = Encoding(s) - - /** - * Decodes a native `ZEncoding` handle into a value [Encoding] and frees - * the handle. Used when an accessor / callback hands back an encoding. - */ - internal fun fromHandle(handle: JniEncoding): Encoding { - try { - return Encoding(handle.toStr(throwZError0)) - } finally { - handle.close() - } + @JvmStatic fun from(s: String): Encoding { + val (id, schema) = EncodingCodec.parse(s) + return Encoding(id, schema) } - - /** Wrap the decomposed `(handle, id)` leaves. Schema and the - * canonical string stay lazy through the handle. */ - internal fun fromParts(encH: JniEncoding, id: Int): Encoding = - Encoding(null, id, encH) } - /** - * Builds a fresh native `ZEncoding` handle from [repr]. The raw `z_*` - * encoding parameters take it **by reference** (not consumed), so the - * caller MUST close the returned handle after the native call. - */ - internal fun toZEncoding(): JniEncoding = - if (handle != null) { - handle.newClone(throwZError0) - } else { - JniEncoding.newFromString(repr, throwZError0) - } - /** * Set a schema to this encoding. Zenoh does not define what a schema is and its semantics is left to the implementer. * E.g. a common schema for `text/plain` encoding is `utf-8`. */ fun withSchema(schema: String): Encoding { - // `withSchema` takes the base encoding flattened to `(id, schema)`; this - // Encoding already exposes that decomposition lazily. - val withSchema = JniEncoding.newWithSchema(idForWire(), schemaForWire(), schema, throwZError0) - try { - return Encoding(withSchema.toStr(throwZError0)) - } finally { - withSchema.close() - } + val (nid, nschema) = EncodingCodec.withSchema(id, this.schema, schema) + return Encoding(nid, nschema) } - override fun toString(): String = repr + /** Canonical textual form. */ + override fun toString(): String = EncodingCodec.render(id, schema) override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Encoding - return repr == other.repr + return id == other.id && schema == other.schema } - override fun hashCode(): Int = repr.hashCode() + override fun hashCode(): Int = 31 * id + (schema?.hashCode() ?: 0) } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt index ff2b59f9..5f3c34ae 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/config/ZenohId.kt @@ -14,8 +14,8 @@ package io.zenoh.config -import io.zenoh.exceptions.throwZError0 import io.zenoh.jni.config.ZenohId as JniZenohId +import io.zenoh.jni.config.zidString import kotlin.math.absoluteValue /** @@ -23,9 +23,12 @@ import kotlin.math.absoluteValue */ data class ZenohId internal constructor(internal val inner: JniZenohId) { - override fun toString(): String { - return inner.toStr(throwZError0) - } + /** + * The standard string form, computed in pure Kotlin by the shared + * bindings tier ([zidString]); correspondence with the native formatter + * is verified by `ZenohIdCorrespondenceTest`. + */ + override fun toString(): String = inner.zidString() override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt index 523b1cae..039dc1ac 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/pubsub/Publisher.kt @@ -80,13 +80,15 @@ class Publisher internal constructor( /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes) { - performPut(payload, encoding, null) + // No per-call encoding: the publisher's default encoding — set + // NATIVELY at declare time — applies, so no encoding data crosses. + performPut(payload, null, null) } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @Throws(ZError::class) fun put(payload: IntoZBytes, options: PutOptions) { - performPut(payload, options.encoding ?: this.encoding, options.attachment) + performPut(payload, options.encoding, options.attachment) } /** Performs a PUT operation on the specified [keyExpr] with the specified [payload]. */ @@ -129,13 +131,15 @@ class Publisher internal constructor( } @Throws(ZError::class) - private fun performPut(payload: IntoZBytes, encoding: Encoding, attachment: IntoZBytes?) { + private fun performPut(payload: IntoZBytes, encoding: Encoding?, attachment: IntoZBytes?) { val p = zPublisher ?: throw publisherNotValid + // `null` encoding = absent: the publisher's default encoding — set + // NATIVELY at declare time — applies, and no encoding data crosses. p.put( payload.into().bytes, - true, - encoding.idForWire(), - encoding.schemaForWire(), + encoding != null, + encoding?.id ?: 0, + encoding?.schema, attachment?.into()?.bytes, throwZError, ) diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt index 37945776..65fde3ca 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Querier.kt @@ -145,13 +145,13 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v private fun resolveGetWithCallback(callback: Callback, options: GetOptions) { val q = zQuerier ?: throw ZError("Querier is not valid.") - val enc = options.encoding ?: Encoding.defaultEncoding() + val enc = options.encoding q.get( options.parameters?.toString(), options.payload?.into()?.bytes, - true, - enc.idForWire(), - enc.schemaForWire(), + enc != null, + enc?.id ?: 0, + enc?.schema, options.attachment?.into()?.bytes, replyCallbackOf { callback.run(it) }, { }, @@ -161,13 +161,13 @@ class Querier internal constructor(val keyExpr: KeyExpr, val qos: QoS, private v private fun resolveGetWithHandler(handler: Handler, options: GetOptions): R { val q = zQuerier ?: throw ZError("Querier is not valid.") - val enc = options.encoding ?: Encoding.defaultEncoding() + val enc = options.encoding q.get( options.parameters?.toString(), options.payload?.into()?.bytes, - true, - enc.idForWire(), - enc.schemaForWire(), + enc != null, + enc?.id ?: 0, + enc?.schema, options.attachment?.into()?.bytes, replyCallbackOf { handler.handle(it) }, { handler.onClose() }, diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt index e523bf8c..895540bb 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/query/Query.kt @@ -59,8 +59,8 @@ class Query internal constructor( keH: io.zenoh.jni.keyexpr.KeyExpr, parameters: String, payloadH: io.zenoh.jni.bytes.ZBytes?, - encH: io.zenoh.jni.bytes.Encoding?, encId: Int?, + encSchema: String?, attachH: io.zenoh.jni.bytes.ZBytes?, acceptsRepliesInt: Int, zq: io.zenoh.jni.query.Query, @@ -72,7 +72,7 @@ class Query internal constructor( ke, selector, payloadH?.let { ZBytes.fromHandle(it) }, - encH?.let { Encoding.fromParts(it, encId!!) }, + encId?.let { Encoding(it, encSchema) }, attachH?.let { ZBytes.fromHandle(it) }, io.zenoh.jni.query.ReplyKeyExpr.fromInt(acceptsRepliesInt).toPublic(), zq @@ -92,12 +92,13 @@ class Query internal constructor( @JvmOverloads fun reply(keyExpr: KeyExpr, payload: IntoZBytes, options: ReplyOptions = ReplyOptions()) { val q = zQuery ?: throw ZError("Query is invalid") + val enc = options.encoding q.replySuccess( keyExpr.flat, payload.into().bytes, - true, - options.encoding.idForWire(), - options.encoding.schemaForWire(), + enc != null, + enc?.id ?: 0, + enc?.schema, options.timeStamp?.ntpValue(), options.attachment?.into()?.bytes, options.express, @@ -154,7 +155,8 @@ class Query internal constructor( @Throws(ZError::class) fun replyErr(message: IntoZBytes, options: ReplyErrOptions = ReplyErrOptions()) { val q = zQuery ?: throw ZError("Query is invalid") - q.replyError(message.into().bytes, true, options.encoding.idForWire(), options.encoding.schemaForWire(), throwZError) + val enc = options.encoding + q.replyError(message.into().bytes, enc != null, enc?.id ?: 0, enc?.schema, throwZError) q.close() zQuery = null } diff --git a/zenoh-java/src/commonMain/kotlin/io/zenoh/sample/Sample.kt b/zenoh-java/src/commonMain/kotlin/io/zenoh/sample/Sample.kt index 86476eda..d7489659 100644 --- a/zenoh-java/src/commonMain/kotlin/io/zenoh/sample/Sample.kt +++ b/zenoh-java/src/commonMain/kotlin/io/zenoh/sample/Sample.kt @@ -65,8 +65,8 @@ data class Sample( fun fromParts( keH: io.zenoh.jni.keyexpr.KeyExpr, payloadH: io.zenoh.jni.bytes.ZBytes, - encH: io.zenoh.jni.bytes.Encoding, encId: Int, + encSchema: String?, kindInt: Int, ntp64: Long?, express: Boolean, @@ -80,7 +80,7 @@ data class Sample( ): Sample = Sample( KeyExpr(keH), ZBytes.fromHandle(payloadH), - Encoding.fromParts(encH, encId), + Encoding(encId, encSchema), io.zenoh.jni.sample.SampleKind.fromInt(kindInt).toPublic(), ntp64?.let { TimeStamp(it) }, QoS( diff --git a/zenoh-java/src/jvmTest/kotlin/io/zenoh/EncodingCorrespondenceTest.kt b/zenoh-java/src/jvmTest/kotlin/io/zenoh/EncodingCorrespondenceTest.kt new file mode 100644 index 00000000..8c3468db --- /dev/null +++ b/zenoh-java/src/jvmTest/kotlin/io/zenoh/EncodingCorrespondenceTest.kt @@ -0,0 +1,116 @@ +// +// 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.bytes.Encoding +import io.zenoh.exceptions.throwZError0 +import io.zenoh.jni.bytes.Encoding as JniEncoding +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Correspondence tests for the pure-JVM [Encoding] implementation. + * + * The SDK implements the encoding string ↔ `(id, schema)` conversion in pure + * Kotlin (no JNI crossing), on the contract that any JVM-side reimplementation + * of zenoh-flat API must be verified against the native implementation. These + * tests drive both implementations — the pure one and the native oracle (the + * generated `Encoding` handle methods) — over the whole predefined id range + * plus the edge shapes of the parse/render rules, asserting equal results. + */ +class EncodingCorrespondenceTest { + + /** Native oracle: the canonical string of `(id, schema)`. */ + private fun nativeRender(id: Int, schema: String?): String { + val h = JniEncoding.newFromId(id, schema, throwZError0) + try { + return h.toStr(throwZError0) + } finally { + h.close() + } + } + + /** Native oracle: parse a string into `(id, schema, canonical string)`. */ + private fun nativeParse(s: String): Triple { + val h = JniEncoding.newFromString(s, throwZError0) + try { + return Triple(h.getId(throwZError0), h.getSchema(throwZError0), h.toStr(throwZError0)) + } finally { + h.close() + } + } + + @Test + fun renderMatchesNativeAcrossIdRange() { + // The whole predefined range, a gap past it (unknown ids), and the + // custom id — with and without a schema. + val ids = (0..64) + listOf(1000, 0xFFFE, 0xFFFF) + for (id in ids) { + for (schema in listOf(null, "utf-8")) { + assertEquals( + "render mismatch for (id=$id, schema=$schema)", + nativeRender(id, schema), + Encoding(id, schema).toString(), + ) + } + } + } + + @Test + fun parseMatchesNativeOnNamesAndEdges() { + // Every predefined canonical name (obtained FROM the native table so the + // corpus can't drift), plus the parse-rule edge shapes. + val names = (0..52).map { nativeRender(it, null) } + val edges = listOf( + "", + "text/plain", + "text/plain;utf-8", + "text/plain;", + "my_custom_encoding", + "custom;with;semicolons", + ";leading_separator", + "unknown_name;schema", + "zenoh/bytes;s", + ) + for (s in names + names.map { "$it;schema" } + edges) { + val (nid, nschema, nstr) = nativeParse(s) + val pure = Encoding.from(s) + assertEquals("id mismatch for \"$s\"", nid, pure.id) + assertEquals("schema mismatch for \"$s\"", nschema, pure.schema) + assertEquals("render mismatch for \"$s\"", nstr, pure.toString()) + } + } + + @Test + fun withSchemaMatchesNative() { + val bases = listOf( + Encoding.TEXT_PLAIN, + Encoding.ZENOH_BYTES, + Encoding.from("my_custom_encoding"), + Encoding.from("text/plain;old-schema"), + ) + for (base in bases) { + val pure = base.withSchema("new-schema") + // The `e` param crosses as its decomposed (id, schema) value. + val w = JniEncoding.newWithSchema(base.id, base.schema, "new-schema", throwZError0) + val nativeStr = try { + w.toStr(throwZError0) + } finally { + w.close() + } + assertEquals("withSchema mismatch for $base", nativeStr, pure.toString()) + } + } +} diff --git a/zenoh-java/src/jvmTest/kotlin/io/zenoh/PublisherEncodingTest.kt b/zenoh-java/src/jvmTest/kotlin/io/zenoh/PublisherEncodingTest.kt new file mode 100644 index 00000000..b53daf69 --- /dev/null +++ b/zenoh-java/src/jvmTest/kotlin/io/zenoh/PublisherEncodingTest.kt @@ -0,0 +1,62 @@ +// +// 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.bytes.Encoding +import io.zenoh.bytes.ZBytes +import io.zenoh.keyexpr.KeyExpr +import io.zenoh.pubsub.PublisherOptions +import io.zenoh.pubsub.PutOptions +import io.zenoh.sample.Sample +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * End-to-end test of the publisher's **default encoding**: set natively once + * at declare time ([PublisherOptions.encoding]), applied by zenoh itself to + * every plain `put` — which crosses **no encoding data at all** — and + * overridden per call via [PutOptions.encoding]. + */ +class PublisherEncodingTest { + + private val custom = Encoding.from("application/custom;my-schema") + + @Test + fun publisherDefaultAppliesNativelyAndPerPutOverrides() { + val session = Zenoh.open(Config.loadDefault()) + val keyExpr = KeyExpr.tryFrom("example/testing/publisher/encoding") + val received = mutableListOf() + val subscriber = session.declareSubscriber(keyExpr) { received.add(it) } + + val options = PublisherOptions() + options.encoding = custom + val publisher = session.declarePublisher(keyExpr, options) + + // Plain put: NO encoding crosses — the native publisher default applies. + publisher.put(ZBytes.from("default")) + // Per-put override still works. + val putOptions = PutOptions() + putOptions.encoding = Encoding.TEXT_PLAIN + publisher.put(ZBytes.from("override"), putOptions) + Thread.sleep(500) + + publisher.close() + subscriber.close() + session.close() + assertEquals(2, received.size) + assertEquals(custom, received[0].encoding) + assertEquals(Encoding.TEXT_PLAIN, received[1].encoding) + } +} diff --git a/zenoh-java/src/jvmTest/kotlin/io/zenoh/ZenohIdCorrespondenceTest.kt b/zenoh-java/src/jvmTest/kotlin/io/zenoh/ZenohIdCorrespondenceTest.kt new file mode 100644 index 00000000..f531c240 --- /dev/null +++ b/zenoh-java/src/jvmTest/kotlin/io/zenoh/ZenohIdCorrespondenceTest.kt @@ -0,0 +1,58 @@ +// +// 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.config.ZenohId +import io.zenoh.exceptions.throwZError0 +import io.zenoh.jni.config.ZenohId as JniZenohId +import kotlin.random.Random +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Correspondence test for the pure-JVM [ZenohId.toString] — the id bytes read + * as a little-endian integer, rendered as lowercase hex without leading zeros + * (`"0"` for the zero id). Verified against the native formatter + * (`zenoh_id_to_string`) over edge patterns and a deterministic random corpus. + */ +class ZenohIdCorrespondenceTest { + + private fun assertCorresponds(bytes: ByteArray) { + val jni = JniZenohId(bytes) + assertEquals( + "zid render mismatch for ${bytes.joinToString(",")}", + jni.toStr(throwZError0), + ZenohId(jni).toString(), + ) + } + + @Test + fun rendersLikeNative() { + val edges = listOf( + ByteArray(16), // zero id + ByteArray(16).also { it[0] = 1 }, // smallest nonzero (LE low byte) + ByteArray(16).also { it[15] = 1 }, // highest byte only + ByteArray(16).also { it[0] = 0x0F }, // sub-0x10 low byte + ByteArray(16) { 0xFF.toByte() }, // all ones + ByteArray(16) { i -> i.toByte() }, // ascending with leading zero byte + ) + edges.forEach(::assertCorresponds) + + val rng = Random(20260716) + repeat(32) { + assertCorresponds(rng.nextBytes(16)) + } + } +}