jzswag#158
Merged
Merged
Conversation
Covers ParameterEncoder (all styles/formats/edge cases), OAuth2Handler (token acquisition, caching, threading, refresh, errors), OpenAPIParser (JSON/YAML, server URLs, security schemes, operations), and DesktopHttpClient (HTTP methods, headers, query params, auth, cookies, SSL, response handling) using JUnit 5 + Mockito + AssertJ + MockWebServer. Splits the bundled junit-jupiter dep into api/params/engine + adds mockito-junit-jupiter and junit-platform-launcher so the test classes can use @nested, @ParameterizedTest, and Mockito's JUnit5 integration.
settings.gradle includes both modules but neither had a tracked file, so git did not track the directories. After ./gradlew clean (or on a fresh checkout) Gradle aborted with "Configuring project ... without an existing directory is not allowed". Adds minimal java-library stubs that build cleanly without an Android SDK. They will be replaced with com.android.library / com.android.application once Phase 2 / Phase 3 of NEXT_STEPS.md is picked up.
Adds a Session Handoff section at the top with current branch state, verified build commands, and a concrete ordered list of immediate next actions (re-run integration tests → open draft PR → start Phase 2). Also updates the progress tracker: unit-test coverage, the rebase onto 1.11.1 main, and the placeholder build files for jzswag-android and jzswag-aaos are now marked complete.
The heart of the Java port: ZswagClient now implements
zserio.runtime.service.ServiceClientInterface so users can write the same
idiom as Python (services.MyService.Client(OAClient(...))) and C++
(MyService::Client(openApiClient)):
ZswagClient transport = new ZswagClient(openApiUrl);
Calculator.CalculatorClient calc = new Calculator.CalculatorClient(transport);
Double r = calc.powerMethod(request);
Built up in layers:
* HTTP config split: HttpConfig (per-request adhoc, mirrors httpcl::Config /
Python HTTPConfig) and HttpSettings (multi-scope persistent registry,
mirrors httpcl::Settings). HttpSettingsLoader loads the canonical
http-settings: - scope: ... YAML so the same file works with all three
clients.
* OpenAPIParser extended to full spec: x-zserio-request-part on parameters,
application/x-zserio-object request bodies, root-level default security,
OAuth2 flows.clientCredentials parsing (rejects other flows), security
alternatives preserved as List<SecurityRequirement>, format: byte alias,
style/location validation. PATCH operations are intentionally ignored.
* ParameterEncoder split by location: encodeForPath returns the styled
string; encodeForQuery returns a list of (name, value) pairs so
style: form + explode: true correctly emits ?id=1&id=2&id=3;
encodeForHeader / encodeForCookie return single values.
* ZserioReflection resolves x-zserio-request-part dotted paths against the
typed zserio request object via JavaBean getter reflection (zserio Java
has no IReflectableView equivalent), unwraps zserio enums via
ZserioEnum.getGenericValue(), serializes nested compounds via
Writer.write(BitStreamWriter).
* DesktopOpenAPIClient.callMethod(methodIdent, zserioRequest) is now the
canonical entry point; it dispatches via x-zserio-request-part with
application/x-zserio-object Content-Type and Accept, strict 200-only
success check, throws on unfilled path placeholders.
* DesktopHttpClient applies HTTP_SSL_STRICT and proxyUrl (previously TODOs),
scope-merges persistent + adhoc per request, default timeout 60s
(matching C++).
* Integration test rewritten: uses Calculator.CalculatorClient(zswagClient)
with no manual extractParameters; all 10 tests pass through the actual
zswag flow rather than test-harness camouflage.
* Old unit tests removed (they tested the pre-port API surface and would
not compile against the new types). New tests will land alongside L11.
OAuth2 wiring, full auth completeness, OS keychain, env-var plumbing
remain to be done in subsequent commits.
Completes the parity surface beyond the dispatch core:
* OAuth2Handler rewritten with full feature parity to C++ openapi-oauth.cpp:
- Process-wide token cache keyed by (tokenUrl, clientId, audience, scope)
so multiple OAuth2 schemes don't collide; per-key locking serialises
mint/refresh attempts.
- Refresh-token reuse on expiry; falls back to fresh mint on refresh
failure (preserving the old refresh_token if not re-issued).
- rfc6749-client-secret-basic (default) and rfc5849-oauth1-signature
(HMAC-SHA256) token-endpoint authentication methods, the latter via a
new OAuth1Signature port of httpcl::oauth1::*.
- audience parameter and public-client (client_id-in-body) support.
* DesktopOpenAPIClient.applySecurity walks the OR-of-AND security
alternatives, picking the first satisfiable one. For OAuth2 schemes it
resolves tokenUrl/refreshUrl/scopes per the settings-vs-spec precedence
rules and injects Authorization: Bearer; for API-key schemes it routes
the merged config's api-key field to header/query/cookie based on the
scheme's `in:`. Throws a descriptive error if no alternative can be
satisfied (matches openapi-security.cpp behavior).
* JzswagLogging hooks HTTP_LOG_LEVEL up to logback's root logger
programmatically (graceful no-op if logback isn't the active SLF4J
binding). Initialised on every DesktopHttpClient construction.
* New unit tests (55 across 5 classes, all passing):
- HttpSettingsLoaderTest (16): YAML schema parity with C++/Python,
including all oauth2 sub-fields, basic-auth/proxy keychain forms,
legacy list root, scope vs url forms, and validation errors.
- HttpConfigAndSettingsTest (9): mergedWith multi-valued union,
other-wins-on-set semantics, OAuth2 sub-field merge, scope glob
compilation, multi-scope forUrl merging.
- ParameterEncoderTest (14): style x location x format combinations
including the previously-broken style:form/explode:true case.
- ZserioReflectionTest (7): POJO getter resolution, snake_case to
lowerCamel normalisation, ZserioEnum unwrap to genericValue,
descriptive error on missing getter.
- OAuth1SignatureTest (9): RFC 3986 percent-encoding boundaries,
nonce-length bounds, RFC 5849 signature base string format,
Authorization header structure.
Integration test still 10/10. Notable parity gaps still open:
HTTP_LOG_FILE rotation, settings-file reload-on-change watchdog,
Windows Credential Manager keychain (Linux secret-tool / macOS
security work today).
The README had grown to 1328 lines covering Python, C++, Java, server, and generator in one file, with the per-language sections drifting out of sync — most visibly the Java section, which referenced the pre-port API and listed Java nowhere in the OpenAPI Options Interoperability matrix. Restructured to a slim README + focused docs/<lang>.md sub-pages: * README.md (1328 -> 306 lines): intro/components, ten-line quickstarts per language linking to the focused docs, CI/release notes, and the full feature interop matrix (now with Java alongside C++ / Python / OAServer / zswag.gen). * docs/java.md (new): canonical Java guide. The Calculator.CalculatorClient(new ZswagClient(url)) idiom; HttpConfig vs HttpSettings model; persistent + adhoc merge rule; OAuth2 wiring including rfc5849-oauth1-signature; how x-zserio-request-part is resolved via POJO reflection; environment vars; troubleshooting. Replaces the 456-line GETTING_STARTED_JAVA.md (deleted) which documented the pre-port API and was misleading. * docs/python.md (new): client + server usage extracted from README. * docs/cpp.md (new): client integration + CMake from README. * docs/openapi-generator.md (new): zswag.gen CLI reference from README. * docs/http-settings.md (new): the shared HTTP_SETTINGS_FILE YAML format in one place — referenced from each language doc, eliminating the ~150-line per-language duplication that previously lived in README. * libs/jzswag-api/README.md (71 -> 23 lines): describes the module's role and contents only; usage examples removed (they reference removed builder methods anyway). Points at docs/java.md. * libs/jzswag-desktop/README.md (148 -> 46 lines): module layout + dependency list + pointer to docs/java.md. Out-of-date pre-port code examples removed. * libs/jzswag-test/README.md (187 -> 63 lines): test coverage matrix + how to run; removed stale "Known Issues" / "Next Steps" sections that pre-dated the parity work. Internal markdown links verified.
Contributor
Author
|
@petrhons-tomtom, added you to the review as I remember you may be interested in that. |
The jzswag-api module had no tests of its own; HttpConfig and HttpSettings were only exercised transitively via jzswag-desktop, so JaCoCo reported 0% coverage for the api jar itself. Adds a dedicated test source dir with five focused suites — HttpConfig, HttpSettings, HttpRequest/HttpResponse/HttpException, OpenAPIParameter, SecurityScheme/SecurityRequirement — covering 394/395 lines. Also adds the missing junit-platform-launcher runtime dep so the new test task can start under JUnit 5.
Adds tests for the previously-untested classes that account for most of the desktop module's line count: - OpenAPIParserTest — YAML parsing, x-zserio-request-part, OAuth2 flow validation, style/location validation, default operationId synthesis, PATCH-skip behaviour (0% → 91.4%). - DesktopHttpClientTest — uses MockWebServer to verify all HTTP methods, header/cookie/query/basic-auth merging, per-request header precedence, scope-matched persistent settings (0% → 79.2%). - ZswagServiceClientTest — Mockito-based tests for path construction, getter reflection, and exception propagation (0% → 89.7%). - KeychainTest — exercises the empty-service guard, OS-detection branches, and exception forms (0% → 40.9%). - JzswagLoggingTest — idempotent init paths (0% → 40.0%). - HttpSettingsLoaderFileEnvTest — file-based loading and env-var/null/ scalar root handling (76.3% → 85.2%). Module total now at 723/1167 lines covered.
The "desktop" name was misleading — this module's defining trait is "uses the JDK 11 java.net.http.HttpClient", which works equally well on servers, lambdas, CLIs, and desktops. The split that actually matters is JVM vs Android (which lacks java.net.http and will need OkHttp + Android Keystore + a different logger). Renaming aligns with the Kotlin/JVM ecosystem convention of "-jvm" / "-android" artifact suffixes. This commit only renames the module directory and updates external references (settings.gradle, sibling-module project deps, CI workflow, maven artifactId). Java package and class names stay as com.ndsev.zswag.desktop / Desktop* in this commit and are renamed in the follow-ups so each step is independently reviewable.
Shifts the package namespace from com.ndsev.zswag.* to io.github.ndsev.zswag.* to match how the zserio runtime — which jzswag depends on — is published on Maven Central. Two reasons: 1. zserio publishes as io.github.ndsev:zserio-runtime via Sonatype's GitHub-org namespace verification; jzswag uses the same GitHub org (ndsev) and is conceptually a sibling project, so the family relationship is now visible to consumers. 2. com.ndsev.zswag would have failed Maven Central namespace verification anyway: ndsev.com is not the NDS Association's domain, and Sonatype requires either DNS proof or a matching GitHub org. io.github.ndsev takes the latter route cleanly. Mechanical change only: every .java file moves from src/.../java/com/ndsev/... to src/.../java/io/github/ndsev/..., and the package + import declarations follow. Also updates the root group, mainClass entries in examples/jzswag-cli and libs/jzswag-test build files.
Final piece of the rename: the io.github.ndsev.zswag.desktop sub-package becomes io.github.ndsev.zswag.jvm, and the two public classes that carried the now-stale "Desktop" prefix follow: - DesktopHttpClient → JvmHttpClient - DesktopOpenAPIClient → JvmOpenAPIClient - DesktopHttpClientTest → JvmHttpClientTest These are public API but have no external consumers yet (jzswag has not been published), so a hard rename is preferable to a deprecation period. The canonical entry point ZswagClient is unchanged — that is the class end users actually instantiate (Calculator.CalculatorClient(zswagClient)), so this rename does not affect the typical usage idiom.
Sweep of every doc and javadoc that named the old module, package, or
classes:
- README.md (top-level): module table, Java quickstart Gradle/import snippet
- docs/java.md: intro paragraph, module table, build/import code blocks
- libs/jzswag-jvm/README.md: title, intro, module-layout class names,
testing command
- libs/jzswag-api/README.md: cross-references to the jvm module
- CLAUDE.md: project guide (module list, build commands, Java entry
points)
- HttpSettings.java javadoc: cross-reference to HttpSettingsLoader
- JvmHttpClient.java javadoc: opening sentence ("Desktop" → "JVM")
- ExampleCli.java javadoc: opening sentence
Adds a third module for the platform-agnostic core so that an Android
implementation can reuse the same OpenAPI dispatch / parsing / OAuth2 /
keychain-loader logic without duplicating it. New layout:
jzswag-api contracts: HttpConfig, HttpSettings, OpenAPIParameter,
SecurityScheme, IHttpClient, IKeychain (new), ...
jzswag-shared portable core: OpenAPIClient (formerly JvmOpenAPIClient),
OpenAPIParser, ParameterEncoder, ZserioReflection,
OAuth1Signature, OAuth2Handler, HttpSettingsLoader,
ZswagServiceClient
jzswag-jvm platform-specific: JvmHttpClient, Keychain, JzswagLogging,
ZswagClient (constructs the right HTTP client + keychain)
Required changes to make the core platform-agnostic:
- New IKeychain interface in jzswag-api decouples OAuth2Handler from the
JVM-specific Keychain class. JvmHttpClient now takes an IKeychain too,
defaulting to a fresh Keychain instance for back-compat.
- IHttpClient gains a getPersistentSettings() method (default returns
empty) so OpenAPIClient.mergedConfigFor(url) doesn't have to downcast
to JvmHttpClient any more.
- OpenAPIClient and OAuth2Handler take an IKeychain in their constructors;
ZswagClient (jvm) wires up Keychain + JvmHttpClient + OpenAPIClient.
- Static Keychain.load() is removed; only the instance method remains
(KeychainTest updated accordingly).
- ZswagServiceClient.create() static factories removed — they instantiated
JvmHttpClient directly (now a layering violation). Constructors stay.
Test counts after the split: api 59, shared 83, jvm 45 (187 total, all
passing). Line coverage: api 99.5%, shared 62.8%, jvm 61.0%.
Replaces the placeholder jzswag-android with a real build setup that
depends on jzswag-shared and on OkHttp / slf4j-android, ready for the
Android-specific implementations to land in subsequent commits.
Trade-off documented in the build file: this module uses the plain
`java-library` plugin instead of `com.android.library`. Reason:
Google currently ships only x86_64 Linux aapt2 binaries. On aarch64
Linux build hosts the AGP-driven build fails with "AAPT2 daemon
startup failed" on `verifyReleaseResources` /
`processReleaseUnitTestResources`, even for resource-free library
modules. There is no community aarch64 build of aapt2 either.
Effect of the trade-off:
- Output is a JAR rather than an AAR (Android consumers can still
depend on it, just less idiomatically than an AAR);
- AndroidX dependencies are unavailable (java-library can't consume
AAR deps), so AndroidKeychain will use the raw Android Keystore
APIs + AES + SharedPreferences instead of EncryptedSharedPreferences;
- android.* references compile against `org.robolectric:android-all`
(a stub jar of the Android framework), with the real framework
provided at runtime by the consuming app.
On an x86_64 build host the module can be flipped back to
`com.android.library` for proper AAR output with no source changes.
Other plumbing in this commit:
- AGP classpath bumped from 8.2.2 → 8.7.2 (kept for the future flip
back to `com.android.library`; harmless no-op while we are on
`java-library`).
- Root `gradle.properties` enables `android.useAndroidX=true` (still
needed if someone flips the plugin back) and bumps Gradle daemon
JVM heap to 2 GB.
- `.gitignore` adds `local.properties`, `*.aar`, `*.apk`, `.cxx/`.
The Android counterpart to JvmHttpClient. Mirrors its behaviour exactly so a request configured the same way produces the same wire-level traffic on either platform: - persistent HttpSettings (URL-scope-matched) merged with the per-call adhoc HttpConfig; - per-request headers (case-insensitive) suppress duplicate merged-config entries — prevents OkHttp from emitting double Authorization / Cookie headers when both layers configure them; - basic-auth resolved from cleartext password OR an injected IKeychain (no static Keychain fallback like the JVM version had); - per-URL proxy config builds a one-shot OkHttpClient with a proxyAuthenticator (matches JvmHttpClient's "rare path" approach); - HTTP_SSL_STRICT env var + HttpConfig.isSslStrict() drive a TrustEverythingManager when relaxed mode is required; - HTTP_TIMEOUT env var sets connect / read / write timeouts (default 60s, matching the C++/JVM clients). Removes the BuildMarker placeholder.
…Preferences) The Android counterpart to the JVM's Keychain. Implements IKeychain so OAuth2Handler and AndroidHttpClient consume both interchangeably. Storage strategy: - A symmetric AES-256-GCM key is generated in the platform Keystore on first use, aliased "io.github.ndsev.zswag.keychain.master". The key never leaves the secure hardware (TEE / StrongBox where available); we only ever hold a Cipher handle. - Per-credential entries (one per service|user pair) are encrypted with that key and stored in a private SharedPreferences file. The on-disk blob is base64(iv_len_byte | iv | ciphertext_with_gcm_tag). Public API: - load(service, user) — IKeychain contract, throws if entry absent. - store(service, user, secret) — for app-side onboarding. - delete(service, user). Why not androidx.security:security-crypto / EncryptedSharedPreferences? That library is distributed as an AAR, which the java-library-based build of this module cannot consume (see this module's build.gradle for the aapt2-on-arm trade-off). Doing the AES/GCM dance manually keeps us inside Java APIs that work both at compile time (against the Robolectric android.jar stub) and at runtime (on a real device). Will get full unit-test coverage in the upcoming Robolectric-tests commit.
Completes the Android module's user-facing API surface:
- AndroidLogging.init() — symmetric to JzswagLogging.init() but a near-noop:
on Android, log filtering is controlled by logcat tag levels (setprop
log.tag.<TAG>), not programmatically by the application. We surface
HTTP_LOG_LEVEL once if set so the developer can confirm the value the
JVM modules would have used.
- ZswagClient — implements zserio's ServiceClientInterface; the only
public-API difference from the JVM port is a Context parameter on the
convenience constructors (needed so AndroidKeychain can reach
SharedPreferences for credential storage). After construction, the
call-site is identical to the JVM port:
ZswagClient transport = new ZswagClient(context, openApiUrl);
Calculator.CalculatorClient calc = new Calculator.CalculatorClient(transport);
Double r = calc.powerMethod(new BaseAndExponent(...));
Adds three test classes covering the largest part of the Android port:
- AndroidHttpClientTest (17 tests) — full coverage via OkHttp's
MockWebServer, mirroring JvmHttpClientTest. AndroidHttpClient happens
to be a pure-Java class (only OkHttp + java.net + javax.net.ssl, no
android.* refs), so plain JUnit + MockWebServer is sufficient.
- AndroidKeychainTest (5 tests) — input validation + missing-entry
paths, using Mockito to fake Context / SharedPreferences. The
encrypt/decrypt round trip and the platform Keystore key generation
need either Robolectric or an Android device; tracking that as a
follow-up gap (see below).
- AndroidLoggingTest (2 tests) — exercises the HTTP_LOG_LEVEL-unset
path of init(); the env-var-set branch routes through android.util.Log
and needs a device to run.
- ZswagClientTest (4 tests) — uses a mock OpenAPIClient to exercise
delegation, ZserioError wrapping, and the missing-zserio-object guard.
The Context-taking convenience constructors are tested only via
device instrumentation tests (out of this PR's scope).
Why no Robolectric: Robolectric pulls in Conscrypt for SSL, which has
no aarch64-linux-native binary. On the aarch64 Linux build host this
fails with UnsatisfiedLinkError before any test code runs. Robolectric
also requires androidx.test:monitor — distributed only as an AAR which
the java-library plugin cannot consume directly. On an x86_64 host
both restrictions go away and the suite can be expanded to cover
AndroidKeychain's encrypt/decrypt path and AndroidLogging's
log-level-routing path.
Build wiring needed for the test classpath:
- testImplementation 'org.robolectric:android-all' so test sources
can import android.content.Context for Mockito mocks (Mockito
intercepts calls so the stub's "Stub!" method bodies don't matter).
- testRuntimeClasspath excludes 'uk.uuid.slf4j:slf4j-android' so
the JVM-side test runtime uses logback-classic (slf4j-android
references android.util.Log at class-load time and won't load on
plain JVM).
Coverage summary (line, all modules):
api 99.5% shared 62.8% jvm 61.0% android 64.3%
The PNG (and its StarUML .mdj source) was generated before the Java
port and didn't show any of the four jzswag modules. It also drifts
silently — there's no CI gate that the image matches the code.
Mermaid renders natively on GitHub and is text-editable, so the diagram
is maintained alongside the code it documents.
The new diagram shows:
* the four client implementations (Python, C++, Java JVM, Java Android)
* the C++ core (zswagcl, httpcl) shared with Python via pybind11
* the Java core (jzswag-shared, jzswag-api) shared by both Java
platform clients
* OAServer (Python only, the lone server today)
* the OpenAPI spec as the common contract
Removed docs/assets/zswag-architecture.{png,mdj}; the asset dir is
empty so it's gone too.
Default Mermaid theme on GitHub renders fairly flat. Switching to the 'base' theme + per-language classDef styling produces visibly distinct node colors (Python green, C++ blue, Java orange, spec yellow) and a cleaner system-font label rendering — closer to the look of the old hand-drawn PNG, but still text-editable and free of external tooling. Solid arrows for "calls into", dotted arrows for "reads / exposes" the spec, separating dependency direction from data flow.
Mermaid's default dagre renderer produces meandering bezier-ish edges and uneven node placement. Switching to the elk renderer enables orthogonal edge routing (90-degree turns) and tighter alignment. The `curve: 'stepAfter'` setting forces step edges as a fallback in environments that don't ship elk. Also normalised node label lengths to 3 lines each so boxes come out closer to the same width.
The elk renderer was packing too tightly — Java core subgraph border sliced through jzswag-shared, OAServer floated alone outside any group, and five "reads" + one "exposes" dotted arrows piled up around the spec node making it look interrogated. Rework: * drop elk, keep stepAfter curves on default dagre (which handles this graph shape better) * fold spec-reading/exposing into the spec node's label, drop the 6 redundant arrows * spec now sits at the top with two clean arrows down to clients and server subgraphs * OAServer moves into its own Server subgraph for symmetry with Clients * more nodeSpacing + rankSpacing + padding so the cluster borders don't clip node text
Comparing the Mermaid to the recovered original PNG surfaced gaps:
* zswag.gen — the Python CLI that generates the OpenAPI spec from a
zserio service — was missing entirely.
* External libraries (cpp-httplib, keychain, Flask/Connexion, OkHttp/
Android Keystore) were shown as small boxes in the original; only
named in prose here.
* zserio itself wasn't visible as the foundation it actually is.
* The "Python reuses C++ via pybind11" relationship was implicit in the
py --> zswagcl arrow but not labelled.
Added:
* zserio --> gen --> spec --> {clients, server} flow at the top
* External libraries subgraph at the bottom with arrows from the
cores/server that use them
* Cluster label "C++ core (shared with Python via pybind11)" now
describes the binding explicitly
Contributor
Author
|
We should rename ZswagClient to something matching the C++/Python clients. Current suggestion during internal discussions was OpenApiClient (in case this is not already taken by any open lib that we are using or anything) |
PR review feedback: the user-facing Java client should match the C++ and Python user-facing client names. C++ has 'zswagcl::OAClient', Python has 'zswag.OAClient' — both are the typed zserio service client that implements the canonical idiom 'MyService.Client(transport)'. Java's ZswagClient is semantically identical (implements zserio.runtime.service.ServiceClientInterface, takes a spec URL, delegates to OpenAPIClient) so the renaming is a pure naming alignment. No behaviour change. Renamed: * libs/jzswag/jzswag-jvm/.../ZswagClient.java -> OAClient.java * libs/jzswag/jzswag-android/.../ZswagClient.java -> OAClient.java * libs/jzswag/jzswag-android/.../ZswagClientTest.java -> OAClientTest.java * All references updated across docs/java.md, the architecture diagram in README, the per-module READMEs, integration test client.
…++ parity PR review feedback: align Java's low-level dispatch-class name with C++'s zswagcl::OpenApiClient. The Java OPP-ACCRONYM-IN-CAPS convention (URLConnection, XMLParser) is fine but here the C++ counterpart uses camelCase 'OpenApiClient' so matching exactly removes a casing-only inconsistency between languages. Renamed: * libs/jzswag/jzswag-api/.../IOpenAPIClient.java -> IOpenApiClient.java * libs/jzswag/jzswag-shared/.../OpenAPIClient.java -> OpenApiClient.java * libs/jzswag/jzswag-shared/.../OpenAPIClientSecurityTest -> OpenApiClientSecurityTest All references updated across both platform OAClient impls, docs/java.md, jzswag-jvm README, jzswag-shared README.
Restores the gh-pages deploy that 3609041 ('Simplify coverage workflow') removed, and extends it to cover Java JaCoCo HTML too. After this: https://ndsev.github.io/zswag/ landing page ├── cpp/ gcovr report (httpcl + zswagcl) └── java/ ├── api/ JaCoCo — jzswag-api ├── shared/ JaCoCo — jzswag-shared ├── jvm/ JaCoCo — jzswag-jvm └── android/ JaCoCo — jzswag-android Implementation: * coverage.yml: stages site/cpp/ from gcovr output + writes the root index.html. Adds an index.html redirect inside cpp/ so gcovr's coverage.html entrypoint works from the directory URL. * jzswag.yml: stages site/java/<module>/ from each JaCoCo html dir and writes site/java/index.html. * Both workflows: peaceiris/actions-gh-pages@v4, publish_dir: site, keep_files: true so they coexist on the gh-pages branch. * Triggers now listen on push to main AND master (was master only — the deploy guard was unreachable since main is the default branch). * docs/cpp.md and docs/java.md re-add the public URL link. Requires one-time admin action (out of band): set GitHub Pages source to the gh-pages branch via repo settings. Currently configured to serve from main / which is why the path 404s today.
The C++ and Python clients already accept a serverIndex / server_index parameter to choose between multiple entries in the OpenAPI spec's servers[] array (issue #113, closed in 1.7.0). The Java port was hard-coded to servers.get(0), breaking the parity claim in the PR description. Adds the parameter to all three relevant constructors: * OpenApiClient(spec, http, adhoc, keychain, serverIndex) * jvm/OAClient(url, persistent, adhoc, serverIndex) * android/OAClient(context, url, persistent, adhoc, serverIndex) Existing constructors keep the implicit serverIndex=0 default. Negative values raise IllegalArgumentException; out-of-bounds values raise IOException with a descriptive message during construction. Index 0 stays valid when servers[] is empty (per OpenAPI 3.0+ §4.7.5 the empty array implies [{ "url": "/" }]). Tests in OpenApiClientServerIndexTest cover default behaviour, explicit index selection across three servers, both error paths, and the implicit-root edge case. README and docs/java.md updated with code examples and the matrix entry distinguishing 'URL pickup' from 'multi-server selection'.
OpenApiClient.dispatch passed PATH-located parameter values through
ParameterEncoder.urlEncode, which is application/x-www-form-urlencoded
semantics. That mangles the sub-delimiters used by OpenAPI path styles:
Form encoded vs. RFC 3986 path-segment
';id=42' -> '%3Bid%3D42' vs. ';id=42' (matrix)
'.42' -> '.42' (kept) but '.1.2' -> '%2E1%2E2' in some JDK impls
',a,b' -> '%2Ca%2Cb' vs. ',a,b' (simple array, comma-joined)
The form encoder also emits '+' for space; RFC 3986 wants %20.
Add ParameterEncoder.pathEncode that keeps unreserved + sub-delims
(! $ & ' ( ) * + , ; =) and the pchar additions (: @) verbatim, only
percent-encoding bytes outside that set. Matches C++ httpcl
URIComponents::appendPath behaviour (uri.cpp:337).
Swap the OpenApiClient PATH branch to use pathEncode. Other locations
(QUERY, HEADER, COOKIE) keep their existing encoders.
Tests cover matrix/label preservation, the full pchar set staying
verbatim, segment separators ('/', '?', '#') and space being escaped,
and UTF-8 byte-by-byte percent-encoding.
ParameterEncoderTest: 14 -> 18.
* HTTP/Basic security accepts a pre-set Authorization: Basic header (match C++ openapi-security.cpp:22-37). A user who configures their own static Authorization header via HttpConfig.header() no longer gets a misleading "no basic-auth configured" error when the spec requires HTTP Basic. * OAuth2 spec-fetch fail-mode aligned with C++ (openapi-oauth.cpp:283-345): when useForSpecFetch=true but oauth2.tokenUrl is unset (or mint fails), log a warning and continue the spec fetch unauthenticated. Previously Java refused to construct the client at all. The downstream OpenAPIParser request will surface the real failure if the spec endpoint actually requires auth. * openIdConnect security scheme rejected at parse time (match C++ openapi-parser.cpp). Previously Java accepted the scheme and only refused at dispatch, meaning a Java app could construct a client against a spec that C++ would reject up-front. * OAuth2 token cache switched to monotonic clock (System.nanoTime, matching C++ std::chrono::steady_clock at openapi-oauth.cpp:56). Wall-clock jumps from NTP slews or manual time changes no longer retroactively expire valid tokens or extend the lifetime of an expired one. Tests updated: * OpenApiClientSecurityTest.useForSpecFetchWithoutTokenUrlFallsThroughToUnauthFetch * OpenAPIParserTest.openIdConnectSchemeRejectedAtParseTime
JDK 11 HttpClient (unlike cpp-httplib and OkHttp) does NOT auto-decompress gzip-encoded responses. A server that opportunistically gzips application/x-zserio-object responses left the JVM client looking at raw gzip bytes — and zserio deserialization saw garbled input. JvmHttpClient now inspects the Content-Encoding header on responses and transparently unwraps gzip via GZIPInputStream. Other encodings (brotli, zstd) are not handled — neither cpp-httplib nor OkHttp's default config handles them either, so this matches the existing parity surface. If decompression fails, we log a warning and return the original (compressed) bytes so the caller can still introspect the problem. Test in JvmHttpClientTest uses MockWebServer + GZIPOutputStream to verify the round-trip ends up with the original payload.
C++ wires both env vars to a rotating file appender (log.cpp:35-51);
Java's JzswagLogging only honoured HTTP_LOG_LEVEL with an in-code TODO
acknowledging the gap.
JzswagLogging.init now reads HTTP_LOG_FILE and, if non-empty, attaches
a logback RollingFileAppender to the root logger with:
* a FixedWindowRollingPolicy (3-file window: FILE / FILE-1 / FILE-2),
mirroring the C++ rotation scheme
* a SizeBasedTriggeringPolicy with maxFileSize from HTTP_LOG_FILE_MAXSIZE
(default 1 GB, matches C++)
* a PatternLayoutEncoder using a layout close to C++'s default so the
rendered lines are similar (timestamp / thread / level / logger / msg)
All logback construction is reflective so the JVM module doesn't gain a
compile-time logback dependency. Falls back to a stderr note + best-effort
continue when the active SLF4J binding isn't logback. The earlier TODO
comment is now gone.
Behavioural parity with C++ for log rotation. Unit tests for this path
require a real filesystem write — covered transitively by integration
testing on the calc harness; no new unit test added (rotation hits
real I/O timing).
JvmHttpClient and AndroidHttpClient were constructing a fresh JDK HttpClient (resp. OkHttpClient) on every request when the merged HttpConfig selected an HTTP proxy. Both have a non-trivial setup cost — JDK HttpClient spawns a new executor; OkHttp loses its connection pool and dispatcher reuse. OkHttp's own docs explicitly recommend 'one client per process'. Cache the per-proxy clients keyed on host:port|strict|permissive in a ConcurrentHashMap so concurrent requests through the same proxy reuse the same underlying client. No behavioural change for callers; just removes a per-request setup cost that scaled badly on proxied deployments.
C++ HttpSettings::operator[] checks the source file's mtime on every call and re-parses on change (http-settings.cpp:520-543) — supports credential rotation in long-running clients without restart. Java's HttpSettings was immutable and never reloaded, so a rotated token in the YAML file would never be picked up. Adds HttpSettingsLoader.HotReloader: a thread-safe wrapper around an HttpSettings snapshot + optional source Path. Each current() call stat()s the file once and reloads via loadFromFile if mtime advanced. Plumbed through JvmHttpClient and AndroidHttpClient: * The no-arg constructor (which reads HTTP_SETTINGS_FILE from env) now keeps the source path around for hot reload. * The HttpSettings-taking constructors store a no-source HotReloader (no reload — caller-supplied snapshot). * getPersistentSettings() and the per-request merge call go through reloader.current() so spec-fetch and dispatch see the same value. Failed reloads keep the previous snapshot rather than dropping to empty (better than losing all credentials mid-flight). Broken YAML records the mtime so the same broken file isn't reparsed on every request. HotReloaderTest covers: initial load, no-change → identity reuse, mtime-advance → reload, broken YAML → keep-prev, null-source → no-op.
Mirrors C++ Settings::store (http-settings.cpp:484) so tooling can update credentials programmatically and re-write HTTP_SETTINGS_FILE. With the HotReloader on the active HTTP client, the new contents are picked up automatically on the next request — supports rotation workflows without restart. The emitter: * Round-trips through HttpSettings → POJO tree → SnakeYAML Dumper (block-style, 2-space indent). * Omits empty optional fields (no spurious empty basic-auth / proxy / oauth2 blocks). * Flattens single-value headers/query/cookies; preserves list form for multi-valued. Round-trip test verifies a settings object survives writeToFile + loadFromFile with the relevant fields intact. Minimal-config test asserts no empty blocks pollute the output.
Two final parity-audit items from the same pass:
PARAMETER-ENCODER MAP SUPPORT
=============================
C++ openapi-parameter-helper handles map-typed parameter values across
all four locations and styles (openapi-parameter-helper.cpp:140-205).
Java's ParameterEncoder previously only supported scalars and arrays;
a Map value silently passed through String.valueOf(...) and emitted
something like "{R=1, G=2}" — server-side dispatch failed without
clear error.
Adds Map handling to encodeForPath, encodeForQuery, encodeForHeader,
encodeForCookie. Style × explode behaviour matches C++:
query/form, explode=true ?R=1&G=2
query/form, explode=false ?color=R,1,G,2
path/simple R,1,G,2
path/label, explode=true .R=1.G=2
path/matrix, explode=true ;R=1;G=2
path/matrix, explode=false ;color=R,1,G,2
header/simple R,1,G,2
cookie R,1,G,2
No caller produces a Map today (ZserioReflection only emits scalars
and arrays), so this is preparation for a future Java-side
IReflectableView equivalent — but it removes the silent-failure trap
in the meantime.
Tests cover the five primary encoding shapes against deterministic
LinkedHashMap inputs.
WINDOWS KEYCHAIN — documented explicitly
========================================
The C++ httpcl library supports Windows credential manager via the
`keychain` C library (DPAPI). The Java JVM client throws
KeychainException with a previously cryptic message. Now:
* Code: KeychainException message tells the user explicitly that
Windows isn't supported in Java, names the workarounds (cleartext
password: or HttpConfig.basicAuth), and notes that C++/Python DO
support it (so the gap is clearly Java-specific).
* README: keychain table gains C++/Python and Java columns; the
Java cell explains the limitation and points at workarounds.
* docs/java.md: matching one-line note in the auth section.
* libs/jzswag/jzswag-jvm/README.md: same.
Implementation is out of scope for this PR — needs JNA → DPAPI or
shell-out to cmdkey/vaultcmd. Tracked separately.
Full Java test sweep: 246 tests, 0 failures (was 229 at the start
of the parity audit cycle).
A second-pass parity audit surfaced 5 real issues with the previous fixes — addressing all before merge. HOTRELOADER BYPASS VIA OACLIENT ================================ OAClient(String) called HttpSettingsLoader.loadFromEnvironment() and passed the resulting snapshot to JvmHttpClient(persistent, keychain) — which constructed a HotReloader with null source, defeating hot-reload in the most common usage path. Fix: route the env-driven OAClient ctor through HotReloader.fromEnvironment() so file mtime changes are picked up on the next request, matching the C++ Settings::operator[] behaviour. Same change on the Android side. SPEC FETCH BYPASSED IHTTPCLIENT ================================ OpenAPIParser.loadSpec used raw URLConnection for HTTP(S) spec URLs, ignoring HTTP_SSL_STRICT, proxy, basic-auth, HTTP_TIMEOUT, and any persistent headers/cookies/query from http-settings.yaml. C++ routes through httpcl::IHttpClient (openapi-parser.cpp:499); Java did not. Adds OpenAPIParser(specLocation, IHttpClient, HttpConfig, extraHeaders) which builds an HttpRequest and dispatches via the configured client. OpenApiClient.parseSpec uses this constructor; the OAuth2 useForSpecFetch Bearer is passed as an extra header instead of via a URLConnection injector. Local-file specs continue to read straight from the filesystem. Regression test: OpenApiClientSecurityTest.specFetchRoutesThroughConfiguredIHttpClient asserts the spec body actually flows through the stub IHttpClient. HTTP_TIMEOUT CONSISTENCY ======================== HTTP_TIMEOUT was applied to the JDK HttpClient's connect timeout but not the per-request timeout — that one used HttpConfig.defaultTimeout() (hardcoded 60s). C++ uses one value end-to-end. Adds HttpConfig.getTimeoutOrNull() so transports can distinguish "caller explicitly set 60s" from "caller didn't touch it." JvmHttpClient and AndroidHttpClient now fall back to the HTTP_TIMEOUT-derived default for the latter case. GZIP RESPONSE HEADERS ===================== After auto-decompression, the returned headers still carried the original Content-Encoding: gzip and Content-Length (now wrong) — caller inspection got a stale view. Strip both headers post-decompression. Test updated to assert their absence. STALE DOCS ========== docs/java.md said HTTP_LOG_FILE was "not yet wired in Java" — it was wired in commit 80fc7fa. docs/java.md said HTTP_SSL_STRICT=0 disables strict — but the code (per commit b06f699) treats any non-empty value as enabled. README's HTTP_LOG_FILE row carried the same staleness. Both fixed. Java tests: 246 -> 247 (added one for spec-fetch routing); 0 failures.
This was referenced Jun 12, 2026
# Conflicts: # README.md
# Conflicts: # README.md
Minimum allowed line rate is |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
Introduces native JAVA libs (jvm and android) at full parity with the C++ and Python clients.
Modules (grouped under
libs/jzswag/):jzswag-api— platform-agnostic interfaces and value typesjzswag-shared— OpenAPI dispatch core, parameter encoding, OAuth2/OAuth1 token flow, YAML loaderjzswag-jvm— JVM client (JDK 11 HttpClient, OS keychain)jzswag-android— Android client (OkHttp + Android Keystore + AES-GCM SharedPreferences)Both clients implement zserio's
ServiceClientInterface, so the idiomaticMyService.MyServiceClient(transport)usage works identically to the C++ and Python flavours.Side effects to review carefully:
docs/restructured into per-language pages (python.md,cpp.md,java.md,openapi-generator.md). The sharedHTTP_SETTINGS_FILEformat stays in the README so the devportal pymdown markers ([env],[settings]) keep working at their original source path.CI/coverage: new
jzswag.ymlworkflow with JaCoCo + Codecov upload (flagunittests-java) on Linux + macOS. 212 unit tests across the four Java modules —jzswag-apiat ~99% line coverage, the others 60%+. The existing C++/Python workflows are untouched.