Release 2.0.1 - #163
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.
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%
Documents the new shared-and-platform split throughout the project: - README.md: Components table now lists all four Java modules with their roles. Quickstart split into JVM and Android variants (the only public-API difference is the Context parameter on the Android ZswagClient constructor). - docs/java.md: full module table, dual-platform code samples. - CLAUDE.md: per-module bullet list, build commands run all four module tests, "Working with the Java client" points reviewers at jzswag-shared for changes that affect parity with C++/Python, plus a note about the Android-on-aarch64 build trade-off. - libs/jzswag-shared/README.md: new file documenting the shared core. - libs/jzswag-android/README.md: new file with full build trade-off rationale + pointers to the platform-specific bits. - libs/jzswag-jvm/README.md: trimmed to JVM-specific content; cross- platform pieces now point to jzswag-shared. - libs/jzswag-api/README.md: refreshed to reflect that all three downstream modules consume from here, plus the new IKeychain interface. CI workflow: - Build & test runs all four Java modules now (api, shared, jvm, android) plus the existing jzswag-test:assemble. - Coverage upload covers all four jacocoTestReport.xml files; min- coverage threshold ratcheted from 25 → 60 to match the parity goal (every module ships ≥60% line coverage). - JaCoCo HTML artifact pattern broadened to libs/jzswag-*/...
Maintainer feedback: the five jzswag-* modules at the libs/ root pollute
the directory next to the C++/Python siblings (httpcl, zswagcl, zswag,
pyzswagcl). Group them under a single libs/jzswag/ folder so the layout
becomes:
libs/
httpcl/ zswagcl/ zswag/ pyzswagcl/ # existing C++/Python
jzswag/
jzswag-api/ jzswag-shared/ jzswag-jvm/
jzswag-android/ jzswag-test/
Module names keep the jzswag- prefix inside the new folder so they line
up 1:1 with their Maven artifactIds (jzswag-api, jzswag-jvm, ...) — no
publication coordinates change.
Mechanical updates only:
- settings.gradle: include 'libs:jzswag:jzswag-X' instead of 'libs:jzswag-X'
- every project(':libs:jzswag-X') → project(':libs:jzswag:jzswag-X')
- jzswag-test/build.gradle: zserioSourceRoot path adjusted by one level
- test-java-client.bash: project_root computation adjusted by one level
- CI workflow: artifact globs and JaCoCo paths point at libs/jzswag/...
- README.md, CLAUDE.md, docs/java.md, per-module READMEs: paths updated
Java packages and Maven artifactIds are unchanged. All four module test
suites still pass (218 tests, ≥60% line coverage on each).
Both noticed during a doc-audit pass: - HttpSettings javadoc said HttpSettingsLoader lives in jzswag-jvm, but it moved to jzswag-shared during the shared-extraction commit so that jzswag-android could reuse it. - IKeychain javadoc claimed AndroidKeychain uses EncryptedSharedPreferences — it doesn't; we explicitly chose raw Android Keystore + manual AES-GCM + plain SharedPreferences because EncryptedSharedPreferences is an AAR dep we can't consume from the java-library plugin (see jzswag-android/build.gradle for the full trade-off).
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.
The OpenAPI Options Interoperability section showed servers as a single ✔️ row with an example using only the historically-supported form (absolute URL with path prefix). Expand to show all three forms from OpenAPI 3.0+ (clarified in 3.2.0 §4.5.2.1) with concrete examples, and split the matrix row to make clear which forms are supported per client. The 'document-relative' row is marked n/a for OAServer and zswag.gen since neither consumes servers[].url at runtime — OAServer routes based on operation paths, zswag.gen emits whatever the user supplies.
Rewrites OpenApiClient.resolveBaseUrl to use java.net.URI.resolve,
which natively implements RFC 3986 §5.3 reference resolution.
Previous implementation handled only:
- Empty server URL (used spec origin)
- URL starting with '/' (path-only, combined with spec origin)
- Absolute URL (returned as-is)
It silently broke for document-relative forms ('.', './v2', '../v2',
bare 'v2') — the else-branch returned them unchanged, producing
nonsense like "." concatenated with the operation path at request time.
The new implementation:
* Converts spec location to java.net.URI (http(s)://, file://, or local
path -> file URI)
* Converts server URL to java.net.URI as a reference
* Returns specBase.resolve(serverRef)
Works for all three URL forms against both HTTP and local-file spec
locations. The earlier "absolute server URL with local-file spec"
behaviour is preserved via an explicit check (avoids producing a
file:// base URL when the server URL is absolute).
OpenApiClientBaseUrlTest covers each reference form via direct URI
resolution (decoupling the test from the spec-fetch path) plus two
end-to-end tests against a real OpenApiClient using a temp-file spec.
Closes #159 (Java side; C++/Python handled in the preceding commit).
codecov/patch on PR #160 flagged 36.36% patch coverage on OpenApiClient.java because the previous test class verified java.net.URI.resolve() in isolation — the production method's branches (file:// fallback, URISyntaxException paths, etc.) weren't hit by any test. Refactor: * Extract resolveBaseUrl(specLocation, serverUrl) as a static package-private helper. The instance method delegates to it. * OpenApiClientBaseUrlTest now calls the static helper directly, exercising every branch including the file:// + relative warning, the malformed-URI path, and the file URI + document-relative case. * Keep one end-to-end test that constructs a real OpenApiClient from a temp-file spec so the wiring stays verified. Tests in this class: 9 -> 13. All 236 Java tests still passing.
# Conflicts: # README.md
# Conflicts: # README.md
Java Coverage (api / shared / jvm / android)
Files
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #163 +/- ##
=============================================
+ Coverage 39.97% 66.79% +26.81%
- Complexity 0 484 +484
=============================================
Files 20 29 +9
Lines 1936 2114 +178
Branches 1142 417 -725
=============================================
+ Hits 774 1412 +638
- Misses 404 509 +105
+ Partials 758 193 -565
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ver-urls # Conflicts: # README.md
Support relative URLs in OpenAPI servers[].url — Java (#159)
josephbirkner
marked this pull request as ready for review
July 6, 2026 13:34
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.
Release 2.0.1
Major zswag release for the upcoming MapViewer 2026.3.1 dependency chain.
v2.0.0was already tagged and published from an earlier release-branch commit. Since PyPI versions are immutable, the finalized release branch is prepared asv2.0.1instead.Release Notes
What's Changed
Python/C++ transport
Java client
servers[].urlhandling.Docs and release infrastructure
v2.0.1.Important:
v2.0.0was already tagged/published from an earlier release-branch commit. This release supersedes it and contains the final release-branch fixes.Full Changelog: v1.14.0...v2.0.1
TODOs
zswag 2.0.1release notes.v2.0.1after merge.