This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/master by this push:
new 21a2891312 Fix remaining Section A docs gaps, BinaryFormat byte[]
handling, GitControl SecretStore adoption, README refresh (TODO-350/352/353/354)
21a2891312 is described below
commit 21a2891312121e2cb468317c7d41736e9033e61d
Author: James Bognar <[email protected]>
AuthorDate: Thu Aug 13 13:50:53 2026 -0400
Fix remaining Section A docs gaps, BinaryFormat byte[] handling, GitControl
SecretStore adoption, README refresh (TODO-350/352/353/354)
TODO-350: completes Section A of the docs-gap sweep — all remaining MED and
LOW items (fabricated-API corrections, method renames, dead links,
compile-breaking snippets) across ~55 juneau-docs topic pages. Section A is now
100% triaged.
TODO-353: Bson/Cbor/MsgPack serializers now honor the configured
BinaryFormat for byte[] output, and the parser sessions decode
BinaryFormat-encoded byte[] elements instead of a lossy UTF-8 conversion (a
latent parse-side bug exposed once the write side stopped emitting native
binary). Untyped-Map.class recovery limits documented as inherent.
TODO-352: GitControl adopts the new SecretStore SPI via an opt-in
BeanStore-resolved constructor (char[] token lookup, close() zeroes the
provider); anonymous and caller-supplied constructors unchanged, no
default-behavior regression. Docs gain a SecretStore/BeanStore credential
snippet.
TODO-354: refreshes the root README — MCP section, aggregator-grouped
module inventory (incl. juneau-secret-keychain and
juneau-rest-server-datatables), 10.0.0-SNAPSHOT pins, and a 9.x->10.0 upgrade
callout. Also adds the juneau-secret-keychain docs-inventory row.
---
README.md | 75 +++++++++++------
.../juneau/marshall/bson/BsonParserSession.java | 7 ++
.../marshall/bson/BsonSerializerSession.java | 10 ++-
.../juneau/marshall/cbor/CborParserSession.java | 9 ++-
.../marshall/cbor/CborSerializerSession.java | 6 +-
.../marshall/msgpack/MsgPackParserSession.java | 9 ++-
.../marshall/msgpack/MsgPackSerializerSession.java | 6 +-
.../juneau/marshall/bson/BsonSerializer_Test.java | 38 +++++++++
.../juneau/marshall/cbor/CborSerializer_Test.java | 37 +++++++++
.../marshaller/MarshallerVariantClass_Test.java | 49 ++++++-----
.../marshall/msgpack/MsgPackSerializerTest.java | 44 ++++++++++
.../transforms/BinaryFormat_RoundTrip_Test.java | 23 ++++--
.../ByteArrayBase64Swap_ComboRoundTripTest.java | 40 ++++-----
juneau-sc/juneau-sc-server/pom.xml | 6 ++
.../server/config/repository/GitControl.java | 75 ++++++++++++++++-
.../server/config/repository/GitControl_Test.java | 94 ++++++++++++++++++++++
16 files changed, 450 insertions(+), 78 deletions(-)
diff --git a/README.md b/README.md
index 0bc8945754..482576a1ef 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,8 @@
Apache Juneau™ is a single cohesive Java ecosystem consisting of a
comprehensive toolkit for marshalling POJOs to a wide variety of content types
using a common framework, along with universal REST server and client APIs for
creating Swagger-based self-documenting REST interfaces.
+> **⚠️ Upgrading from 9.x?** The project is currently developing the
**10.0.0** release, which includes a number of breaking changes (the public
`ObjectRest` class removed, `SerializerSet`/`ParserSet` lookups now returning
`Optional`, the next-gen `RestClient` no longer implicitly defaulting to JSON,
the `juneau-assertions`/`juneau-bct`/`juneau-junit5` modules merged into a
single `juneau-test` artifact, and the legacy
`juneau-my-jetty-microservice`/`juneau-examples-rest` modules removed [...]
+
## Key Features
* **Universal Serialization** - Marshal POJOs to JSON, XML, HTML,
URL-Encoding, UON, MessagePack, CSV, and more
@@ -34,8 +36,20 @@ Apache Juneau™ is a single cohesive Java ecosystem
consisting of a comprehensi
* **Fluent Assertions** - Powerful testing framework with fluent-style
assertions
* **Type Conversion** - Lightweight, MarshallingContext-free converter
framework with caching and broad type support
* **Large-Dataset Streaming** - BeanSupplier/BeanConsumer/BeanChannel APIs for
serializing and parsing large datasets without loading all elements into
memory; supports direct database integration via lifecycle methods
(begin/acceptThrows/onError/complete)
+* **MCP (Model Context Protocol) Support** - First-party server and client
support for exposing REST resources as MCP tools/prompts/resources, or
consuming MCP servers from Java, for both the `2025-06-18` and `2026-07-28`
protocol revisions
* **Zero Dependencies** - Core marshalling requires no external dependencies
+## MCP (Model Context Protocol) Support
+
+Apache Juneau ships first-party [MCP](https://modelcontextprotocol.io/)
integration, built the same way as the rest of the framework:
annotation-driven, POJO-based, no magic. The module family is split into
revision-neutral **cores** plus thin **adapters** per protocol revision, so a
`2025-06-18`-only deployment never pulls in `2026-07-28`-only dependencies
(OAuth 2.1, JWT, reactive-streams SSE, etc.):
+
+* **juneau-bean-jsonrpc** - revision-neutral JSON-RPC 2.0 envelope beans, with
`juneau-bean-mcp-v20250618` / `juneau-bean-mcp-v20260728` adapters for each
revision's wire beans.
+* **juneau-rest-server-mcp** - revision-neutral server dispatch core, with
`juneau-rest-server-mcp-v20250618` / `juneau-rest-server-mcp-v20260728`
adapters for exposing tools, prompts, and resources (dedicated servlet or
drop-in mixin, plain or Spring Boot).
+* **juneau-rest-client-mcp** - revision-neutral client core, with
`juneau-rest-client-mcp-v20250618` / `juneau-rest-client-mcp-v20260728` typed
client facades, plus `juneau-rest-client-mcp-auth` for the client-side OAuth
2.1 acquisition flow.
+* **juneau-examples-mcp** - a runnable first-party example (notes-service
demo, with plain, Spring Boot, and OAuth-secured variants).
+
+The `2026-07-28` revision is a strict superset of `2025-06-18` and is where
new capability work (Multi-Round-Trip Requests/elicitation, subscriptions,
cache hints, trace-context propagation) lands going forward. See **[MCP (Model
Context Protocol)](https://juneau.apache.org/docs/topics/JuneauMcp)** for the
quickstart, setup guide, and full API reference.
+
## Documentation & Resources
### Official Resources
@@ -69,7 +83,7 @@ Apache Juneau™ excels in the following scenarios:
* **Serverless unit testing of REST APIs** - Test REST services without
servlet containers using MockRestClient for fast, comprehensive testing
* **Microservice development** - Build lightweight microservices with embedded
Jetty or Spring Boot integration
* **Data transformation and mapping** - Convert between different data formats
and handle complex object hierarchies with swap mechanisms
-* **Bean-Centric Testing and fluent-style assertions** - Write readable test
assertions with comprehensive validation capabilities using juneau-bct and
juneau-assertions
+* **Bean-Centric Testing and fluent-style assertions** - Write readable test
assertions with comprehensive validation capabilities using juneau-test
* **Content negotiation and HTTP/2 support** - Handle multiple content types
automatically with modern HTTP features
## Getting Started in 5 Minutes
@@ -80,10 +94,12 @@ Apache Juneau™ excels in the following scenarios:
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-shaded-all</artifactId>
- <version>9.1.0</version>
+ <version>10.0.0-SNAPSHOT</version>
</dependency>
```
+> `10.0.0` is currently under development (tracking `-SNAPSHOT` builds) and
has not yet been released. See the
[Downloads](https://juneau.apache.org/downloads) page for the latest released
version.
+
### 2. Serialize a POJO to JSON
```java
@@ -282,27 +298,37 @@ microservice.start();
## Description
-Apache Juneau™ is a single cohesive Java ecosystem consisting of the following
parts:
-
-* **juneau-marshall** - A universal toolkit for marshalling POJOs to a
variety of content types using a common framework with no external library
dependencies.
-* **juneau-marshall-rdf** - Additional support for various RDF languages.
-* **juneau-bean-atom, juneau-bean-common, juneau-bean-html5,
juneau-bean-jsonschema, juneau-bean-openapi-v3** - A variety of predefined
serializable beans such as HTML5, Swagger and ATOM.
-* **juneau-config** - A sophisticated configuration file API.
-* **juneau-assertions** - Fluent-style assertions API.
-* **juneau-bct** - Bean-Centric Testing framework that extends JUnit with
streamlined assertion methods for Java objects.
-* **juneau-svl** - Simple Variable Language for dynamic string processing.
-* **juneau-rest-common** - REST APIs common to client and server side.
-* **juneau-rest-server** - A universal REST server API for creating
Swagger-based self-documenting REST interfaces using POJOs, simply deployed as
one or more top-level servlets in any Servlet 3.1.0+ container. Includes Spring
Boot and JAX-RS integration support.
-* **juneau-rest-client** - A universal REST client API for interacting with
Juneau or 3rd-party REST interfaces using POJOs and proxy interfaces.
-* **juneau-rest-server-springboot** - Spring boot integration for
juneau-rest-servlet.
-* **juneau-rest-mock** - REST testing API.
-* **juneau-microservice** - Microservice API.
-* **juneau-microservice-jetty** - Jetty microservice API.
-* **juneau-examples-core** - Core code examples.
-* **juneau-petstore-core** - Shared petstore domain + REST resources.
-* **juneau-petstore-jetty** - Jetty/Microservice deployment of the petstore.
-* **juneau-petstore-springboot** - Spring Boot deployment of the petstore.
-* **juneau-shaded** - Shaded (uber) JARs combining multiple Juneau modules for
simplified dependency management, especially useful for Bazel builds.
+Apache Juneau™ is a single cohesive Java ecosystem consisting of the following
parts, grouped by aggregator module. For the complete, always-current
per-artifact list, see the [Juneau Ecosystem
Overview](https://juneau.apache.org/docs/topics/JuneauEcosystemOverview).
+
+* **juneau-core** - Core marshalling and support APIs, with no external
dependencies unless noted:
+ * **juneau-commons** - Shared low-level utilities used across the ecosystem,
including the Simple Variable Language (SVL) for dynamic string processing.
+ * **juneau-marshall** - A universal toolkit for marshalling POJOs to
a variety of content types using a common framework with no external library
dependencies.
+ * **juneau-marshall-rdf** - Additional support for various RDF languages.
+ * **juneau-config** - A sophisticated configuration file API.
+ * **juneau-test** - Unified test-support API combining fluent-style
assertions, Bean-Centric Testing, and JUnit 5 extensions (replaces the former
`juneau-assertions`/`juneau-bct`/`juneau-junit5` artifacts).
+* **juneau-bean** - Predefined serializable beans: **juneau-bean-atom**,
**juneau-bean-common**, **juneau-bean-hal**, **juneau-bean-html5**,
**juneau-bean-jsonapi**, **juneau-bean-jsonpatch**, **juneau-bean-jsonrpc**,
**juneau-bean-jsonschema**, **juneau-bean-openapi-v3**,
**juneau-bean-rfc7807**, **juneau-bean-swagger-v2** - such as HTML5,
Swagger/OpenAPI, ATOM, HAL, JSON:API, JSON Patch, and RFC 7807 Problem Details.
(See [MCP Support](#mcp-model-context-protocol-support) above for the [...]
+* **juneau-rest** - REST server and client APIs:
+ * **juneau-rest-common** / **juneau-rest-common-classic** - REST APIs common
to client and server side (next-gen vs. classic client stack).
+ * **juneau-rest-server** - A universal REST server API for creating
Swagger-based self-documenting REST interfaces using POJOs, simply deployed as
one or more top-level servlets in any Servlet 3.1.0+ container. Includes Spring
Boot and JAX-RS integration support.
+ * **juneau-rest-server-rdf** - RDF support for the REST server.
+ * **juneau-rest-server-springboot** - Spring Boot integration for
juneau-rest-server.
+ * **juneau-rest-server-auth-jwt, -saml, -oauth, -oidc-rp** and
**juneau-rest-auth-oauth-flow** - Authentication/authorization add-ons (JWT,
SAML, OAuth, OpenID Connect RP).
+ * **juneau-rest-server-metrics-micrometer, -tracing-otel,
-management-logging** - Observability add-ons (Micrometer metrics,
OpenTelemetry tracing, request/response logging).
+ * **juneau-rest-server-datatables** - Server-side processing adapter (plus
browser-side helpers) for [DataTables](https://datatables.net/).
+ * **juneau-rest-server-reactive, -reactive-reactor** - Reactive-streams
(SSE) response support.
+ * **juneau-rest-server-view-jsp, -thymeleaf, -mustache, -freemarker** -
View-engine add-ons.
+ * **juneau-rest-client** / **juneau-rest-client-classic** - A universal REST
client API for interacting with Juneau or 3rd-party REST interfaces using POJOs
and proxy interfaces (next-gen vs. classic client stack), with
**juneau-rest-client-apache-httpclient-45/-50**, **juneau-rest-client-jetty**,
and **juneau-rest-client-okhttp** transport backends.
+ * **juneau-rest-mock** - REST testing API.
+ * MCP client/server modules - see [MCP
Support](#mcp-model-context-protocol-support) above.
+* **juneau-microservice** - **juneau-microservice**,
**juneau-microservice-jetty**, **juneau-microservice-tomcat**,
**juneau-microservice-test**, **juneau-microservice-examples** - Lightweight
standalone microservice APIs (Jetty and Tomcat), plus a JUnit 5 test harness.
+* **juneau-sc** - **juneau-sc-server** - Git-backed source-control
configuration server integration (`GitControl`, etc.).
+* **juneau-secret-keychain** - Opt-in `SecretStore` implementation backed by
the macOS `security` keychain CLI (implements the `SecretStore` SPI in
`juneau-commons`).
+* **juneau-examples** - **juneau-examples-core** - Core code examples;
**juneau-examples-mcp** - Runnable MCP example (notes-service demo).
+* **juneau-petstore** - **juneau-petstore-core** - Shared petstore domain +
REST resources; **juneau-petstore-jetty** - Jetty/Microservice deployment;
**juneau-petstore-springboot** - Spring Boot deployment.
+* **juneau-shaded** - Shaded (uber) JARs combining multiple Juneau modules for
simplified dependency management, especially useful for Bazel builds:
**juneau-shaded-core**, **juneau-shaded-rest-client**,
**juneau-shaded-rest-server**, **juneau-shaded-rest-server-springboot**, and
**juneau-shaded-all**.
+* **juneau-bundles** - Curated dependency bundles pulling a coherent module
set per deployment shape: **juneau-microservice-jetty-bundle**,
**juneau-microservice-tomcat-bundle**, **juneau-springboot-bundle**,
**juneau-observability-otlp-bundle**.
+* **juneau-bom** - Published Maven Bill-of-Materials for version-aligning
Juneau dependencies.
+* **juneau-distrib** - Release distribution assembly.
Questions via email to [email protected] are always welcome.
@@ -317,8 +343,9 @@ Juneau is packed with features that may not be obvious at
first. Users are encou
* Exhaustively tested
* Lots of up-to-date documentation and examples
* Minimal library dependencies:
- * juneau-marshall, juneau-bean-atom, juneau-bean-common, juneau-bean-html5,
juneau-bean-jsonschema, juneau-bean-openapi-v3, juneau-svl, juneau-config - No
external dependencies. Entirely self-contained.
+ * juneau-commons, juneau-marshall, juneau-bean-atom, juneau-bean-common,
juneau-bean-html5, juneau-bean-jsonschema, juneau-bean-openapi-v3,
juneau-config - No external dependencies. Entirely self-contained.
* juneau-marshall-rdf - Optional RDF support. Requires Apache Jena 5.6.0+.
+ * juneau-test - Requires opentest4j (JUnit Jupiter is a provided-scope
dependency, supplied by your test runtime).
* juneau-rest-server - Any Servlet 3.1.0+ container.
* juneau-rest-client - Apache HttpClient 4.5+.
* Built on top of Servlet and Apache HttpClient APIs that allow you to use the
newest HTTP/2 features such as request/response multiplexing and server push.
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
index 614b572232..553c889cdc 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonParserSession.java
@@ -125,6 +125,13 @@ public class BsonParserSession extends
InputStreamParserSession implements Recor
yield null;
}
};
+ // A byte[]-targeted string element is a BinaryFormat-encoded
payload (TODO-353 write-side fix
+ // counterpart) rather than a literal string value - decode it
back to bytes.
+ if (elementType == 0x02 && nn(targetType) &&
targetType.isByteArray()) {
+ var binaryFormat = getBinaryFormat();
+ if (binaryFormat != BinaryFormat.NOT_SET)
+ o = binaryFormat.parse((String)o);
+ }
if (nn(parentBean()) && nn(o))
setParent(targetType, o, parentBean());
return o;
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
index 5cba3f91e2..74885553c5 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/bson/BsonSerializerSession.java
@@ -202,8 +202,14 @@ public class BsonSerializerSession extends
OutputStreamSerializerSession impleme
out.writeChildDocument(child);
}
} else if (sType.isByteArray()) {
- out.writeElement(BINARY.value, name);
- out.writeBinary((byte[])o);
+ var binaryFormat = getBinaryFormat();
+ if (binaryFormat == BinaryFormat.NOT_SET) {
+ out.writeElement(BINARY.value, name);
+ out.writeBinary((byte[])o);
+ } else {
+ out.writeElement(STRING.value, name);
+ out.writeString(binaryFormat.format((byte[])o));
+ }
} else if (sType.isCollection() || sType.isArray()) {
out.writeElement(ARRAY.value, name);
var child = out.createChild();
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
index b202f9fc38..8e8e76a30d 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborParserSession.java
@@ -280,7 +280,14 @@ public class CborParserSession extends
InputStreamParserSession implements Token
} else if (sType.isBoolean() || sType.isCharSequence()
|| sType.isChar() || sType.isNumber() || sType.isByteArray()) {
// Merged scalar tier: one CBOR data type
(BOOLEAN/UINT/NINT/FLOAT/STRING/BINARY) covers
// many scalar Java types — the read already
happened above and convertToType narrows.
- o = convertToType(o, sType);
+ // A byte[]-targeted string element is a
BinaryFormat-encoded payload (TODO-353 write-side
+ // fix counterpart) rather than a literal
string - decode it back to bytes directly, since
+ // the generic String-to-byte[] conversion
doesn't know about BinaryFormat.
+ var binaryFormat = getBinaryFormat();
+ if (dt == STRING && sType.isByteArray() &&
binaryFormat != BinaryFormat.NOT_SET)
+ o = binaryFormat.parse((String)o);
+ else
+ o = convertToType(o, sType);
} else if (sType.isDate()) {
o = readDate(String.valueOf(o), sType);
} else if (sType.isCalendar()) {
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
index fb28689b95..e0a1168a4f 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/cbor/CborSerializerSession.java
@@ -218,7 +218,11 @@ public class CborSerializerSession extends
OutputStreamSerializerSession impleme
} else if (sType.isCollection()) {
writeCollection(out, (Collection)o, eType);
} else if (sType.isByteArray()) {
- out.appendBinary((byte[])o);
+ var binaryFormat = getBinaryFormat();
+ if (binaryFormat == BinaryFormat.NOT_SET)
+ out.appendBinary((byte[])o);
+ else
+
out.appendString(binaryFormat.format((byte[])o));
} else if (sType.isArray()) {
writeCollection(out, toList(sType.inner(), o), eType);
} else if (sType.isBoolean()) {
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
index 32979b4ea2..e52aa04785 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackParserSession.java
@@ -310,7 +310,14 @@ public class MsgPackParserSession extends
InputStreamParserSession implements To
} else if (sType.isBoolean() || sType.isCharSequence()
|| sType.isChar() || sType.isNumber() || sType.isByteArray()) {
// Merged scalar tier: one wire-type
(BOOLEAN/INT/LONG/FLOAT/DOUBLE/STRING/BIN) covers
// many scalar Java types — the read already
happened above and convertToType narrows.
- o = convertToType(o, sType);
+ // A byte[]-targeted string element is a
BinaryFormat-encoded payload (TODO-353 write-side
+ // fix counterpart) rather than a literal
string - decode it back to bytes directly, since
+ // the generic String-to-byte[] conversion
doesn't know about BinaryFormat.
+ var binaryFormat = getBinaryFormat();
+ if (dt == STRING && sType.isByteArray() &&
binaryFormat != BinaryFormat.NOT_SET)
+ o = binaryFormat.parse((String)o);
+ else
+ o = convertToType(o, sType);
} else if (sType.isDate()) {
o = readDate(String.valueOf(o), sType);
} else if (sType.isCalendar()) {
diff --git
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
index 7422596c3b..3396456ea6 100644
---
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
+++
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerSession.java
@@ -280,7 +280,11 @@ public class MsgPackSerializerSession extends
OutputStreamSerializerSession impl
} else if (sType.isCollection()) {
writeCollection(out, (Collection)o, eType);
} else if (sType.isByteArray()) {
- out.appendBinary((byte[])o);
+ var binaryFormat = getBinaryFormat();
+ if (binaryFormat == BinaryFormat.NOT_SET)
+ out.appendBinary((byte[])o);
+ else
+
out.appendString(binaryFormat.format((byte[])o));
} else if (sType.isArray()) {
writeCollection(out, toList(sType.inner(), o), eType);
} else if (sType.isBoolean()) {
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
index 4863605212..4d83a3efae 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/bson/BsonSerializer_Test.java
@@ -24,6 +24,7 @@ import java.time.*;
import java.util.*;
import org.apache.juneau.*;
+import org.apache.juneau.marshall.*;
import org.apache.juneau.marshall.collections.*;
import org.junit.jupiter.api.*;
@@ -109,5 +110,42 @@ class BsonSerializer_Test extends TestBase {
assertEquals("LARGE", parsed.get("size"));
}
+ @Test
+ void a09_writeByteArraySpacedHexHonorsBinaryFormat() throws Exception {
+ var bean = new BeanWithBytes();
+ var defaultOut = BsonSerializer.DEFAULT.write(bean);
+ var spacedHexOut =
BsonSerializer.DEFAULT_SPACED_HEX.write(bean);
+ assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex
output should differ from the native binary output");
+ var parsed = BsonParser.DEFAULT_SPACED_HEX.read(spacedHexOut,
BeanWithBytes.class);
+ assertArrayEquals(bean.data, parsed.data);
+ }
+
+ @Test
+ void a10_writeByteArrayBase64HonorsBinaryFormat() throws Exception {
+ var bean = new BeanWithBytes();
+ var defaultOut = BsonSerializer.DEFAULT.write(bean);
+ var base64Out = BsonSerializer.DEFAULT_BASE64.write(bean);
+ assertFalse(Arrays.equals(defaultOut, base64Out), "Base64
output should differ from the native binary output");
+ var parsed = BsonParser.DEFAULT_BASE64.read(base64Out,
BeanWithBytes.class);
+ assertArrayEquals(bean.data, parsed.data);
+ }
+
+ @Test
+ void a11_writeByteArrayNotSetKeepsNativeBinary() throws Exception {
+ // NOT_SET (the default on BsonSerializer.DEFAULT) is
unaffected by the SpacedHex/Base64 fix -- byte[]
+ // values still use BSON's native binary element (subtype 0x05)
rather than a string encoding.
+ var bean = new BeanWithBytes();
+ var s =
BsonSerializer.create().keepNullProperties().binaryFormat(BinaryFormat.NOT_SET).build();
+ var bytes = s.write(bean);
+ var p = BsonParser.create().build();
+ var parsed = p.read(bytes, JsonMap.class);
+ assertArrayEquals(bean.data, (byte[])parsed.get("data"));
+ }
+
+ /** Bean with a single byte[] property, used to exercise the
MarshalledPropertyPostProcessor bean-property path. */
+ public static class BeanWithBytes {
+ public byte[] data = { 0x01, 0x02, 0x03 };
+ }
+
enum Size { SMALL, MEDIUM, LARGE }
}
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
index 44e1e0426a..c4c77db04f 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/cbor/CborSerializer_Test.java
@@ -21,6 +21,8 @@ import static org.apache.juneau.commons.utils.Shorts.*;
import static org.apache.juneau.commons.utils.StringUtils.*;
import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+
import org.apache.juneau.*;
import org.apache.juneau.marshall.*;
import org.apache.juneau.marshall.collections.*;
@@ -254,6 +256,41 @@ class CborSerializer_Test extends TestBase {
CborSerializer.DEFAULT_BASE64.write(JsonMap.of("a",
1)), JsonMap.class).getInt("a"));
}
+ @Test
+ void c21_spacedHexByteArrayPropertyHonorsBinaryFormat() throws
Exception {
+ var bean = new BeanWithBytes();
+ var defaultOut = CborSerializer.DEFAULT.write(bean);
+ var spacedHexOut =
CborSerializer.DEFAULT_SPACED_HEX.write(bean);
+ assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex
output should differ from the native binary output");
+ var parsed = CborParser.DEFAULT_SPACED_HEX.read(spacedHexOut,
BeanWithBytes.class);
+ assertArrayEquals(bean.data, parsed.data);
+ }
+
+ @Test
+ void c22_base64ByteArrayPropertyHonorsBinaryFormat() throws Exception {
+ var bean = new BeanWithBytes();
+ var defaultOut = CborSerializer.DEFAULT.write(bean);
+ var base64Out = CborSerializer.DEFAULT_BASE64.write(bean);
+ assertFalse(Arrays.equals(defaultOut, base64Out), "Base64
output should differ from the native binary output");
+ var parsed = CborParser.DEFAULT_BASE64.read(base64Out,
BeanWithBytes.class);
+ assertArrayEquals(bean.data, parsed.data);
+ }
+
+ @Test
+ void c23_spacedHexTopLevelByteArrayHonorsBinaryFormat() throws
Exception {
+ var data = new byte[] { 1, 2, 3 };
+ var defaultOut = CborSerializer.DEFAULT.write(data);
+ var spacedHexOut =
CborSerializer.DEFAULT_SPACED_HEX.write(data);
+ assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex
output should differ from the native binary output");
+ var parsed = CborParser.DEFAULT_SPACED_HEX.read(spacedHexOut,
byte[].class);
+ assertArrayEquals(data, parsed);
+ }
+
+ /** Bean with a single byte[] property, used to exercise the
MarshalledPropertyPostProcessor bean-property path. */
+ public static class BeanWithBytes {
+ public byte[] data = { 1, 2, 3 };
+ }
+
public static class Bean1 {
public String s;
public int i;
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
index 8982e3a2e3..a627a28e46 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/marshaller/MarshallerVariantClass_Test.java
@@ -327,43 +327,54 @@ class MarshallerVariantClass_Test extends TestBase {
assertBean(m, "a", "1");
}
- // NOTE: BsonSerializer/CborSerializer/MsgPackSerializer currently
always write byte[] values using
- // each format's native binary wire type regardless of the configured
BinaryFormat (SPACED_HEX /
- // BASE64 only affects other conversion paths) -- so for a POJO with a
byte[] field, the SpacedHex/
- // Base64 variant serializers produce byte-identical output to DEFAULT.
These tests therefore verify
- // wiring (equality to the authoritative *Serializer.DEFAULT_*
constant) + round-trip rather than a
- // visibly distinct serialized form.
+ // NOTE (TODO-353): BsonSerializer/CborSerializer/MsgPackSerializer now
honor the configured
+ // BinaryFormat for byte[] output -- SPACED_HEX/BASE64 switch the
byte[] wire representation from
+ // each format's native binary opcode to that format's native string
type containing the
+ // spaced-hex/base64 text, so the SpacedHex/Base64 variant serializers
produce visibly distinct
+ // output from DEFAULT for a POJO with a byte[] field. These tests
verify both the distinct wire
+ // form and the round-trip back to the original bytes.
+ //
+ // The round-trip parse is done with an explicit Map<String,byte[]>
type hint rather than the bare
+ // Map.class used elsewhere in this file: once byte[] is on the wire as
a string (SPACED_HEX/BASE64),
+ // the string is indistinguishable from a genuine text value without a
type hint telling the parser
+ // the target is byte[] -- this is expected, since the encoding trades
the format's native
+ // self-describing binary tag for a text representation that needs the
same external typing a text
+ // serializer (e.g. JSON) would need to recover byte[] from a string.
@Test void b23_bsonSpacedHexRoundTrip() throws Exception {
var bean = Map.of("a", new byte[]{1, 2, 3});
var out = BsonSpacedHex.of(bean);
assertArrayEquals(BsonSerializer.DEFAULT_SPACED_HEX.write(bean), out);
- var m = BsonSpacedHex.to(out, Map.class);
- assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+ assertFalse(Arrays.equals(Bson.of(bean), out), "SpacedHex
output should differ from the native binary output");
+ Map<String,byte[]> m = BsonSpacedHex.to(out, Map.class,
String.class, byte[].class);
+ assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
}
@Test void b24_bsonBase64RoundTrip() throws Exception {
var bean = Map.of("a", new byte[]{1, 2, 3});
var out = BsonBase64.of(bean);
assertArrayEquals(BsonSerializer.DEFAULT_BASE64.write(bean),
out);
- var m = BsonBase64.to(out, Map.class);
- assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+ assertFalse(Arrays.equals(Bson.of(bean), out), "Base64 output
should differ from the native binary output");
+ Map<String,byte[]> m = BsonBase64.to(out, Map.class,
String.class, byte[].class);
+ assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
}
@Test void b25_cborSpacedHexRoundTrip() throws Exception {
var bean = Map.of("a", new byte[]{1, 2, 3});
var out = CborSpacedHex.of(bean);
assertArrayEquals(CborSerializer.DEFAULT_SPACED_HEX.write(bean), out);
- var m = CborSpacedHex.to(out, Map.class);
- assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+ assertFalse(Arrays.equals(Cbor.of(bean), out), "SpacedHex
output should differ from the native binary output");
+ Map<String,byte[]> m = CborSpacedHex.to(out, Map.class,
String.class, byte[].class);
+ assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
}
@Test void b26_cborBase64RoundTrip() throws Exception {
var bean = Map.of("a", new byte[]{1, 2, 3});
var out = CborBase64.of(bean);
assertArrayEquals(CborSerializer.DEFAULT_BASE64.write(bean),
out);
- var m = CborBase64.to(out, Map.class);
- assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+ assertFalse(Arrays.equals(Cbor.of(bean), out), "Base64 output
should differ from the native binary output");
+ Map<String,byte[]> m = CborBase64.to(out, Map.class,
String.class, byte[].class);
+ assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
}
@Test void b27_cborNativeRoundTrip() throws Exception {
@@ -381,16 +392,18 @@ class MarshallerVariantClass_Test extends TestBase {
var bean = Map.of("a", new byte[]{1, 2, 3});
var out = MsgPackSpacedHex.of(bean);
assertArrayEquals(MsgPackSerializer.DEFAULT_SPACED_HEX.write(bean), out);
- var m = MsgPackSpacedHex.to(out, Map.class);
- assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+ assertFalse(Arrays.equals(MsgPack.of(bean), out), "SpacedHex
output should differ from the native binary output");
+ Map<String,byte[]> m = MsgPackSpacedHex.to(out, Map.class,
String.class, byte[].class);
+ assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
}
@Test void b29_msgPackBase64RoundTrip() throws Exception {
var bean = Map.of("a", new byte[]{1, 2, 3});
var out = MsgPackBase64.of(bean);
assertArrayEquals(MsgPackSerializer.DEFAULT_BASE64.write(bean),
out);
- var m = MsgPackBase64.to(out, Map.class);
- assertArrayEquals(new byte[]{1, 2, 3}, (byte[]) m.get("a"));
+ assertFalse(Arrays.equals(MsgPack.of(bean), out), "Base64
output should differ from the native binary output");
+ Map<String,byte[]> m = MsgPackBase64.to(out, Map.class,
String.class, byte[].class);
+ assertArrayEquals(new byte[]{1, 2, 3}, m.get("a"));
}
@Test void b30_msgPackNativeRoundTrip() throws Exception {
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
index aa576f6cf0..0debca23f2 100755
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/msgpack/MsgPackSerializerTest.java
@@ -21,6 +21,8 @@ import static org.apache.juneau.commons.utils.StringUtils.*;
import static org.apache.juneau.marshall.marshaller.MarshallUtils.*;
import static org.junit.jupiter.api.Assertions.*;
+import java.util.*;
+
import org.apache.juneau.*;
import org.apache.juneau.marshall.json5.*;
import org.junit.jupiter.api.*;
@@ -217,6 +219,48 @@ class MsgPackSerializerTest extends TestBase {
public int age = 21;
}
+ /** Bean with a single byte[] property, used to exercise the
MarshalledPropertyPostProcessor bean-property path. */
+ public static class BeanWithBytes {
+ public byte[] data = { 1, 2, 3 };
+ }
+
+
//====================================================================================================
+ // BinaryFormat honored for byte[] output (TODO-353)
+
//====================================================================================================
+ @Test void b01_spacedHexByteArrayPropertyHonorsBinaryFormat() throws
Exception {
+ var bean = new BeanWithBytes();
+ var defaultOut = MsgPackSerializer.DEFAULT.write(bean);
+ var spacedHexOut =
MsgPackSerializer.DEFAULT_SPACED_HEX.write(bean);
+ assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex
output should differ from the native binary output");
+ var parsed =
MsgPackParser.DEFAULT_SPACED_HEX.read(spacedHexOut, BeanWithBytes.class);
+ assertArrayEquals(bean.data, parsed.data);
+ }
+
+ @Test void b02_base64ByteArrayPropertyHonorsBinaryFormat() throws
Exception {
+ var bean = new BeanWithBytes();
+ var defaultOut = MsgPackSerializer.DEFAULT.write(bean);
+ var base64Out = MsgPackSerializer.DEFAULT_BASE64.write(bean);
+ assertFalse(Arrays.equals(defaultOut, base64Out), "Base64
output should differ from the native binary output");
+ var parsed = MsgPackParser.DEFAULT_BASE64.read(base64Out,
BeanWithBytes.class);
+ assertArrayEquals(bean.data, parsed.data);
+ }
+
+ @Test void b03_spacedHexTopLevelByteArrayHonorsBinaryFormat() throws
Exception {
+ var data = new byte[] { 1, 2, 3 };
+ var defaultOut = MsgPackSerializer.DEFAULT.write(data);
+ var spacedHexOut =
MsgPackSerializer.DEFAULT_SPACED_HEX.write(data);
+ assertFalse(Arrays.equals(defaultOut, spacedHexOut), "SpacedHex
output should differ from the native binary output");
+ var parsed =
MsgPackParser.DEFAULT_SPACED_HEX.read(spacedHexOut, byte[].class);
+ assertArrayEquals(data, parsed);
+ }
+
+ @Test void b04_notSetKeepsNativeBinary() throws Exception {
+ // NOT_SET (the default on MsgPackSerializer.DEFAULT) is
unaffected by the SpacedHex/Base64 fix --
+ // byte[] values still use MsgPack's native bin family rather
than a string encoding.
+ var data = new byte[] { 1, 2, 3 };
+ test(data, "C4 03 01 02 03");
+ }
+
private static void test(Object input, String expected) throws
Exception {
var b = msgPack(input);
assertEquals(expected, toSpacedHex(b));
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
index 86f308c571..f9abac69b7 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/BinaryFormat_RoundTrip_Test.java
@@ -68,14 +68,21 @@ import org.junit.jupiter.params.provider.*;
* comes to 42 × 5 = 210 testers per test method.
*
* <p>
- * {@link BinaryFormat} only affects text-based serializers per the
class-level "Binary serializers" note —
- * BSON / CBOR / MsgPack / Prototext / Parquet emit native bytes regardless of
the configured constant. The
- * variant {@code binarySwap} installed by {@code
MarshalledPropertyPostProcessor} respects that by handing
- * the raw {@code byte[]} back to {@link
org.apache.juneau.marshall.serializer.OutputStreamSerializerSession}
- * subtypes instead of the formatted wire string, so bean-property round-trips
through binary serializers
- * still resolve to the original bytes via native handling. Top-level /
{@link List}-element paths route
- * through the default-swap dispatch ({@link
org.apache.juneau.marshall.swaps.BinarySwap}) which short-circuits to
- * raw bytes for binary sessions — same lossless round-trip via the native
path.
+ * {@link BinaryFormat} affects every text-based serializer, plus BSON / CBOR
/ MsgPack (binary serializers
+ * with a native byte-array wire type, fixed under TODO-353): {@link
BinaryFormat#NOT_SET} emits their
+ * native binary opcode as before, while every other constant switches the
{@code byte[]} wire
+ * representation to that format's native string type carrying the
spaced-hex/base64/etc. text — the
+ * variant {@code binarySwap} installed by {@code
MarshalledPropertyPostProcessor} hands the raw
+ * {@code byte[]} straight through to
+ * {@link org.apache.juneau.marshall.serializer.OutputStreamSerializerSession}
subtypes, which make the
+ * NOT_SET/non-NOT_SET decision themselves at their {@code byte[]}-write
dispatch site. Top-level /
+ * {@link List}-element paths route through the default-swap dispatch
+ * ({@link org.apache.juneau.marshall.swaps.BinarySwap}), which likewise
short-circuits to raw bytes for
+ * binary sessions and lets the same per-format dispatch site apply the
configured format. Serializers
+ * without a native byte-array wire type (Parquet, binary RDF) always run
through the swap-formatted text
+ * string. Prototext still has the pre-TODO-353 native-bytes-always bug (out
of scope here — it emits its
+ * native bytes opcode unconditionally); its round-trip below still passes
because it's lossless either way,
+ * not because it honors the configured format. All combos round-trip to the
original bytes regardless.
*/
@SuppressWarnings({
"unused" // Exception parameter intentionally unused in catch block;
only the fact of the exception matters.
diff --git
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
index af22885980..129f23958c 100644
---
a/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
+++
b/juneau-core/juneau-marshall/src/test/java/org/apache/juneau/marshall/transforms/ByteArrayBase64Swap_ComboRoundTripTest.java
@@ -64,8 +64,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("_value=AQID")
.urlEncT("_value=AQID")
.urlEncR("_value=AQID")
- .msgPack("C403010203")
- .msgPackT("C403010203")
+ .msgPack("A441514944")
+ .msgPackT("A441514944")
.verify(x -> verify(x).isType(byte[].class))
.build(),
tester(2, "ByteArray2d", byte[][].class, new
byte[][]{{1,2,3},{4,5,6},null})
@@ -88,8 +88,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("0=AQID&1=BAUG&2=null")
.urlEncT("0=AQID&1=BAUG&2=null")
.urlEncR("0=AQID\n&1=BAUG\n&2=null")
- .msgPack("93C403010203C403040506C0")
- .msgPackT("93C403010203C403040506C0")
+ .msgPack("93A441514944A442415547C0")
+ .msgPackT("93A441514944A442415547C0")
.verify(x -> verify(x).isType(byte[][].class))
.build(),
tester(3, "ListOfByteArrays", getType(List.class,byte[].class),
l(bytes(1,2,3),bytes(4,5,6),null))
@@ -112,8 +112,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("0=AQID&1=BAUG&2=null")
.urlEncT("0=AQID&1=BAUG&2=null")
.urlEncR("0=AQID\n&1=BAUG\n&2=null")
- .msgPack("93C403010203C403040506C0")
- .msgPackT("93C403010203C403040506C0")
+ .msgPack("93A441514944A442415547C0")
+ .msgPackT("93A441514944A442415547C0")
.verify(x -> verify(x).isType(List.class))
.verify(x -> verify(x.get(0)).isType(byte[].class))
.build(),
@@ -137,8 +137,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("foo=AQID&bar=null&null=BAUG&'null'=BwgJ")
.urlEncT("foo=AQID&bar=null&null=BAUG&'null'=BwgJ")
.urlEncR("foo=AQID\n&bar=null\n&null=BAUG\n&'null'=BwgJ")
-
.msgPack("84A3666F6FC403010203A3626172C0C0C403040506A46E756C6CC403070809")
-
.msgPackT("84A3666F6FC403010203A3626172C0C0C403040506A46E756C6CC403070809")
+
.msgPack("84A3666F6FA441514944A3626172C0C0A442415547A46E756C6CA44277674A")
+
.msgPackT("84A3666F6FA441514944A3626172C0C0A442415547A46E756C6CA44277674A")
.verify(x -> verify(x).isType(Map.class))
.verify(x ->
verify(x.keySet().iterator().next()).isType(String.class))
.verify(x ->
verify(x.values().iterator().next()).isType(byte[].class))
@@ -163,8 +163,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("f=AQID")
.urlEncT("f=AQID")
.urlEncR("f=AQID")
- .msgPack("81A166C403010203")
- .msgPackT("81A166C403010203")
+ .msgPack("81A166A441514944")
+ .msgPackT("81A166A441514944")
.verify(x ->
verify(x).isType(BeanWithByteArrayField.class))
.build(),
tester(6, "BeanWithByteArray2dField",
BeanWithByteArray2dField.class, new BeanWithByteArray2dField().init())
@@ -187,8 +187,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("f=@(AQID,BAUG,null)")
.urlEncT("f=@(AQID,BAUG,null)")
.urlEncR("f=@(\n\tAQID,\n\tBAUG,\n\tnull\n)")
- .msgPack("81A16693C403010203C403040506C0")
- .msgPackT("81A16693C403010203C403040506C0")
+ .msgPack("81A16693A441514944A442415547C0")
+ .msgPackT("81A16693A441514944A442415547C0")
.verify(x ->
verify(x).isType(BeanWithByteArray2dField.class))
.build(),
tester(7, "BeanWithByteArrayNullField",
BeanWithByteArrayNullField.class, new BeanWithByteArrayNullField().init())
@@ -235,8 +235,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("f=@(AQID,BAUG,null)")
.urlEncT("f=@(AQID,BAUG,null)")
.urlEncR("f=@(\n\tAQID,\n\tBAUG,\n\tnull\n)")
- .msgPack("81A16693C403010203C403040506C0")
- .msgPackT("81A16693C403010203C403040506C0")
+ .msgPack("81A16693A441514944A442415547C0")
+ .msgPackT("81A16693A441514944A442415547C0")
.verify(x ->
verify(x).isType(BeanWithByteArrayListField.class))
.build(),
tester(9, "BeanWithByteArrayMapField",
BeanWithByteArrayMapField.class, new BeanWithByteArrayMapField().init())
@@ -259,8 +259,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("f=(foo=AQID,bar=null,null=BAUG)")
.urlEncT("f=(foo=AQID,bar=null,null=BAUG)")
.urlEncR("f=(\n\tfoo=AQID,\n\tbar=null,\n\tnull=BAUG\n)")
-
.msgPack("81A16683A3666F6FC403010203A3626172C0C0C403040506")
-
.msgPackT("81A16683A3666F6FC403010203A3626172C0C0C403040506")
+
.msgPack("81A16683A3666F6FA441514944A3626172C0C0A442415547")
+
.msgPackT("81A16683A3666F6FA441514944A3626172C0C0A442415547")
.verify(x ->
verify(x).isType(BeanWithByteArrayMapField.class))
.build(),
tester(10, "BeanWithByteArrayBeanListField",
BeanWithByteArrayBeanListField.class, new
BeanWithByteArrayBeanListField().init())
@@ -283,8 +283,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("f=@((f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),null)")
.urlEncT("f=@((f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),null)")
.urlEncR("f=@(\n\t(\n\t\tf1=AQID,\n\t\tf2=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf3=null,\n\t\tf4=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf5=(\n\t\t\tfoo=AQID,\n\t\t\tbar=null,\n\t\t\tnull=BAUG\n\t\t)\n\t),\n\tnull\n)")
-
.msgPack("81A1669285A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506C0")
-
.msgPackT("81A1669285A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506C0")
+
.msgPack("81A1669285A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547C0")
+
.msgPackT("81A1669285A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547C0")
.verify(x ->
verify(x).isType(BeanWithByteArrayBeanListField.class))
.build(),
tester(11, "BeanWithByteArrayBeanMapField",
BeanWithByteArrayBeanMapField.class, new BeanWithByteArrayBeanMapField().init())
@@ -307,8 +307,8 @@ class ByteArrayBase64Swap_ComboRoundTripTest extends
ComboRoundTripTest_Base {
.urlEnc("f=(foo=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),bar=null,null=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)))")
.urlEncT("f=(foo=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)),bar=null,null=(f1=AQID,f2=@(AQID,BAUG,null),f3=null,f4=@(AQID,BAUG,null),f5=(foo=AQID,bar=null,null=BAUG)))")
.urlEncR("f=(\n\tfoo=(\n\t\tf1=AQID,\n\t\tf2=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf3=null,\n\t\tf4=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf5=(\n\t\t\tfoo=AQID,\n\t\t\tbar=null,\n\t\t\tnull=BAUG\n\t\t)\n\t),\n\tbar=null,\n\tnull=(\n\t\tf1=AQID,\n\t\tf2=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf3=null,\n\t\tf4=@(\n\t\t\tAQID,\n\t\t\tBAUG,\n\t\t\tnull\n\t\t),\n\t\tf5=(\n\t\t\tfoo=AQID,\n\t\t\tbar=null,\n\t\t\tnull=BAUG\n\t\t)\n\t)\n)")
-
.msgPack("81A16683A3666F6F85A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506A3626172C0C085A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506")
-
.msgPackT("81A16683A3666F6F85A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506A3626172C0C085A26631C403010203A2663293C403010203C403040506C0A26633C0A2663493C403010203C403040506C0A2663583A3666F6FC403010203A3626172C0C0C403040506")
+
.msgPack("81A16683A3666F6F85A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547A3626172C0C085A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547")
+
.msgPackT("81A16683A3666F6F85A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547A3626172C0C085A26631A441514944A2663293A441514944A442415547C0A26633C0A2663493A441514944A442415547C0A2663583A3666F6FA441514944A3626172C0C0A442415547")
.verify(x ->
verify(x).isType(BeanWithByteArrayBeanMapField.class))
.build()
};
diff --git a/juneau-sc/juneau-sc-server/pom.xml
b/juneau-sc/juneau-sc-server/pom.xml
index 219b47ac8f..73161f72e4 100644
--- a/juneau-sc/juneau-sc-server/pom.xml
+++ b/juneau-sc/juneau-sc-server/pom.xml
@@ -45,6 +45,12 @@
<artifactId>juneau-microservice-jetty</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
index 24d5090340..a7df635de2 100644
---
a/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
+++
b/juneau-sc/juneau-sc-server/src/main/java/org/apache/juneau/server/config/repository/GitControl.java
@@ -18,7 +18,9 @@ package org.apache.juneau.server.config.repository;
import java.io.*;
+import org.apache.juneau.commons.inject.*;
import org.apache.juneau.commons.logging.*;
+import org.apache.juneau.commons.secret.*;
import org.eclipse.jgit.api.*;
import org.eclipse.jgit.api.errors.*;
import org.eclipse.jgit.internal.storage.file.*;
@@ -34,8 +36,20 @@ import org.eclipse.jgit.transport.*;
* Used to fetch configuration files from a remote Git repository.
*
* <p>
+ * Credentials are never hardcoded in this class. They are either supplied
directly by the caller (via the 5-arg
+ * constructor) or, preferably, resolved at construction time from a pluggable
{@link SecretStore} (via the
+ * {@link #GitControl(String, String, String, String, BeanStore, boolean)
BeanStore/SecretStore} constructor) so the
+ * password/token can be sourced from an environment variable, an OS keychain,
or another secret backend rather than a
+ * literal string.
+ *
+ * <p>
* The underlying JGit {@link Repository} and {@link Git} handles are opened
in the constructor and released by
* {@link #close()}, so instances must be used with try-with-resources to
avoid leaking file/native handles.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='jc'>{@link SecretStore}
+ * <li class='jc'>{@link SecretStores}
+ * </ul>
*/
public class GitControl implements AutoCloseable {
@@ -79,7 +93,7 @@ public class GitControl implements AutoCloseable {
* @throws IOException If the repository cannot be opened.
*/
@SuppressWarnings({
- "resource" // localRepo (FileRepository) and git (Git) are
long-lived fields closed in close(); Git wraps the externally-created
Repository without closing it, so both are released there. Warnings surface at
the constructor assignments (lines 84/87).
+ "resource" // localRepo (FileRepository) and git (Git) are
long-lived fields closed in close(); Git wraps the externally-created
Repository without closing it, so both are released there.
})
public GitControl(String localPath, String remotePath, String username,
String password, boolean forcePush) throws IOException {
this.localPath = localPath;
@@ -90,17 +104,74 @@ public class GitControl implements AutoCloseable {
git = new Git(localRepo);
}
+ /**
+ * Constructor that resolves the Git password/token from a pluggable
{@link SecretStore}.
+ *
+ * <p>
+ * The active store is resolved from the supplied {@link BeanStore} via
{@link SecretStores#resolve(BeanStore)},
+ * defaulting to an {@link InMemorySecretStore} when the bean store is
<jk>null</jk> or contributes none. The
+ * password/token is looked up under <jv>secretKey</jv> as a {@code
char[]} so it is never materialized as a
+ * {@link String}, and the credentials are zeroed by {@link #close()}.
Pass <jk>null</jk> for <jv>username</jv> to
+ * use anonymous transport (the secret store is not consulted in that
case).
+ *
+ * @param localPath Local directory path for the repository.
+ * @param remotePath Remote Git repository URI.
+ * @param username Git username, or <jk>null</jk> for anonymous
transport.
+ * @param secretKey The key under which the password/token is stored in
the resolved {@link SecretStore}.
+ * <br>Ignored when <jv>username</jv> is <jk>null</jk>.
+ * @param beanStore The bean store to resolve the {@link SecretStore}
from. Can be <jk>null</jk> to use the
+ * default {@link InMemorySecretStore}.
+ * @param forcePush Whether {@link #pushToRepo()} performs a force-push
(destructive remote history rewrite).
+ * <br>Force-push is opt-in; defaults to <jk>false</jk> in the
other constructors.
+ * @throws IOException If the repository cannot be opened.
+ */
+ @SuppressWarnings({
+ "resource" // localRepo (FileRepository) and git (Git) are
long-lived fields closed in close(); Git wraps the externally-created
Repository without closing it, so both are released there.
+ })
+ public GitControl(String localPath, String remotePath, String username,
String secretKey, BeanStore beanStore, boolean forcePush) throws IOException {
+ this.localPath = localPath;
+ this.remotePath = remotePath;
+ this.localRepo = new FileRepository(localPath + "/.git");
+ this.cp = findCredentialsProvider(username, secretKey,
beanStore);
+ this.forcePush = forcePush;
+ git = new Git(localRepo);
+ }
+
+ /**
+ * Builds the credentials provider by resolving the password/token from
the {@link SecretStore} contributed to the
+ * supplied bean store.
+ *
+ * <p>
+ * Returns <jk>null</jk> (anonymous transport) when <jv>username</jv>
is <jk>null</jk>. When the secret is absent
+ * from the resolved store an empty password is used, so a caller
cannot accidentally send a stale literal. The
+ * retrieved {@code char[]} is handed to the provider and zeroed by
{@link #close()}.
+ *
+ * @param username Git username, or <jk>null</jk> for anonymous
transport.
+ * @param secretKey The key under which the password/token is stored.
+ * @param beanStore The bean store to resolve the {@link SecretStore}
from. Can be <jk>null</jk>.
+ * @return The credentials provider, or <jk>null</jk> for anonymous
transport.
+ */
+ static CredentialsProvider findCredentialsProvider(String username,
String secretKey, BeanStore beanStore) {
+ if (username == null)
+ return null;
+ var secret =
SecretStores.resolve(beanStore).find(secretKey).orElseGet(() -> new char[0]);
+ return new UsernamePasswordCredentialsProvider(username,
secret);
+ }
+
/**
* Closes the underlying JGit {@link Git} and {@link Repository}
handles.
*
* <p>
* The {@link Git} instance wraps an externally-supplied {@link
Repository} (it does not close it on
- * {@link Git#close()}), so both are released explicitly here.
+ * {@link Git#close()}), so both are released explicitly here. Any
credentials held by the provider are zeroed so
+ * a resolved secret does not linger in memory beyond the life of this
instance.
*/
@Override
public void close() {
git.close();
localRepo.close();
+ if (cp instanceof UsernamePasswordCredentialsProvider cp2)
+ cp2.clear();
}
/**
diff --git
a/juneau-sc/juneau-sc-server/src/test/java/org/apache/juneau/server/config/repository/GitControl_Test.java
b/juneau-sc/juneau-sc-server/src/test/java/org/apache/juneau/server/config/repository/GitControl_Test.java
new file mode 100644
index 0000000000..a7bb48426e
--- /dev/null
+++
b/juneau-sc/juneau-sc-server/src/test/java/org/apache/juneau/server/config/repository/GitControl_Test.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.server.config.repository;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.commons.secret.*;
+import org.eclipse.jgit.transport.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.io.*;
+
+/**
+ * Coverage for {@link GitControl}'s {@link SecretStore}-backed credential
resolution.
+ *
+ * <p>
+ * The {@code findCredentialsProvider} tests exercise the resolution logic
directly without opening a repository; the
+ * construction test confirms the {@link SecretStore} constructor wires the
resolved credentials through without
+ * regressing the anonymous default.
+ */
+class GitControl_Test {
+
+ private static char[] passwordOf(CredentialsProvider cp) throws
Exception {
+ var pass = new CredentialItem.Password();
+ assertTrue(cp.get(new URIish("https://example.com/repo.git"),
pass));
+ return pass.getValue();
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // findCredentialsProvider - the SecretStore resolution logic.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_nullUsernameYieldsAnonymousTransport() {
+ assertNull(GitControl.findCredentialsProvider(null,
"GIT_TOKEN", new BasicBeanStore()));
+ }
+
+ @Test void a02_contributedStoreResolvesSecret() throws Exception {
+ var store = new InMemorySecretStore();
+ store.store("GIT_TOKEN", "hunter2".toCharArray());
+ var beanStore = new BasicBeanStore().addBean(SecretStore.class,
store);
+
+ var cp = GitControl.findCredentialsProvider("svc-account",
"GIT_TOKEN", beanStore);
+ assertInstanceOf(UsernamePasswordCredentialsProvider.class, cp);
+ assertArrayEquals("hunter2".toCharArray(), passwordOf(cp));
+ }
+
+ @Test void a03_absentSecretYieldsEmptyPassword() throws Exception {
+ // No store contributed -> InMemorySecretStore default -> key
absent -> empty password (never a stale literal).
+ var cp = GitControl.findCredentialsProvider("svc-account",
"GIT_TOKEN", null);
+ assertInstanceOf(UsernamePasswordCredentialsProvider.class, cp);
+ assertArrayEquals(new char[0], passwordOf(cp));
+ }
+
+ @Test void a04_emptyBeanStoreFallsBackToInMemoryDefault() throws
Exception {
+ var cp = GitControl.findCredentialsProvider("svc-account",
"GIT_TOKEN", new BasicBeanStore());
+ assertArrayEquals(new char[0], passwordOf(cp));
+ }
+
+ //
-----------------------------------------------------------------------------------------------------------------
+ // Constructor integration.
+ //
-----------------------------------------------------------------------------------------------------------------
+
+ @Test void
b01_secretStoreConstructorWiresCredentialsAndClosesCleanly(@TempDir File dir)
throws IOException {
+ var store = new InMemorySecretStore();
+ store.store("GIT_TOKEN", "hunter2".toCharArray());
+ var beanStore = new BasicBeanStore().addBean(SecretStore.class,
store);
+
+ try (var gitControl = new GitControl(dir.getAbsolutePath(),
"https://example.com/repo.git", "svc-account", "GIT_TOKEN", beanStore, false)) {
+ assertNotNull(gitControl);
+ }
+ }
+
+ @Test void b02_nullUsernameConstructorUsesAnonymousTransport(@TempDir
File dir) throws IOException {
+ try (var gitControl = new GitControl(dir.getAbsolutePath(),
"https://example.com/repo.git", null, "GIT_TOKEN", null, false)) {
+ assertNotNull(gitControl);
+ }
+ }
+}