You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2026/05/20 13:10:15 UTC
(juneau) branch master updated: chore: SSE + HAL/JsonApi/JsonPatch/RFC7807 bean modules, msgpack media-type fix, SonarLint cleanups, MockConsole test isolation
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 7d55bc7f4d chore: SSE + HAL/JsonApi/JsonPatch/RFC7807 bean modules, msgpack media-type fix, SonarLint cleanups, MockConsole test isolation
7d55bc7f4d is described below
commit 7d55bc7f4dc73c5e9416ad9b5566faf64f7996b2
Author: James Bognar <ja...@jamesbog-ltmjqf2.internal.salesforce.com>
AuthorDate: Wed May 20 09:10:07 2026 -0400
chore: SSE + HAL/JsonApi/JsonPatch/RFC7807 bean modules, msgpack media-type fix, SonarLint cleanups, MockConsole test isolation
- juneau-marshall: new org.apache.juneau.sse package (SseSerializer/SseParser/SseEvent/SseEventReader + sessions); marshaller.Sse convenience; per-event Writer.flush; SonarLint cleanups (S2095/S115/UncheckedIOException refactor in SseSerializerSession)
- juneau-marshall msgpack: normalize canonical media type to application/msgpack (retain octal/msgpack alias for back-compat)
- juneau-marshall collections: assertArgNotNull in MarshalledList/MarshalledMap parse ctors; java:S110/S2386/S2160 suppressions in JsonMap; package-info touch-ups
- juneau-bean: new juneau-bean-hal, juneau-bean-jsonapi, juneau-bean-jsonpatch, juneau-bean-rfc7807 modules with round-trip tests
- juneau-rest-client-classic: RestClient/RestRequest/RestResponse touch-ups and new RestClient_Logging_StreamClosed_Test (FULL-detail logging on consumed entity)
- juneau-rest-common BasicHttpException: java:S3008/S1948/S1165 field suppressions
- juneau-rest-mock MockConsole: per-instance ByteArrayOutputStream (fixes order-dependent RestClient logging test)
- juneau-examples-rest: SseDemoResource + RootResources wiring
- juneau-utest: new SSE/HAL/JsonApi/JsonPatch/RFC7807/MsgPackMediaType tests plus broad test refresh
- todo: archive TODO-45/46/47 as FINISHED; add TODO-39 (sonarqube command), TODO-4 (duration format control), TODO-48 (marshalled-collection empty-return), TODO-49 (SchemaUtils null-returns); trim TODO-43 from TODO.md
---
juneau-bean/juneau-bean-hal/pom.xml | 102 +++
.../java/org/apache/juneau/bean/hal/HalLink.java | 236 +++++++
.../org/apache/juneau/bean/hal/HalLinkArray.java | 58 ++
.../apache/juneau/bean/hal/HalLinkOrArraySwap.java | 74 +++
.../org/apache/juneau/bean/hal/HalResource.java | 278 ++++++++
.../apache/juneau/bean/hal/HalResourceArray.java | 58 ++
.../juneau/bean/hal/HalResourceOrArraySwap.java | 71 ++
.../org/apache/juneau/bean/hal/package-info.java | 68 ++
juneau-bean/juneau-bean-jsonapi/pom.xml | 102 +++
.../juneau/bean/jsonapi/JsonApiDocument.java | 259 ++++++++
.../apache/juneau/bean/jsonapi/JsonApiError.java | 211 ++++++
.../juneau/bean/jsonapi/JsonApiErrorSource.java | 100 +++
.../apache/juneau/bean/jsonapi/JsonApiLink.java | 198 ++++++
.../bean/jsonapi/JsonApiLinkOrStringSwap.java | 67 ++
.../juneau/bean/jsonapi/JsonApiRelationship.java | 146 +++++
.../juneau/bean/jsonapi/JsonApiResource.java | 231 +++++++
.../bean/jsonapi/JsonApiResourceIdentifier.java | 118 ++++
.../apache/juneau/bean/jsonapi/JsonApiVersion.java | 96 +++
.../apache/juneau/bean/jsonapi/package-info.java | 86 +++
juneau-bean/juneau-bean-jsonpatch/pom.xml | 102 +++
.../org/apache/juneau/bean/jsonpatch/AddOp.java | 66 ++
.../org/apache/juneau/bean/jsonpatch/CopyOp.java | 66 ++
.../apache/juneau/bean/jsonpatch/JsonPatch.java | 60 ++
.../juneau/bean/jsonpatch/JsonPatchOperation.java | 88 +++
.../org/apache/juneau/bean/jsonpatch/MoveOp.java | 66 ++
.../org/apache/juneau/bean/jsonpatch/RemoveOp.java | 44 ++
.../apache/juneau/bean/jsonpatch/ReplaceOp.java | 66 ++
.../org/apache/juneau/bean/jsonpatch/TestOp.java | 66 ++
.../apache/juneau/bean/jsonpatch/package-info.java | 67 ++
juneau-bean/juneau-bean-rfc7807/pom.xml | 102 +++
.../org/apache/juneau/bean/rfc7807/Problem.java | 274 ++++++++
.../apache/juneau/bean/rfc7807/package-info.java | 27 +
juneau-bean/pom.xml | 4 +
.../apache/juneau/commons/bean/BeanSession.java | 2 +-
.../juneau/commons/utils/ThrowableUtils.java | 7 +-
.../main/java/org/apache/juneau/config/Entry.java | 4 +-
.../main/java/org/apache/juneau/BeanMapLoader.java | 4 +-
.../java/org/apache/juneau/MarshallingContext.java | 6 +-
.../java/org/apache/juneau/MarshallingSession.java | 8 +-
.../main/java/org/apache/juneau/UriContext.java | 2 +-
.../org/apache/juneau/collections/JsonList.java | 18 +-
.../org/apache/juneau/collections/JsonMap.java | 24 +-
.../apache/juneau/collections/MarshalledList.java | 10 +-
.../apache/juneau/collections/MarshalledMap.java | 10 +-
.../apache/juneau/collections/package-info.java | 4 +-
.../java/org/apache/juneau/json5/Json5List.java | 10 +-
.../java/org/apache/juneau/json5/Json5Map.java | 10 +-
.../org/apache/juneau/jsonschema/SchemaUtils.java | 4 +-
.../java/org/apache/juneau/marshaller/Sse.java | 154 +++++
.../org/apache/juneau/msgpack/MsgPackParser.java | 9 +-
.../apache/juneau/msgpack/MsgPackSerializer.java | 12 +-
.../main/java/org/apache/juneau/sse/SseEvent.java | 193 ++++++
.../java/org/apache/juneau/sse/SseEventReader.java | 270 ++++++++
.../main/java/org/apache/juneau/sse/SseParser.java | 133 ++++
.../org/apache/juneau/sse/SseParserSession.java | 108 +++
.../java/org/apache/juneau/sse/SseSerializer.java | 172 +++++
.../apache/juneau/sse/SseSerializerSession.java | 158 +++++
.../java/org/apache/juneau/sse/package-info.java | 39 ++
.../apache/juneau/examples/rest/RootResources.java | 1 +
.../juneau/examples/rest/SseDemoResource.java | 92 +++
.../juneau/rest/client/classic/RestClient.java | 6 +-
.../juneau/rest/client/classic/RestRequest.java | 4 +-
.../juneau/rest/client/classic/RestResponse.java | 67 +-
.../juneau/http/response/BasicHttpException.java | 1 +
.../juneau/rest/mock/classic/MockConsole.java | 7 +-
.../rest/swagger/BasicSwaggerProviderSession.java | 4 +-
juneau-utest/pom.xml | 20 +
.../test/java/org/apache/juneau/BeanMap_Test.java | 104 +--
.../org/apache/juneau/DataConversion_Test.java | 4 +-
.../test/java/org/apache/juneau/JsonList_Test.java | 2 +-
.../test/java/org/apache/juneau/JsonMap_Test.java | 224 +++----
.../org/apache/juneau/MarshalledConfig_Test.java | 4 +-
.../java/org/apache/juneau/ObjectSwap_Test.java | 2 +-
.../juneau/a/rttests/JsonMaps_RoundTripTest.java | 6 +-
.../a/rttests/SimpleObjects_RoundTripTest.java | 2 +-
.../a/rttests/TrimStrings_RoundTripTest.java | 12 +-
.../apache/juneau/bean/hal/Hal_RoundTrip_Test.java | 350 ++++++++++
.../bean/jsonapi/JsonApi_RoundTrip_Test.java | 446 +++++++++++++
.../bean/jsonpatch/JsonPatch_RoundTrip_Test.java | 236 +++++++
.../jsonschema/JsonSchemaBeanGenerator_Test.java | 2 +-
.../bean/rfc7807/Problem_RoundTrip_Test.java | 276 ++++++++
.../apache/juneau/cbor/CborCompliance_Test.java | 2 +-
.../apache/juneau/cbor/CborOutputStream_Test.java | 4 +-
.../org/apache/juneau/cbor/CborRoundTrip_Test.java | 2 +-
.../apache/juneau/cbor/CborSerializer_Test.java | 6 +-
.../juneau/collections/MarshalledList_Test.java | 14 +-
.../juneau/collections/MarshalledMap_Test.java | 18 +-
.../httppart/OpenApiPartSerializer_Test.java | 68 +-
.../java/org/apache/juneau/json/Json_Test.java | 4 +-
.../org/apache/juneau/json5/Json5List_Test.java | 16 +-
.../org/apache/juneau/json5/Json5Map_Test.java | 18 +-
.../juneau/msgpack/MsgPackMediaType_Test.java | 90 +++
.../juneau/msgpack/MsgPackSerializerTest.java | 8 +-
.../apache/juneau/objecttools/ObjectRest_Test.java | 16 +-
.../RestClient_Logging_StreamClosed_Test.java | 244 +++++++
.../client/classic/RestClient_Marshalls_Test.java | 4 +-
.../test/java/org/apache/juneau/sse/Sse_Test.java | 728 +++++++++++++++++++++
.../ReaderObjectSwap_ComboSerializeTest.java | 4 +-
.../SwapsAnnotation_ComboSerializeTest.java | 2 +-
.../org/apache/juneau/uon/UonSerializer_Test.java | 58 +-
.../urlencoding/UrlEncodingSerializer_Test.java | 58 +-
.../org/apache/juneau/utils/FilteredMapTest.java | 2 +-
...c7807.md => FINISHED-45-juneau-bean-rfc7807.md} | 26 +-
...l-sse.md => FINISHED-46-juneau-marshall-sse.md} | 26 +-
...s.md => FINISHED-47-additional-bean-modules.md} | 34 +-
todo/TODO-39-sonarqube-command.md | 292 +++++++++
todo/TODO-4-duration-format-control.md | 305 +++++++++
.../TODO-48-empty-return-marshalled-collections.md | 271 ++++++++
todo/TODO-49-schemautils-null-returns.md | 38 ++
todo/TODO.md | 15 -
110 files changed, 8828 insertions(+), 445 deletions(-)
diff --git a/juneau-bean/juneau-bean-hal/pom.xml b/juneau-bean/juneau-bean-hal/pom.xml
new file mode 100644
index 0000000000..e1f1975a59
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean</artifactId>
+ <version>9.5.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>juneau-bean-hal</artifactId>
+ <name>Apache Juneau HAL Beans</name>
+ <description>Apache Juneau Serializable Beans - HAL (Hypertext Application Language)</description>
+ <packaging>bundle</packaging>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-marshall</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ <executions>
+ <execution>
+ <id>bundle-manifest</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-prepare-agent</id>
+ <goals>
+ <goal>prepare-agent</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>default-report</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>report</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLink.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLink.java
new file mode 100644
index 0000000000..94da4d890d
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLink.java
@@ -0,0 +1,236 @@
+/*
+ * 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.bean.hal;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a HAL Link Object as defined by
+ * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-5">draft-kelly-json-hal-08 § 5</a>.
+ *
+ * <p>
+ * A Link Object describes a hyperlink from the containing resource to another resource. The {@code href} property is
+ * the only required field; all other fields are optional.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-5">HAL Link Object</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a>
+ * </ul>
+ */
+@Marshalled
+@SuppressWarnings({
+ "java:S116" // Field names mirror HAL spec.
+})
+public class HalLink {
+
+ private String href;
+ private Boolean templated;
+ private String type;
+ private String deprecation;
+ private String name;
+ private String profile;
+ private String title;
+ private String hreflang;
+
+ /**
+ * Default constructor.
+ */
+ public HalLink() {}
+
+ /**
+ * Convenience constructor with the required {@code href} field.
+ *
+ * @param href The link URI or URI template.
+ */
+ public HalLink(String href) {
+ this.href = href;
+ }
+
+ /**
+ * Bean property getter: <property>href</property>.
+ *
+ * <p>
+ * The link's target URI or URI template (per RFC 6570). This is the only required HAL Link field.
+ *
+ * @return The value of the <property>href</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getHref() { return href; }
+
+ /**
+ * Bean property setter: <property>href</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setHref(String value) {
+ href = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>templated</property>.
+ *
+ * <p>
+ * Indicates whether {@link #getHref() href} is a URI template per RFC 6570.
+ *
+ * @return The value of the <property>templated</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Boolean getTemplated() { return templated; }
+
+ /**
+ * Bean property setter: <property>templated</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setTemplated(Boolean value) {
+ templated = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>type</property>.
+ *
+ * <p>
+ * A hint about the media type expected when dereferencing the target resource.
+ *
+ * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getType() { return type; }
+
+ /**
+ * Bean property setter: <property>type</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setType(String value) {
+ type = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>deprecation</property>.
+ *
+ * <p>
+ * A URL providing information about deprecation of the link.
+ *
+ * @return The value of the <property>deprecation</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getDeprecation() { return deprecation; }
+
+ /**
+ * Bean property setter: <property>deprecation</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setDeprecation(String value) {
+ deprecation = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>name</property>.
+ *
+ * <p>
+ * A secondary key for selecting a link from a set sharing the same relation.
+ *
+ * @return The value of the <property>name</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getName() { return name; }
+
+ /**
+ * Bean property setter: <property>name</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setName(String value) {
+ name = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>profile</property>.
+ *
+ * <p>
+ * A URI hint about the profile (additional semantics) of the target resource.
+ *
+ * @return The value of the <property>profile</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getProfile() { return profile; }
+
+ /**
+ * Bean property setter: <property>profile</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setProfile(String value) {
+ profile = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>title</property>.
+ *
+ * <p>
+ * A human-readable identifier for the link, suitable for display.
+ *
+ * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getTitle() { return title; }
+
+ /**
+ * Bean property setter: <property>title</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setTitle(String value) {
+ title = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>hreflang</property>.
+ *
+ * <p>
+ * The language of the target resource (RFC 5646 language tag).
+ *
+ * @return The value of the <property>hreflang</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getHreflang() { return hreflang; }
+
+ /**
+ * Bean property setter: <property>hreflang</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public HalLink setHreflang(String value) {
+ hreflang = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkArray.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkArray.java
new file mode 100644
index 0000000000..ccbc5aec60
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkArray.java
@@ -0,0 +1,58 @@
+/*
+ * 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.bean.hal;
+
+import java.util.*;
+
+/**
+ * Represents a list of {@link HalLink} objects.
+ *
+ * <p>
+ * Serializes as a top-level JSON array when used as a value inside a HAL {@code _links} map. Mirrors the
+ * top-level-array pattern used by {@code JsonSchemaArray}.
+ *
+ * @serial exclude
+ */
+public class HalLinkArray extends LinkedList<HalLink> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Default constructor.
+ */
+ public HalLinkArray() {}
+
+ /**
+ * Constructor with predefined links to add to this array.
+ *
+ * @param value The list of links in this array.
+ */
+ public HalLinkArray(HalLink...value) {
+ addAll(value);
+ }
+
+ /**
+ * Convenience method for adding one or more {@link HalLink} objects to this array.
+ *
+ * @param value The {@link HalLink} objects to add to this array.
+ * @return This object.
+ */
+ public HalLinkArray addAll(HalLink...value) {
+ Collections.addAll(this, value);
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkOrArraySwap.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkOrArraySwap.java
new file mode 100644
index 0000000000..4a4ef3c14e
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalLinkOrArraySwap.java
@@ -0,0 +1,74 @@
+/*
+ * 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.bean.hal;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Per-property {@link ObjectSwap} applied to HAL {@code _links} maps so that each map entry round-trips
+ * as either a single {@link HalLink} (JSON object) or a {@link HalLinkArray} (JSON array) per the spec.
+ *
+ * <p>
+ * The swap operates on the whole {@code Map<String,Object>} bean property and walks the entries:
+ * <ul class='spaced-list'>
+ * <li>On <b>serialize</b> ({@link #swap}): identity - values that are {@link HalLink} or
+ * {@link HalLinkArray} already serialize as the correct JSON shape.
+ * <li>On <b>parse</b> ({@link #unswap}): each value that came back as a parsed list is converted to
+ * {@link HalLinkArray}; each value that came back as a parsed map is converted to {@link HalLink}.
+ * <li>{@code null} input ⇒ {@code null} output.
+ * </ul>
+ *
+ * <p>
+ * Mirrors {@code BooleanOrSchemaSwap} in {@code juneau-bean-jsonschema} (the entry-disambiguation idea), with
+ * iteration over map entries instead of operating on a single value.
+ */
+public class HalLinkOrArraySwap extends ObjectSwap<Object,Object> {
+
+ @Override /* Overridden from ObjectSwap */
+ public Object swap(MarshallingSession session, Object o) throws SerializeException {
+ return o;
+ }
+
+ @Override /* Overridden from ObjectSwap */
+ public Object unswap(MarshallingSession session, Object o, ClassMeta<?> hint) throws ParseException {
+ if (o == null)
+ return null;
+ if (o instanceof Map<?,?> raw) {
+ var out = new LinkedHashMap<String,Object>();
+ var linkCm = session.getClassMeta(HalLink.class);
+ var arrCm = session.getClassMeta(HalLinkArray.class);
+ for (var e : raw.entrySet()) {
+ var key = String.valueOf(e.getKey());
+ var v = e.getValue();
+ if (v instanceof Collection<?>) {
+ out.put(key, session.convertToType(v, arrCm));
+ } else if (v instanceof Map<?,?>) {
+ out.put(key, session.convertToType(v, linkCm));
+ } else {
+ out.put(key, v);
+ }
+ }
+ return out;
+ }
+ return o;
+ }
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResource.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResource.java
new file mode 100644
index 0000000000..a20d753ab5
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResource.java
@@ -0,0 +1,278 @@
+/*
+ * 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.bean.hal;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.Utils.*;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Represents a HAL Resource Object as defined by
+ * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08#section-4">draft-kelly-json-hal-08 § 4</a>.
+ *
+ * <p>
+ * A HAL Resource carries three pieces of state:
+ * <ul class='spaced-list'>
+ * <li>The reserved {@code _links} member - a map of relation name to either a single {@link HalLink}
+ * or a {@link HalLinkArray}.
+ * <li>The reserved {@code _embedded} member - a map of relation name to either a single {@link HalResource}
+ * or a {@link HalResourceArray} (sub-resources). HAL embeddings are conceptually a <i>tree</i>; cyclic
+ * {@code _embedded} graphs are rejected by Juneau's {@code BeanTraverseContext} by default.
+ * <li>Arbitrary payload fields, set via the dynamic {@link #set(String,Object)} method (uses the
+ * {@code @BeanProp("*")} extra-properties triplet, the same pattern used by {@code OpenApiElement} and
+ * {@code SwaggerElement}).
+ * </ul>
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * HalResource <jv>order</jv> = <jk>new</jk> HalResource()
+ * .addLink(<js>"self"</js>, <jk>new</jk> HalLink().setHref(<js>"/orders/123"</js>))
+ * .addLinks(<js>"curies"</js>,
+ * <jk>new</jk> HalLink().setName(<js>"acme"</js>).setHref(<js>"https://acme.example/{rel}"</js>).setTemplated(<jk>true</jk>))
+ * .set(<js>"total"</js>, 99.50);
+ *
+ * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>order</jv>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a href="https://stateless.group/hal_specification.html">HAL Specification</a>
+ * <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a>
+ * </ul>
+ */
+@Marshalled
+@SuppressWarnings({
+ "java:S116" // Field names mirror HAL spec.
+})
+public class HalResource {
+
+ private static final String ARG_property = "property";
+ private static final String ARG_relation = "relation";
+
+ private Map<String,Object> _links;
+ private Map<String,Object> _embedded;
+ private Map<String,Object> properties;
+
+ /**
+ * Default constructor.
+ */
+ public HalResource() {}
+
+ /**
+ * Bean property getter: <property>_links</property>.
+ *
+ * <p>
+ * The reserved {@code _links} member. Each map value is either a single {@link HalLink} (rendered as a JSON
+ * object on the wire) or a {@link HalLinkArray} (rendered as a JSON array).
+ *
+ * @return The value of the <property>_links</property> property, or <jk>null</jk> if it is not set.
+ */
+ @BeanProp("_links")
+ @Swap(HalLinkOrArraySwap.class)
+ public Map<String,Object> getLinks() {
+ return _links;
+ }
+
+ /**
+ * Bean property setter: <property>_links</property>.
+ *
+ * @param value The new value for this property. Can be <jk>null</jk> to unset.
+ * @return This object.
+ */
+ @BeanProp("_links")
+ @Swap(HalLinkOrArraySwap.class)
+ public HalResource setLinks(Map<String,Object> value) {
+ _links = value;
+ return this;
+ }
+
+ /**
+ * Convenience method that adds a single link under the given relation.
+ *
+ * <p>
+ * If a value already exists for {@code relation}, it is replaced with the supplied single link. To accumulate
+ * multiple links under the same relation use {@link #addLinks(String, HalLink...)} instead.
+ *
+ * @param relation The link relation (e.g. {@code "self"}, {@code "next"}, {@code "curies"}).
+ * @param value The link to set.
+ * @return This object.
+ */
+ public HalResource addLink(String relation, HalLink value) {
+ assertArgNotNull(ARG_relation, relation);
+ if (_links == null)
+ _links = map();
+ _links.put(relation, value);
+ return this;
+ }
+
+ /**
+ * Convenience method that appends one or more links to a {@link HalLinkArray} stored under {@code relation}.
+ *
+ * <p>
+ * If the current value at {@code relation} is a single {@link HalLink}, it is promoted to a {@link HalLinkArray}
+ * containing the original link and the appended ones.
+ *
+ * @param relation The link relation.
+ * @param values The links to append.
+ * @return This object.
+ */
+ public HalResource addLinks(String relation, HalLink...values) {
+ assertArgNotNull(ARG_relation, relation);
+ if (_links == null)
+ _links = map();
+ var existing = _links.get(relation);
+ HalLinkArray arr;
+ if (existing instanceof HalLinkArray a) {
+ arr = a;
+ } else if (existing instanceof HalLink l) {
+ arr = new HalLinkArray(l);
+ _links.put(relation, arr);
+ } else {
+ arr = new HalLinkArray();
+ _links.put(relation, arr);
+ }
+ arr.addAll(values);
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>_embedded</property>.
+ *
+ * <p>
+ * The reserved {@code _embedded} member. Each map value is either a single {@link HalResource} or a
+ * {@link HalResourceArray}.
+ *
+ * @return The value of the <property>_embedded</property> property, or <jk>null</jk> if it is not set.
+ */
+ @BeanProp("_embedded")
+ @Swap(HalResourceOrArraySwap.class)
+ public Map<String,Object> getEmbedded() {
+ return _embedded;
+ }
+
+ /**
+ * Bean property setter: <property>_embedded</property>.
+ *
+ * <p>
+ * Note: HAL embeddings are conceptually a tree, not a graph. Juneau's {@code BeanTraverseContext} throws on
+ * cyclic references by default, which matches the spec.
+ *
+ * @param value The new value for this property. Can be <jk>null</jk> to unset.
+ * @return This object.
+ */
+ @BeanProp("_embedded")
+ @Swap(HalResourceOrArraySwap.class)
+ public HalResource setEmbedded(Map<String,Object> value) {
+ _embedded = value;
+ return this;
+ }
+
+ /**
+ * Convenience method that embeds a single sub-resource under the given relation.
+ *
+ * @param relation The relation name.
+ * @param value The sub-resource.
+ * @return This object.
+ */
+ public HalResource addEmbedded(String relation, HalResource value) {
+ assertArgNotNull(ARG_relation, relation);
+ if (_embedded == null)
+ _embedded = map();
+ _embedded.put(relation, value);
+ return this;
+ }
+
+ /**
+ * Convenience method that appends one or more sub-resources to a {@link HalResourceArray} stored under
+ * {@code relation}.
+ *
+ * @param relation The relation name.
+ * @param values The sub-resources to append.
+ * @return This object.
+ */
+ public HalResource addEmbedded(String relation, HalResource...values) {
+ assertArgNotNull(ARG_relation, relation);
+ if (_embedded == null)
+ _embedded = map();
+ var existing = _embedded.get(relation);
+ HalResourceArray arr;
+ if (existing instanceof HalResourceArray a) {
+ arr = a;
+ } else if (existing instanceof HalResource r) {
+ arr = new HalResourceArray(r);
+ _embedded.put(relation, arr);
+ } else {
+ arr = new HalResourceArray();
+ _embedded.put(relation, arr);
+ }
+ arr.addAll(values);
+ return this;
+ }
+
+ /**
+ * Generic property keyset.
+ *
+ * @return All the payload (non-{@code _links}, non-{@code _embedded}) property names on this resource. Never <jk>null</jk>.
+ */
+ @BeanProp("*")
+ public Set<String> extraKeys() {
+ return properties == null ? Collections.emptySet() : properties.keySet();
+ }
+
+ /**
+ * Generic property getter.
+ *
+ * @param property The property name. Must not be <jk>null</jk>.
+ * @return The property value, or <jk>null</jk> if the property is not set.
+ */
+ @BeanProp("*")
+ public Object get(String property) {
+ assertArgNotNull(ARG_property, property);
+ return opt(properties).map(x -> x.get(property)).orElse(null);
+ }
+
+ /**
+ * Generic property setter.
+ *
+ * <p>
+ * Use this to set arbitrary payload fields alongside {@code _links} and {@code _embedded}.
+ *
+ * @param property The property name. Must not be <jk>null</jk>.
+ * @param value The new value for the property.
+ * @return This object.
+ */
+ @BeanProp("*")
+ public HalResource set(String property, Object value) {
+ assertArgNotNull(ARG_property, property);
+ if (properties == null)
+ properties = map();
+ properties.put(property, value);
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceArray.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceArray.java
new file mode 100644
index 0000000000..f71dd06865
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceArray.java
@@ -0,0 +1,58 @@
+/*
+ * 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.bean.hal;
+
+import java.util.*;
+
+/**
+ * Represents a list of {@link HalResource} objects.
+ *
+ * <p>
+ * Serializes as a top-level JSON array when used as a value inside a HAL {@code _embedded} map. Mirrors the
+ * top-level-array pattern used by {@code JsonSchemaArray}.
+ *
+ * @serial exclude
+ */
+public class HalResourceArray extends LinkedList<HalResource> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Default constructor.
+ */
+ public HalResourceArray() {}
+
+ /**
+ * Constructor with predefined resources to add to this array.
+ *
+ * @param value The list of resources in this array.
+ */
+ public HalResourceArray(HalResource...value) {
+ addAll(value);
+ }
+
+ /**
+ * Convenience method for adding one or more {@link HalResource} objects to this array.
+ *
+ * @param value The {@link HalResource} objects to add to this array.
+ * @return This object.
+ */
+ public HalResourceArray addAll(HalResource...value) {
+ Collections.addAll(this, value);
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceOrArraySwap.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceOrArraySwap.java
new file mode 100644
index 0000000000..58be476399
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/HalResourceOrArraySwap.java
@@ -0,0 +1,71 @@
+/*
+ * 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.bean.hal;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Per-property {@link ObjectSwap} applied to HAL {@code _embedded} maps so that each map entry round-trips
+ * as either a single {@link HalResource} (JSON object) or a {@link HalResourceArray} (JSON array) per the
+ * spec.
+ *
+ * <p>
+ * The swap operates on the whole {@code Map<String,Object>} bean property and walks the entries:
+ * <ul class='spaced-list'>
+ * <li>On <b>serialize</b> ({@link #swap}): identity - values that are {@link HalResource} or
+ * {@link HalResourceArray} already serialize as the correct JSON shape.
+ * <li>On <b>parse</b> ({@link #unswap}): each value that came back as a parsed list is converted to
+ * {@link HalResourceArray}; each value that came back as a parsed map is converted to {@link HalResource}.
+ * <li>{@code null} input ⇒ {@code null} output.
+ * </ul>
+ */
+public class HalResourceOrArraySwap extends ObjectSwap<Object,Object> {
+
+ @Override /* Overridden from ObjectSwap */
+ public Object swap(MarshallingSession session, Object o) throws SerializeException {
+ return o;
+ }
+
+ @Override /* Overridden from ObjectSwap */
+ public Object unswap(MarshallingSession session, Object o, ClassMeta<?> hint) throws ParseException {
+ if (o == null)
+ return null;
+ if (o instanceof Map<?,?> raw) {
+ var out = new LinkedHashMap<String,Object>();
+ var resCm = session.getClassMeta(HalResource.class);
+ var arrCm = session.getClassMeta(HalResourceArray.class);
+ for (var e : raw.entrySet()) {
+ var key = String.valueOf(e.getKey());
+ var v = e.getValue();
+ if (v instanceof Collection<?>) {
+ out.put(key, session.convertToType(v, arrCm));
+ } else if (v instanceof Map<?,?>) {
+ out.put(key, session.convertToType(v, resCm));
+ } else {
+ out.put(key, v);
+ }
+ }
+ return out;
+ }
+ return o;
+ }
+}
diff --git a/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/package-info.java b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/package-info.java
new file mode 100644
index 0000000000..f83de48694
--- /dev/null
+++ b/juneau-bean/juneau-bean-hal/src/main/java/org/apache/juneau/bean/hal/package-info.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} types for the
+ * <a href="https://stateless.group/hal_specification.html">HAL (Hypertext Application Language)</a>
+ * hypermedia format, also covered by
+ * <a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a>.
+ *
+ * <h5 class='topic'>Overview</h5>
+ *
+ * <p>
+ * HAL is a JSON-based hypermedia format used by RESTful APIs to express links and embedded resources alongside the
+ * payload itself. This module provides typed Juneau beans modeling the HAL wire format — no marshaller is needed; the
+ * existing {@link org.apache.juneau.json.JsonSerializer} / {@link org.apache.juneau.json.JsonParser} already produce
+ * and consume {@code application/hal+json}. The matching {@code ContentType} constant is
+ * {@code org.apache.juneau.http.header.ContentType#APPLICATION_HAL_JSON} (in {@code juneau-rest-common}).
+ *
+ * <h5 class='topic'>Bean Classes</h5>
+ * <ul class='spaced-list'>
+ * <li>{@link org.apache.juneau.bean.hal.HalResource} - Top-level HAL resource. Carries
+ * {@code _links}, {@code _embedded}, and arbitrary payload fields.
+ * <li>{@link org.apache.juneau.bean.hal.HalLink} - HAL Link Object with the 8 spec fields.
+ * <li>{@link org.apache.juneau.bean.hal.HalLinkArray} - {@code LinkedList<HalLink>} used as
+ * the multi-link branch inside an {@code _links} map.
+ * <li>{@link org.apache.juneau.bean.hal.HalResourceArray} - {@code LinkedList<HalResource>}
+ * used as the multi-resource branch inside an {@code _embedded} map.
+ * <li>{@link org.apache.juneau.bean.hal.HalLinkOrArraySwap} - {@link org.apache.juneau.swap.ObjectSwap}
+ * that materializes each {@code _links} value as either a single {@link org.apache.juneau.bean.hal.HalLink}
+ * (JSON object) or a {@link org.apache.juneau.bean.hal.HalLinkArray} (JSON array) per spec.
+ * <li>{@link org.apache.juneau.bean.hal.HalResourceOrArraySwap} - The {@code _embedded} analogue.
+ * </ul>
+ *
+ * <h5 class='topic'>Example</h5>
+ *
+ * <p class='bjava'>
+ * <jc>// Build a HAL resource with a self link, a curies array, and embedded items.</jc>
+ * HalResource <jv>order</jv> = <jk>new</jk> HalResource()
+ * .addLink(<js>"self"</js>, <jk>new</jk> HalLink().setHref(<js>"/orders/123"</js>))
+ * .addLinks(<js>"curies"</js>,
+ * <jk>new</jk> HalLink().setName(<js>"acme"</js>).setHref(<js>"https://acme.example/{rel}"</js>).setTemplated(<jk>true</jk>))
+ * .set(<js>"total"</js>, 99.50);
+ *
+ * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>order</jv>);
+ * HalResource <jv>back</jv> = JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>json</jv>, HalResource.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a href="https://stateless.group/hal_specification.html">HAL Specification</a>
+ * <li class='link'><a href="https://datatracker.ietf.org/doc/html/draft-kelly-json-hal-08">draft-kelly-json-hal-08</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanHal">juneau-bean-hal</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.hal;
diff --git a/juneau-bean/juneau-bean-jsonapi/pom.xml b/juneau-bean/juneau-bean-jsonapi/pom.xml
new file mode 100644
index 0000000000..c3ce7e648e
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean</artifactId>
+ <version>9.5.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>juneau-bean-jsonapi</artifactId>
+ <name>Apache Juneau JSON:API Beans</name>
+ <description>Apache Juneau Serializable Beans - JSON:API v1.1</description>
+ <packaging>bundle</packaging>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-marshall</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ <executions>
+ <execution>
+ <id>bundle-manifest</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-prepare-agent</id>
+ <goals>
+ <goal>prepare-agent</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>default-report</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>report</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiDocument.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiDocument.java
new file mode 100644
index 0000000000..86f145311b
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiDocument.java
@@ -0,0 +1,259 @@
+/*
+ * 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.bean.jsonapi;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+import static org.apache.juneau.commons.utils.ThrowableUtils.illegalState;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents the top-level JSON:API document as defined by
+ * <a href="https://jsonapi.org/format/#document-top-level">JSON:API v1.1 § Document Structure</a>.
+ *
+ * <p>
+ * Carries the standard six top-level members: {@code data}, {@code errors}, {@code meta}, {@code jsonapi},
+ * {@code links}, {@code included}.
+ *
+ * <h5 class='topic'>Mutual exclusion</h5>
+ *
+ * <p>
+ * Per spec, a document MUST contain at least one of {@code data}/{@code errors}/{@code meta}, and {@code data}
+ * and {@code errors} MUST NOT coexist. The {@link #validate()} helper checks both rules and throws
+ * {@link IllegalStateException} on violation.
+ *
+ * <h5 class='topic'>The polymorphic {@code data} member</h5>
+ *
+ * <p>
+ * The top-level {@code data} can be {@code null}, a single {@link JsonApiResource}, or an array of them. We
+ * model {@code data} as a plain {@code Object} property; the writer chooses single-vs-array shape based on
+ * which value was set. Typed setters {@link #setData(JsonApiResource)} and {@link #setData(List)} are provided
+ * for convenience.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiDocument {
+
+ private Object data;
+ private List<JsonApiError> errors;
+ private Map<String,Object> meta;
+ private JsonApiVersion jsonapi;
+ private Map<String,Object> links;
+ private List<JsonApiResource> included;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiDocument() {}
+
+ /**
+ * Bean property getter: <property>data</property>.
+ *
+ * <p>
+ * Top-level resource linkage. May be {@code null}, a single {@link JsonApiResource}, or a
+ * {@code List<JsonApiResource>}.
+ *
+ * @return The value of the <property>data</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Object getData() { return data; }
+
+ /**
+ * Bean property setter: <property>data</property>.
+ *
+ * @param value The new value (single resource, list of resources, or {@code null} to unset).
+ * @return This object.
+ */
+ public JsonApiDocument setData(Object value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Typed setter for single-resource primary data.
+ *
+ * @param value The resource.
+ * @return This object.
+ */
+ public JsonApiDocument setData(JsonApiResource value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Typed setter for to-many primary data.
+ *
+ * @param value The list of resources.
+ * @return This object.
+ */
+ public JsonApiDocument setData(List<JsonApiResource> value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>errors</property>.
+ *
+ * @return The value of the <property>errors</property> property, or <jk>null</jk> if it is not set.
+ */
+ public List<JsonApiError> getErrors() { return errors; }
+
+ /**
+ * Bean property setter: <property>errors</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiDocument setErrors(List<JsonApiError> value) {
+ errors = value;
+ return this;
+ }
+
+ /**
+ * Bean property appender: <property>errors</property>.
+ *
+ * @param value The error objects to append.
+ * @return This object.
+ */
+ public JsonApiDocument addErrors(JsonApiError...value) {
+ if (errors == null)
+ errors = list();
+ Collections.addAll(errors, value);
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiDocument setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>jsonapi</property>.
+ *
+ * @return The value of the <property>jsonapi</property> property, or <jk>null</jk> if it is not set.
+ */
+ public JsonApiVersion getJsonapi() { return jsonapi; }
+
+ /**
+ * Bean property setter: <property>jsonapi</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiDocument setJsonapi(JsonApiVersion value) {
+ jsonapi = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>links</property>.
+ *
+ * <p>
+ * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+ *
+ * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public Map<String,Object> getLinks() { return links; }
+
+ /**
+ * Bean property setter: <property>links</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public JsonApiDocument setLinks(Map<String,Object> value) {
+ links = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>included</property>.
+ *
+ * @return The value of the <property>included</property> property, or <jk>null</jk> if it is not set.
+ */
+ public List<JsonApiResource> getIncluded() { return included; }
+
+ /**
+ * Bean property setter: <property>included</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiDocument setIncluded(List<JsonApiResource> value) {
+ included = value;
+ return this;
+ }
+
+ /**
+ * Bean property appender: <property>included</property>.
+ *
+ * @param value The resource objects to append.
+ * @return This object.
+ */
+ public JsonApiDocument addIncluded(JsonApiResource...value) {
+ if (included == null)
+ included = list();
+ Collections.addAll(included, value);
+ return this;
+ }
+
+ /**
+ * Validates document-level invariants per the JSON:API spec.
+ *
+ * <p>
+ * Throws if {@code data} and {@code errors} are both present, or if none of {@code data}/{@code errors}/{@code meta}
+ * is present.
+ *
+ * @return This object.
+ * @throws IllegalStateException If the document violates a top-level invariant.
+ */
+ public JsonApiDocument validate() {
+ if (data != null && errors != null)
+ throw illegalState("JSON:API document must not contain both 'data' and 'errors'.");
+ if (data == null && errors == null && meta == null)
+ throw illegalState("JSON:API document must contain at least one of 'data', 'errors', or 'meta'.");
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiError.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiError.java
new file mode 100644
index 0000000000..680454bd43
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiError.java
@@ -0,0 +1,211 @@
+/*
+ * 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.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Error Object as defined by
+ * <a href="https://jsonapi.org/format/#error-objects">JSON:API v1.1 § Error Objects</a>.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiError {
+
+ private String id;
+ private Map<String,Object> links;
+ private String status;
+ private String code;
+ private String title;
+ private String detail;
+ private JsonApiErrorSource source;
+ private Map<String,Object> meta;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiError() {}
+
+ /**
+ * Bean property getter: <property>id</property>.
+ *
+ * @return The value of the <property>id</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getId() { return id; }
+
+ /**
+ * Bean property setter: <property>id</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setId(String value) {
+ id = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>links</property>.
+ *
+ * <p>
+ * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+ *
+ * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public Map<String,Object> getLinks() { return links; }
+
+ /**
+ * Bean property setter: <property>links</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public JsonApiError setLinks(Map<String,Object> value) {
+ links = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>status</property>.
+ *
+ * <p>
+ * Per spec, the HTTP status code as a string.
+ *
+ * @return The value of the <property>status</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getStatus() { return status; }
+
+ /**
+ * Bean property setter: <property>status</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setStatus(String value) {
+ status = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>code</property>.
+ *
+ * <p>
+ * An application-specific error code.
+ *
+ * @return The value of the <property>code</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getCode() { return code; }
+
+ /**
+ * Bean property setter: <property>code</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setCode(String value) {
+ code = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>title</property>.
+ *
+ * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getTitle() { return title; }
+
+ /**
+ * Bean property setter: <property>title</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setTitle(String value) {
+ title = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>detail</property>.
+ *
+ * @return The value of the <property>detail</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getDetail() { return detail; }
+
+ /**
+ * Bean property setter: <property>detail</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setDetail(String value) {
+ detail = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>source</property>.
+ *
+ * @return The value of the <property>source</property> property, or <jk>null</jk> if it is not set.
+ */
+ public JsonApiErrorSource getSource() { return source; }
+
+ /**
+ * Bean property setter: <property>source</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setSource(JsonApiErrorSource value) {
+ source = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiError setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiErrorSource.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiErrorSource.java
new file mode 100644
index 0000000000..1a14cc59d7
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiErrorSource.java
@@ -0,0 +1,100 @@
+/*
+ * 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.bean.jsonapi;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents the {@code source} sub-object of a JSON:API Error Object.
+ *
+ * <p>
+ * Per <a href="https://jsonapi.org/format/#error-objects">JSON:API v1.1 § Error Objects</a>, the
+ * {@code source} object can carry a {@code pointer} (JSON Pointer to the offending element), a
+ * {@code parameter} (the URI query parameter name), and/or a {@code header} (the request header name).
+ */
+@Marshalled
+public class JsonApiErrorSource {
+
+ private String pointer;
+ private String parameter;
+ private String header;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiErrorSource() {}
+
+ /**
+ * Bean property getter: <property>pointer</property>.
+ *
+ * @return The value of the <property>pointer</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getPointer() { return pointer; }
+
+ /**
+ * Bean property setter: <property>pointer</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiErrorSource setPointer(String value) {
+ pointer = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>parameter</property>.
+ *
+ * @return The value of the <property>parameter</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getParameter() { return parameter; }
+
+ /**
+ * Bean property setter: <property>parameter</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiErrorSource setParameter(String value) {
+ parameter = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>header</property>.
+ *
+ * @return The value of the <property>header</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getHeader() { return header; }
+
+ /**
+ * Bean property setter: <property>header</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiErrorSource setHeader(String value) {
+ header = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLink.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLink.java
new file mode 100644
index 0000000000..66810ab176
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLink.java
@@ -0,0 +1,198 @@
+/*
+ * 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.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Link Object as defined by
+ * <a href="https://jsonapi.org/format/#document-links">JSON:API v1.1 § Links</a>.
+ *
+ * <p>
+ * Per the spec, each {@code links} entry may be either a JSON string URL or a JSON object with these members:
+ * {@code href} (required URI), {@code rel}, {@code describedby}, {@code title}, {@code type} (media-type hint),
+ * {@code hreflang} (language tag), and {@code meta}. The single-vs-object union is handled by
+ * {@link JsonApiLinkOrStringSwap}, applied via field-level {@code @BeanProp("links") @Swap(...)} on every type
+ * carrying a {@code links} map.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a href="https://jsonapi.org/format/#document-links">JSON:API v1.1 > Links</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiLink {
+
+ private String href;
+ private String rel;
+ private String describedby;
+ private String title;
+ private String type;
+ private String hreflang;
+ private Map<String,Object> meta;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiLink() {}
+
+ /**
+ * Convenience constructor with the required {@code href} field.
+ *
+ * @param href The link target URI.
+ */
+ public JsonApiLink(String href) {
+ this.href = href;
+ }
+
+ /**
+ * Bean property getter: <property>href</property>.
+ *
+ * @return The value of the <property>href</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getHref() { return href; }
+
+ /**
+ * Bean property setter: <property>href</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setHref(String value) {
+ href = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>rel</property>.
+ *
+ * @return The value of the <property>rel</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getRel() { return rel; }
+
+ /**
+ * Bean property setter: <property>rel</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setRel(String value) {
+ rel = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>describedby</property>.
+ *
+ * @return The value of the <property>describedby</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getDescribedby() { return describedby; }
+
+ /**
+ * Bean property setter: <property>describedby</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setDescribedby(String value) {
+ describedby = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>title</property>.
+ *
+ * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getTitle() { return title; }
+
+ /**
+ * Bean property setter: <property>title</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setTitle(String value) {
+ title = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>type</property>.
+ *
+ * <p>
+ * Media-type hint for the target resource.
+ *
+ * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getType() { return type; }
+
+ /**
+ * Bean property setter: <property>type</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setType(String value) {
+ type = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>hreflang</property>.
+ *
+ * @return The value of the <property>hreflang</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getHreflang() { return hreflang; }
+
+ /**
+ * Bean property setter: <property>hreflang</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setHreflang(String value) {
+ hreflang = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiLink setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLinkOrStringSwap.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLinkOrStringSwap.java
new file mode 100644
index 0000000000..b9befc3cf0
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiLinkOrStringSwap.java
@@ -0,0 +1,67 @@
+/*
+ * 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.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.swap.*;
+
+/**
+ * Per-property {@link ObjectSwap} applied to JSON:API {@code links} maps so that each map entry round-trips
+ * as either a JSON string URL or a {@link JsonApiLink} JSON object per the spec.
+ *
+ * <p>
+ * The swap operates on the whole {@code Map<String,Object>} bean property and walks the entries:
+ * <ul class='spaced-list'>
+ * <li>On <b>serialize</b> ({@link #swap}): identity - {@code String} values and {@link JsonApiLink} values
+ * serialize directly as JSON strings and JSON objects respectively.
+ * <li>On <b>parse</b> ({@link #unswap}): each value that came back as a parsed map (i.e. originally a JSON
+ * object) is converted to a {@link JsonApiLink}; all other values pass through unchanged.
+ * <li>{@code null} input ⇒ {@code null} output.
+ * </ul>
+ */
+public class JsonApiLinkOrStringSwap extends ObjectSwap<Object,Object> {
+
+ @Override /* Overridden from ObjectSwap */
+ public Object swap(MarshallingSession session, Object o) throws SerializeException {
+ return o;
+ }
+
+ @Override /* Overridden from ObjectSwap */
+ public Object unswap(MarshallingSession session, Object o, ClassMeta<?> hint) throws ParseException {
+ if (o == null)
+ return null;
+ if (o instanceof Map<?,?> raw) {
+ var out = new LinkedHashMap<String,Object>();
+ var linkCm = session.getClassMeta(JsonApiLink.class);
+ for (var e : raw.entrySet()) {
+ var key = String.valueOf(e.getKey());
+ var v = e.getValue();
+ if (v instanceof Map<?,?>) {
+ out.put(key, session.convertToType(v, linkCm));
+ } else {
+ out.put(key, v);
+ }
+ }
+ return out;
+ }
+ return o;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiRelationship.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiRelationship.java
new file mode 100644
index 0000000000..0b6777165e
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiRelationship.java
@@ -0,0 +1,146 @@
+/*
+ * 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.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Relationship Object as defined by
+ * <a href="https://jsonapi.org/format/#document-resource-object-relationships">JSON:API v1.1 § Relationships</a>.
+ *
+ * <p>
+ * A relationship carries:
+ * <ul class='spaced-list'>
+ * <li>{@code data} - resource linkage. Per spec, this can be {@code null}, a single
+ * {@link JsonApiResourceIdentifier}, or a JSON array of identifiers. Modeled here as {@code Object}.
+ * <li>{@code links} - related URLs. Each value can be either a JSON string URL or a {@link JsonApiLink} object;
+ * the field-level {@link JsonApiLinkOrStringSwap} swap handles the union.
+ * <li>{@code meta} - free-form meta bag.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiRelationship {
+
+ private Object data;
+ private Map<String,Object> links;
+ private Map<String,Object> meta;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiRelationship() {}
+
+ /**
+ * Bean property getter: <property>data</property>.
+ *
+ * <p>
+ * Resource linkage. Per spec, this may be {@code null}, a single {@link JsonApiResourceIdentifier}, or a
+ * {@code List<JsonApiResourceIdentifier>}.
+ *
+ * @return The value of the <property>data</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Object getData() { return data; }
+
+ /**
+ * Bean property setter: <property>data</property>.
+ *
+ * @param value The new value (single identifier, list of identifiers, or {@code null} to unset).
+ * @return This object.
+ */
+ public JsonApiRelationship setData(Object value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Typed setter for single-identifier linkage.
+ *
+ * @param value The identifier.
+ * @return This object.
+ */
+ public JsonApiRelationship setData(JsonApiResourceIdentifier value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Typed setter for to-many linkage.
+ *
+ * @param value The list of identifiers.
+ * @return This object.
+ */
+ public JsonApiRelationship setData(List<JsonApiResourceIdentifier> value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>links</property>.
+ *
+ * <p>
+ * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+ *
+ * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public Map<String,Object> getLinks() { return links; }
+
+ /**
+ * Bean property setter: <property>links</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public JsonApiRelationship setLinks(Map<String,Object> value) {
+ links = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiRelationship setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResource.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResource.java
new file mode 100644
index 0000000000..119f07eb76
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResource.java
@@ -0,0 +1,231 @@
+/*
+ * 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.bean.jsonapi;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Resource Object as defined by
+ * <a href="https://jsonapi.org/format/#document-resource-objects">JSON:API v1.1 § Resource Objects</a>.
+ *
+ * <p>
+ * Carries the standard six members: {@code type}, {@code id}, {@code attributes}, {@code relationships},
+ * {@code links}, {@code meta}.
+ *
+ * <h5 class='topic'>Why {@code type} is a plain String</h5>
+ *
+ * <p>
+ * JSON:API uses {@code type} as an open-ended string field on resource objects (the entity-type name, e.g.
+ * {@code "articles"}); the value is chosen by the API and is <b>not</b> a closed Java class hierarchy.
+ * Juneau's {@code @Marshalled(typePropertyName=...)} discriminator defaults to {@code _type} (so there is also
+ * no literal name clash). <b>Do NOT</b> annotate {@code JsonApiResource} with
+ * {@code @Marshalled(typePropertyName="type", dictionary=...)} — that would conflate JSON:API resource typing
+ * with Juneau bean dispatch, break round-tripping of unknown {@code type} values, and force every API to
+ * enumerate its resource types in a Java dictionary up front.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiResource {
+
+ private String type;
+ private String id;
+ private Map<String,Object> attributes;
+ private Map<String,JsonApiRelationship> relationships;
+ private Map<String,Object> links;
+ private Map<String,Object> meta;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiResource() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param type The entity-type name.
+ * @param id The identifier value.
+ */
+ public JsonApiResource(String type, String id) {
+ this.type = type;
+ this.id = id;
+ }
+
+ /**
+ * Bean property getter: <property>type</property>.
+ *
+ * <p>
+ * Plain {@code String} entity-type name. See class-level Javadoc for why this is not a Juneau polymorphic
+ * discriminator.
+ *
+ * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getType() { return type; }
+
+ /**
+ * Bean property setter: <property>type</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResource setType(String value) {
+ type = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>id</property>.
+ *
+ * @return The value of the <property>id</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getId() { return id; }
+
+ /**
+ * Bean property setter: <property>id</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResource setId(String value) {
+ id = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>attributes</property>.
+ *
+ * <p>
+ * The spec lets {@code attributes} be any JSON object. Modeled as {@code Map<String,Object>} (a nested
+ * object on the wire — not flattened).
+ *
+ * @return The value of the <property>attributes</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getAttributes() { return attributes; }
+
+ /**
+ * Bean property setter: <property>attributes</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResource setAttributes(Map<String,Object> value) {
+ attributes = value;
+ return this;
+ }
+
+ /**
+ * Convenience method to add a single attribute.
+ *
+ * @param name The attribute name.
+ * @param value The attribute value.
+ * @return This object.
+ */
+ public JsonApiResource putAttribute(String name, Object value) {
+ if (attributes == null)
+ attributes = map();
+ attributes.put(name, value);
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>relationships</property>.
+ *
+ * @return The value of the <property>relationships</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,JsonApiRelationship> getRelationships() { return relationships; }
+
+ /**
+ * Bean property setter: <property>relationships</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResource setRelationships(Map<String,JsonApiRelationship> value) {
+ relationships = value;
+ return this;
+ }
+
+ /**
+ * Convenience method to add a single named relationship.
+ *
+ * @param name The relationship name.
+ * @param value The relationship object.
+ * @return This object.
+ */
+ public JsonApiResource putRelationship(String name, JsonApiRelationship value) {
+ if (relationships == null)
+ relationships = map();
+ relationships.put(name, value);
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>links</property>.
+ *
+ * <p>
+ * Per spec, each {@code links} value can be either a JSON string URL or a {@link JsonApiLink} object.
+ *
+ * @return The value of the <property>links</property> property, or <jk>null</jk> if it is not set.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public Map<String,Object> getLinks() { return links; }
+
+ /**
+ * Bean property setter: <property>links</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ @BeanProp(value="links")
+ @Swap(JsonApiLinkOrStringSwap.class)
+ public JsonApiResource setLinks(Map<String,Object> value) {
+ links = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResource setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResourceIdentifier.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResourceIdentifier.java
new file mode 100644
index 0000000000..084c8f7068
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiResourceIdentifier.java
@@ -0,0 +1,118 @@
+/*
+ * 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.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents a JSON:API Resource Identifier Object as defined by
+ * <a href="https://jsonapi.org/format/#document-resource-identifier-objects">JSON:API v1.1 § Resource Identifier Objects</a>.
+ *
+ * <p>
+ * A slim form of {@link JsonApiResource} used inside relationship {@code data} linkage. Carries only {@code type},
+ * {@code id}, and an optional {@code meta} bag. {@code type} is a plain {@code String} for the same reasons
+ * documented on {@link JsonApiResource#getType()}.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiResourceIdentifier {
+
+ private String type;
+ private String id;
+ private Map<String,Object> meta;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiResourceIdentifier() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param type The entity-type name.
+ * @param id The identifier value.
+ */
+ public JsonApiResourceIdentifier(String type, String id) {
+ this.type = type;
+ this.id = id;
+ }
+
+ /**
+ * Bean property getter: <property>type</property>.
+ *
+ * @return The value of the <property>type</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getType() { return type; }
+
+ /**
+ * Bean property setter: <property>type</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResourceIdentifier setType(String value) {
+ type = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>id</property>.
+ *
+ * @return The value of the <property>id</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getId() { return id; }
+
+ /**
+ * Bean property setter: <property>id</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResourceIdentifier setId(String value) {
+ id = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiResourceIdentifier setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiVersion.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiVersion.java
new file mode 100644
index 0000000000..370a21bd48
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/JsonApiVersion.java
@@ -0,0 +1,96 @@
+/*
+ * 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.bean.jsonapi;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Represents the JSON:API top-level {@code jsonapi} member.
+ *
+ * <p>
+ * Per <a href="https://jsonapi.org/format/#document-jsonapi-object">JSON:API v1.1 § JSON:API Object</a>,
+ * this object carries the optional protocol {@code version} string (e.g. {@code "1.1"}) and an optional
+ * {@code meta} bag.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+@Marshalled
+public class JsonApiVersion {
+
+ private String version;
+ private Map<String,Object> meta;
+
+ /**
+ * Default constructor.
+ */
+ public JsonApiVersion() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param version The JSON:API protocol version (e.g. {@code "1.1"}).
+ */
+ public JsonApiVersion(String version) {
+ this.version = version;
+ }
+
+ /**
+ * Bean property getter: <property>version</property>.
+ *
+ * @return The value of the <property>version</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getVersion() { return version; }
+
+ /**
+ * Bean property setter: <property>version</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiVersion setVersion(String value) {
+ version = value;
+ return this;
+ }
+
+ /**
+ * Bean property getter: <property>meta</property>.
+ *
+ * @return The value of the <property>meta</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Map<String,Object> getMeta() { return meta; }
+
+ /**
+ * Bean property setter: <property>meta</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonApiVersion setMeta(Map<String,Object> value) {
+ meta = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/package-info.java b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/package-info.java
new file mode 100644
index 0000000000..57d5adfb51
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonapi/src/main/java/org/apache/juneau/bean/jsonapi/package-info.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} types for the
+ * <a href="https://jsonapi.org/format/">JSON:API v1.1</a> wire format.
+ *
+ * <h5 class='topic'>Overview</h5>
+ *
+ * <p>
+ * JSON:API is a media-type ({@code application/vnd.api+json}) for building APIs with shared conventions for
+ * resources, relationships, links, errors, and metadata. This module provides typed Juneau beans modeling the
+ * JSON:API wire format - no marshaller is needed; the existing {@link org.apache.juneau.json.JsonSerializer} /
+ * {@link org.apache.juneau.json.JsonParser} already produce and consume the wire format. The matching
+ * {@code ContentType} constant is {@code org.apache.juneau.http.header.ContentType#APPLICATION_VND_API_JSON}.
+ *
+ * <h5 class='topic'>Bean Classes</h5>
+ * <ul class='spaced-list'>
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiDocument} - Top-level container with
+ * {@code data}/{@code errors}/{@code meta}/{@code jsonapi}/{@code links}/{@code included}.
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiResource} - Resource object with
+ * {@code type}/{@code id}/{@code attributes}/{@code relationships}/{@code links}/{@code meta}.
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiResourceIdentifier} - Slim form used inside relationship linkage.
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiRelationship} - Relationship object
+ * ({@code data} linkage, {@code links}, {@code meta}).
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiError} - Error object with
+ * {@code id}/{@code links}/{@code status}/{@code code}/{@code title}/{@code detail}/{@code source}/{@code meta}.
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiErrorSource} - The {@code source} sub-object
+ * ({@code pointer}/{@code parameter}/{@code header}).
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiLink} - Link object with
+ * {@code href}/{@code rel}/{@code describedby}/{@code title}/{@code type}/{@code hreflang}/{@code meta}.
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiVersion} - The {@code jsonapi} member
+ * ({@code version}/{@code meta}).
+ * <li>{@link org.apache.juneau.bean.jsonapi.JsonApiLinkOrStringSwap} -
+ * {@link org.apache.juneau.swap.ObjectSwap} for {@code links} map values; each value can be either a JSON
+ * string URL or a {@link org.apache.juneau.bean.jsonapi.JsonApiLink} object per the spec.
+ * </ul>
+ *
+ * <h5 class='topic'>Important: {@code JsonApiResource.type} is a plain String</h5>
+ *
+ * <p>
+ * JSON:API uses {@code type} as an open-ended string field on resource objects (the entity-type name, e.g.
+ * {@code "articles"}); the value is chosen by the API and is <b>not</b> a closed Java class hierarchy. Therefore
+ * {@code JsonApiResource} models {@code type} as a plain {@code String} and is <b>not</b> annotated with
+ * {@code @Marshalled(typePropertyName="type", dictionary=...)}. Conflating JSON:API resource typing with Juneau
+ * bean dispatch would break round-tripping of unknown {@code type} values and force every API to enumerate its
+ * resource types in a Java dictionary up front.
+ *
+ * <h5 class='topic'>Document-level mutual exclusion</h5>
+ *
+ * <p>
+ * Per the JSON:API spec, a document MUST contain at least one of {@code data}, {@code errors}, or {@code meta};
+ * {@code data} and {@code errors} MUST NOT coexist. {@link org.apache.juneau.bean.jsonapi.JsonApiDocument}
+ * documents this constraint and offers an optional {@code validate()} helper.
+ *
+ * <h5 class='topic'>Example</h5>
+ *
+ * <p class='bjava'>
+ * <jc>// Build a compound document with included resources.</jc>
+ * JsonApiResource <jv>article</jv> = <jk>new</jk> JsonApiResource()
+ * .setType(<js>"articles"</js>).setId(<js>"1"</js>)
+ * .putAttribute(<js>"title"</js>, <js>"JSON:API paints my bikeshed!"</js>);
+ * JsonApiDocument <jv>doc</jv> = <jk>new</jk> JsonApiDocument().setData(<jv>article</jv>);
+ * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>doc</jv>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a href="https://jsonapi.org/format/">JSON:API v1.1</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonApi">juneau-bean-jsonapi</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.jsonapi;
diff --git a/juneau-bean/juneau-bean-jsonpatch/pom.xml b/juneau-bean/juneau-bean-jsonpatch/pom.xml
new file mode 100644
index 0000000000..c8cde6fa67
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean</artifactId>
+ <version>9.5.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>juneau-bean-jsonpatch</artifactId>
+ <name>Apache Juneau JSON Patch Beans</name>
+ <description>Apache Juneau Serializable Beans - JSON Patch (RFC 6902)</description>
+ <packaging>bundle</packaging>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-marshall</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ <executions>
+ <execution>
+ <id>bundle-manifest</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-prepare-agent</id>
+ <goals>
+ <goal>prepare-agent</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>default-report</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>report</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/AddOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/AddOp.java
new file mode 100644
index 0000000000..4fe79d6ee1
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/AddOp.java
@@ -0,0 +1,66 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "add"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.1">RFC 6902 § 4.1</a>.
+ *
+ * <p>
+ * Adds {@link #getValue() value} at {@link #getPath() path}.
+ */
+@Marshalled(typeName = "add")
+public class AddOp extends JsonPatchOperation {
+
+ private Object value;
+
+ /**
+ * Default constructor.
+ */
+ public AddOp() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer target.
+ * @param value The value to add.
+ */
+ public AddOp(String path, Object value) {
+ super(path);
+ this.value = value;
+ }
+
+ /**
+ * Bean property getter: <property>value</property>.
+ *
+ * @return The value of the <property>value</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Object getValue() { return value; }
+
+ /**
+ * Bean property setter: <property>value</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public AddOp setValue(Object value) {
+ this.value = value;
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/CopyOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/CopyOp.java
new file mode 100644
index 0000000000..8480b8057b
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/CopyOp.java
@@ -0,0 +1,66 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "copy"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.5">RFC 6902 § 4.5</a>.
+ *
+ * <p>
+ * Copies the value at {@link #getFrom() from} to {@link #getPath() path}.
+ */
+@Marshalled(typeName = "copy")
+public class CopyOp extends JsonPatchOperation {
+
+ private String from;
+
+ /**
+ * Default constructor.
+ */
+ public CopyOp() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer destination.
+ * @param from The JSON Pointer source.
+ */
+ public CopyOp(String path, String from) {
+ super(path);
+ this.from = from;
+ }
+
+ /**
+ * Bean property getter: <property>from</property>.
+ *
+ * @return The value of the <property>from</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getFrom() { return from; }
+
+ /**
+ * Bean property setter: <property>from</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public CopyOp setFrom(String value) {
+ from = value;
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatch.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatch.java
new file mode 100644
index 0000000000..823a625859
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatch.java
@@ -0,0 +1,60 @@
+/*
+ * 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.bean.jsonpatch;
+
+import java.util.*;
+
+/**
+ * Represents a JSON Patch document per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>.
+ *
+ * <p>
+ * A JSON Patch document is a JSON array of operation objects; this type therefore extends
+ * {@link LinkedList} of {@link JsonPatchOperation}. Mirrors the top-level-array pattern used by
+ * {@code JsonSchemaArray} - no {@code @Json(wrapperAttr=...)} wrapping.
+ *
+ * @serial exclude
+ */
+public class JsonPatch extends LinkedList<JsonPatchOperation> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Default constructor.
+ */
+ public JsonPatch() {}
+
+ /**
+ * Constructor with predefined operations to add to this patch.
+ *
+ * @param value The operations in this patch.
+ */
+ public JsonPatch(JsonPatchOperation...value) {
+ append(value);
+ }
+
+ /**
+ * Convenience method for appending one or more {@link JsonPatchOperation} objects to this patch.
+ *
+ * @param value The operations to append.
+ * @return This object.
+ */
+ public JsonPatch append(JsonPatchOperation...value) {
+ Collections.addAll(this, value);
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatchOperation.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatchOperation.java
new file mode 100644
index 0000000000..e58d02d0f5
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/JsonPatchOperation.java
@@ -0,0 +1,88 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.json.*;
+
+/**
+ * Abstract base for a JSON Patch operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902</a>.
+ *
+ * <p>
+ * Polymorphic dispatch is wired here via
+ * {@code @Marshalled(typePropertyName="op", dictionary={...})}: on read, the parser inspects the {@code op}
+ * member and instantiates the matching concrete subclass; on write, the serializer emits {@code op} as the
+ * discriminator and the operation's own members alongside.
+ *
+ * <p>
+ * <b>Not</b> declared {@code sealed} - keeping it open lets downstream callers add their own ops if needed
+ * (e.g. JSON Patch extensions or vendor-specific test operators) by adding the new subclass to a custom
+ * dictionary.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonPatch">juneau-bean-jsonpatch</a>
+ * </ul>
+ */
+@Marshalled(
+ typePropertyName = "op",
+ dictionary = { AddOp.class, RemoveOp.class, ReplaceOp.class, MoveOp.class, CopyOp.class, TestOp.class }
+)
+public abstract class JsonPatchOperation {
+
+ private String path;
+
+ /**
+ * Default constructor.
+ */
+ protected JsonPatchOperation() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer target of the operation.
+ */
+ protected JsonPatchOperation(String path) {
+ this.path = path;
+ }
+
+ /**
+ * Bean property getter: <property>path</property>.
+ *
+ * <p>
+ * The target JSON Pointer (RFC 6901) of this operation.
+ *
+ * @return The value of the <property>path</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getPath() { return path; }
+
+ /**
+ * Bean property setter: <property>path</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public JsonPatchOperation setPath(String value) {
+ path = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return JsonSerializer.DEFAULT.toString(this);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/MoveOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/MoveOp.java
new file mode 100644
index 0000000000..90f40fd33a
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/MoveOp.java
@@ -0,0 +1,66 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "move"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.4">RFC 6902 § 4.4</a>.
+ *
+ * <p>
+ * Moves the value at {@link #getFrom() from} to {@link #getPath() path}.
+ */
+@Marshalled(typeName = "move")
+public class MoveOp extends JsonPatchOperation {
+
+ private String from;
+
+ /**
+ * Default constructor.
+ */
+ public MoveOp() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer destination.
+ * @param from The JSON Pointer source.
+ */
+ public MoveOp(String path, String from) {
+ super(path);
+ this.from = from;
+ }
+
+ /**
+ * Bean property getter: <property>from</property>.
+ *
+ * @return The value of the <property>from</property> property, or <jk>null</jk> if it is not set.
+ */
+ public String getFrom() { return from; }
+
+ /**
+ * Bean property setter: <property>from</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public MoveOp setFrom(String value) {
+ from = value;
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/RemoveOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/RemoveOp.java
new file mode 100644
index 0000000000..d01bc2bf82
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/RemoveOp.java
@@ -0,0 +1,44 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "remove"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.2">RFC 6902 § 4.2</a>.
+ *
+ * <p>
+ * Removes the value at {@link #getPath() path}. Carries no extra members beyond {@code op} and {@code path}.
+ */
+@Marshalled(typeName = "remove")
+public class RemoveOp extends JsonPatchOperation {
+
+ /**
+ * Default constructor.
+ */
+ public RemoveOp() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer target.
+ */
+ public RemoveOp(String path) {
+ super(path);
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/ReplaceOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/ReplaceOp.java
new file mode 100644
index 0000000000..701828b3a1
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/ReplaceOp.java
@@ -0,0 +1,66 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "replace"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.3">RFC 6902 § 4.3</a>.
+ *
+ * <p>
+ * Replaces the value at {@link #getPath() path} with {@link #getValue() value}.
+ */
+@Marshalled(typeName = "replace")
+public class ReplaceOp extends JsonPatchOperation {
+
+ private Object value;
+
+ /**
+ * Default constructor.
+ */
+ public ReplaceOp() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer target.
+ * @param value The replacement value.
+ */
+ public ReplaceOp(String path, Object value) {
+ super(path);
+ this.value = value;
+ }
+
+ /**
+ * Bean property getter: <property>value</property>.
+ *
+ * @return The value of the <property>value</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Object getValue() { return value; }
+
+ /**
+ * Bean property setter: <property>value</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public ReplaceOp setValue(Object value) {
+ this.value = value;
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/TestOp.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/TestOp.java
new file mode 100644
index 0000000000..5d422d237a
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/TestOp.java
@@ -0,0 +1,66 @@
+/*
+ * 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.bean.jsonpatch;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * The JSON Patch {@code "test"} operation per
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902#section-4.6">RFC 6902 § 4.6</a>.
+ *
+ * <p>
+ * Tests that the value at {@link #getPath() path} equals {@link #getValue() value}.
+ */
+@Marshalled(typeName = "test")
+public class TestOp extends JsonPatchOperation {
+
+ private Object value;
+
+ /**
+ * Default constructor.
+ */
+ public TestOp() {}
+
+ /**
+ * Convenience constructor.
+ *
+ * @param path The JSON Pointer target.
+ * @param value The value to test for.
+ */
+ public TestOp(String path, Object value) {
+ super(path);
+ this.value = value;
+ }
+
+ /**
+ * Bean property getter: <property>value</property>.
+ *
+ * @return The value of the <property>value</property> property, or <jk>null</jk> if it is not set.
+ */
+ public Object getValue() { return value; }
+
+ /**
+ * Bean property setter: <property>value</property>.
+ *
+ * @param value The new value for this property.
+ * @return This object.
+ */
+ public TestOp setValue(Object value) {
+ this.value = value;
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/package-info.java b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/package-info.java
new file mode 100644
index 0000000000..7d9d6285c1
--- /dev/null
+++ b/juneau-bean/juneau-bean-jsonpatch/src/main/java/org/apache/juneau/bean/jsonpatch/package-info.java
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} types for
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6902">JSON Patch (RFC 6902)</a>.
+ *
+ * <h5 class='topic'>Overview</h5>
+ *
+ * <p>
+ * A JSON Patch document is a JSON array of operation objects describing changes to apply to a target JSON
+ * document. Each operation object has an {@code op} member naming one of the six operation types
+ * ({@code add}, {@code remove}, {@code replace}, {@code move}, {@code copy}, {@code test}), a {@code path}
+ * member (JSON Pointer per RFC 6901), and op-specific additional members. The matching {@code ContentType}
+ * constant is {@code org.apache.juneau.http.header.ContentType#APPLICATION_JSON_PATCH}.
+ *
+ * <h5 class='topic'>Bean Classes</h5>
+ * <ul class='spaced-list'>
+ * <li>{@link org.apache.juneau.bean.jsonpatch.JsonPatchOperation} - Abstract base. Polymorphic dispatch
+ * wired via {@code @Marshalled(typePropertyName="op", dictionary={...})}, so the parser materializes the
+ * correct concrete subclass keyed off the {@code op} member.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.AddOp} - {@code op:"add"} + {@code value}.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.RemoveOp} - {@code op:"remove"}.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.ReplaceOp} - {@code op:"replace"} + {@code value}.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.MoveOp} - {@code op:"move"} + {@code from}.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.CopyOp} - {@code op:"copy"} + {@code from}.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.TestOp} - {@code op:"test"} + {@code value}.
+ * <li>{@link org.apache.juneau.bean.jsonpatch.JsonPatch} - {@code LinkedList<JsonPatchOperation>};
+ * the JSON Patch <i>document</i> serializes as a top-level JSON array (mirrors {@code JsonSchemaArray}).
+ * </ul>
+ *
+ * <h5 class='topic'>Example</h5>
+ *
+ * <p class='bjava'>
+ * JsonPatch <jv>patch</jv> = <jk>new</jk> JsonPatch()
+ * .append(<jk>new</jk> AddOp(<js>"/a/b/c"</js>, <js>"foo"</js>))
+ * .append(<jk>new</jk> RemoveOp(<js>"/a/b/c"</js>))
+ * .append(<jk>new</jk> ReplaceOp(<js>"/a/b/c"</js>, 42))
+ * .append(<jk>new</jk> MoveOp(<js>"/a/b/c"</js>, <js>"/a/b/d"</js>))
+ * .append(<jk>new</jk> CopyOp(<js>"/a/b/c"</js>, <js>"/a/b/e"</js>))
+ * .append(<jk>new</jk> TestOp(<js>"/a/b/c"</js>, <js>"foo"</js>));
+ *
+ * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>patch</jv>);
+ * JsonPatch <jv>back</jv> = JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>json</jv>, JsonPatch.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a href="https://datatracker.ietf.org/doc/html/rfc6902">RFC 6902 - JSON Patch</a>
+ * <li class='link'><a href="https://datatracker.ietf.org/doc/html/rfc6901">RFC 6901 - JSON Pointer</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanJsonPatch">juneau-bean-jsonpatch</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.jsonpatch;
diff --git a/juneau-bean/juneau-bean-rfc7807/pom.xml b/juneau-bean/juneau-bean-rfc7807/pom.xml
new file mode 100644
index 0000000000..2159996b2f
--- /dev/null
+++ b/juneau-bean/juneau-bean-rfc7807/pom.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean</artifactId>
+ <version>9.5.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>juneau-bean-rfc7807</artifactId>
+ <name>Apache Juneau RFC 7807 Beans</name>
+ <description>Apache Juneau Serializable Beans - RFC 7807 Problem Details for HTTP APIs (application/problem+json)</description>
+ <packaging>bundle</packaging>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-marshall</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <supportIncrementalBuild>true</supportIncrementalBuild>
+ </configuration>
+ <executions>
+ <execution>
+ <id>bundle-manifest</id>
+ <phase>process-classes</phase>
+ <goals>
+ <goal>manifest</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>attach-sources</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>jar-no-fork</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-prepare-agent</id>
+ <goals>
+ <goal>prepare-agent</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>default-report</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>report</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/Problem.java b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/Problem.java
new file mode 100644
index 0000000000..1772d29602
--- /dev/null
+++ b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/Problem.java
@@ -0,0 +1,274 @@
+/*
+ * 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.bean.rfc7807;
+
+import java.net.*;
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.commons.bean.*;
+
+/**
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807">RFC 7807 — Problem Details for HTTP APIs</a>
+ * bean.
+ *
+ * <p>
+ * Represents the canonical {@code application/problem+json} document. The five standard members defined by
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">§3.1</a> are modelled as typed
+ * properties; arbitrary <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">§3.2</a>
+ * extension members flatten into the top-level JSON object via a {@link BeanProp @BeanProp("*")} triplet.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bjava'>
+ * Problem <jv>problem</jv> = <jk>new</jk> Problem()
+ * .setType(<jk>new</jk> URI(<js>"https://example.com/probs/out-of-credit"</js>))
+ * .setTitle(<js>"You do not have enough credit."</js>)
+ * .setStatus(403)
+ * .setDetail(<js>"Your current balance is 30, but that costs 50."</js>)
+ * .setInstance(<jk>new</jk> URI(<js>"/account/12345/msgs/abc"</js>))
+ * .set(<js>"balance"</js>, 30)
+ * .set(<js>"accounts"</js>, <jsm>list</jsm>(<js>"/account/12345"</js>, <js>"/account/67890"</js>));
+ *
+ * String <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>problem</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5>
+ * <ul class='spaced-list'>
+ * <li>The {@code type} field is nullable. {@link #getType()} returns the raw value (possibly {@code null});
+ * {@link #getTypeOrDefault()} returns the spec default {@code "about:blank"} per
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 §3.1</a> when the
+ * field is unset. The field is never eagerly defaulted, so a fresh {@code Problem} round-trips without a
+ * synthetic {@code "type":"about:blank"} on the wire.
+ * <li>RFC 7807 was obsoleted by <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc9457">RFC 9457</a> in
+ * July 2023, but the data model and the {@code application/problem+json} IANA registration are unchanged.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanRfc7807">juneau-bean-rfc7807</a>
+ * </ul>
+ */
+@Marshalled
+public class Problem {
+
+ /**
+ * The default {@code type} URI defined by
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 §3.1</a> for problem
+ * documents that omit the {@code type} member.
+ */
+ public static final URI DEFAULT_TYPE = URI.create("about:blank");
+
+ private URI type;
+ private String title;
+ private Integer status;
+ private String detail;
+ private URI instance;
+ private Map<String,Object> extra;
+
+ /**
+ * Convenience factory for the common "build a problem from a status code + title + detail" call site.
+ *
+ * <p>
+ * Populates only the {@code status}, {@code title}, and {@code detail} members. Callers wire from
+ * {@link Throwable}s or HTTP exception types themselves — for example:
+ * <p class='bjava'>
+ * Problem.<jsm>fromStatus</jsm>(<jv>e</jv>.getStatusCode(), <jv>e</jv>.getStatusLine().getReasonPhrase(), <jv>e</jv>.getMessage());
+ * </p>
+ *
+ * @param status The HTTP status code (e.g. {@code 404}).
+ * @param title A short, human-readable summary of the problem type. Can be <jk>null</jk>.
+ * @param detail A human-readable explanation specific to this occurrence. Can be <jk>null</jk>.
+ * @return A new {@link Problem} populated with the supplied values.
+ */
+ public static Problem fromStatus(int status, String title, String detail) {
+ return new Problem().setStatus(status).setTitle(title).setDetail(detail);
+ }
+
+ /**
+ * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">type</a> URI that identifies
+ * the problem type.
+ *
+ * <p>
+ * Returns the raw field value, which may be <jk>null</jk> when the {@code type} member is absent. Use
+ * {@link #getTypeOrDefault()} to get the spec default {@code "about:blank"} for absent values.
+ *
+ * @return The {@code type} URI, or <jk>null</jk> if not set.
+ */
+ public URI getType() {
+ return type;
+ }
+
+ /**
+ * Returns the {@code type} URI, defaulting to {@link #DEFAULT_TYPE} ({@code "about:blank"}) per
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">RFC 7807 §3.1</a> when the
+ * field is <jk>null</jk>.
+ *
+ * <p>
+ * The underlying field is left untouched, so this method never causes a synthetic {@code "type":"about:blank"}
+ * to appear on the wire.
+ *
+ * @return The {@code type} URI, never <jk>null</jk>.
+ */
+ @BeanIgnore
+ public URI getTypeOrDefault() {
+ return type == null ? DEFAULT_TYPE : type;
+ }
+
+ /**
+ * Sets the {@code type} URI.
+ *
+ * @param value The new value. Can be <jk>null</jk> to unset the property.
+ * @return This object.
+ */
+ public Problem setType(URI value) {
+ type = value;
+ return this;
+ }
+
+ /**
+ * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">title</a> — a short,
+ * human-readable summary of the problem type.
+ *
+ * @return The title, or <jk>null</jk> if not set.
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Sets the {@code title}.
+ *
+ * @param value The new value. Can be <jk>null</jk> to unset the property.
+ * @return This object.
+ */
+ public Problem setTitle(String value) {
+ title = value;
+ return this;
+ }
+
+ /**
+ * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">status</a> — the HTTP
+ * status code originally generated by the origin server for this occurrence.
+ *
+ * <p>
+ * Modelled as {@link Integer} (boxed) because RFC 7807 declares {@code status} as OPTIONAL.
+ *
+ * @return The status code, or <jk>null</jk> if not set.
+ */
+ public Integer getStatus() {
+ return status;
+ }
+
+ /**
+ * Sets the {@code status} HTTP status code.
+ *
+ * @param value The new value. Can be <jk>null</jk> to unset the property.
+ * @return This object.
+ */
+ public Problem setStatus(Integer value) {
+ status = value;
+ return this;
+ }
+
+ /**
+ * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">detail</a> — a
+ * human-readable explanation specific to this occurrence of the problem.
+ *
+ * @return The detail, or <jk>null</jk> if not set.
+ */
+ public String getDetail() {
+ return detail;
+ }
+
+ /**
+ * Sets the {@code detail}.
+ *
+ * @param value The new value. Can be <jk>null</jk> to unset the property.
+ * @return This object.
+ */
+ public Problem setDetail(String value) {
+ detail = value;
+ return this;
+ }
+
+ /**
+ * The <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.1">instance</a> URI that
+ * identifies the specific occurrence of the problem.
+ *
+ * @return The {@code instance} URI, or <jk>null</jk> if not set.
+ */
+ public URI getInstance() {
+ return instance;
+ }
+
+ /**
+ * Sets the {@code instance} URI.
+ *
+ * @param value The new value. Can be <jk>null</jk> to unset the property.
+ * @return This object.
+ */
+ public Problem setInstance(URI value) {
+ instance = value;
+ return this;
+ }
+
+ /**
+ * Generic <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">extension-member</a>
+ * keyset.
+ *
+ * @return All extension keys on this problem. Never <jk>null</jk>.
+ */
+ @BeanProp("*")
+ public Set<String> extraKeys() {
+ return extra == null ? Collections.emptySet() : extra.keySet();
+ }
+
+ /**
+ * Generic extension-member getter.
+ *
+ * <p>
+ * Used by the bean framework to read arbitrary
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">§3.2</a> extension members
+ * that flatten into the top-level JSON object.
+ *
+ * @param property The property name to retrieve. Must not be <jk>null</jk>.
+ * @return The property value, or <jk>null</jk> if the property does not exist.
+ */
+ @BeanProp("*")
+ public Object get(String property) {
+ return extra == null ? null : extra.get(property);
+ }
+
+ /**
+ * Generic extension-member setter.
+ *
+ * <p>
+ * Used by the bean framework to set arbitrary
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807#section-3.2">§3.2</a> extension members.
+ * The extension map is lazily initialised on first use.
+ *
+ * @param property The property name to set. Must not be <jk>null</jk>.
+ * @param value The new value for the property.
+ * @return This object.
+ */
+ @BeanProp("*")
+ public Problem set(String property, Object value) {
+ if (extra == null)
+ extra = new LinkedHashMap<>();
+ extra.put(property, value);
+ return this;
+ }
+}
diff --git a/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/package-info.java b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/package-info.java
new file mode 100644
index 0000000000..a89914d5b0
--- /dev/null
+++ b/juneau-bean/juneau-bean-rfc7807/src/main/java/org/apache/juneau/bean/rfc7807/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+/**
+ * Juneau {@link org.apache.juneau.annotation.Marshalled @Marshalled} beans for
+ * <a class="doclink" href="https://www.rfc-editor.org/rfc/rfc7807">RFC 7807 — Problem Details for HTTP APIs</a>
+ * (media type {@code application/problem+json}).
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauBeanRfc7807">juneau-bean-rfc7807</a>
+ * </ul>
+ */
+package org.apache.juneau.bean.rfc7807;
diff --git a/juneau-bean/pom.xml b/juneau-bean/pom.xml
index 7e7c6d0b88..9269640e0b 100644
--- a/juneau-bean/pom.xml
+++ b/juneau-bean/pom.xml
@@ -33,10 +33,14 @@
<modules>
<module>juneau-bean-atom</module>
<module>juneau-bean-common</module>
+ <module>juneau-bean-hal</module>
<module>juneau-bean-html5</module>
+ <module>juneau-bean-jsonapi</module>
+ <module>juneau-bean-jsonpatch</module>
<module>juneau-bean-jsonschema</module>
<module>juneau-bean-mcp</module>
<module>juneau-bean-openapi-v3</module>
+ <module>juneau-bean-rfc7807</module>
<module>juneau-bean-swagger-v2</module>
</modules>
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java
index 359c4595bb..b0353d6fdc 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/bean/BeanSession.java
@@ -84,7 +84,7 @@ public interface BeanSession {
* directly, so the parse is delegated to the session via this SPI.
*
* <p>
- * Implementations typically route through {@code JsonMap.ofText(value).session(this)}.
+ * Implementations typically route through {@code JsonMap.ofString(value).session(this)}.
*
* @param value The JSON-formatted character sequence to parse. Must not be <jk>null</jk>.
* @return The parsed map.
diff --git a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java
index 878833e222..4e48a3bada 100644
--- a/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java
+++ b/juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/utils/ThrowableUtils.java
@@ -313,8 +313,11 @@ public class ThrowableUtils {
* @param maxDepth The maximum number of stack trace elements to print. If <jk>null</jk> or negative, prints all elements.
*/
public static void printStackTrace(Throwable t, Integer maxDepth) {
- LOG.warning(t, "Stack trace");
- printStackTrace(t, new PrintWriter(System.err, true), maxDepth);
+ var sw = new StringWriter();
+ try (var pw = new PrintWriter(sw)) {
+ printStackTrace(t, pw, maxDepth);
+ }
+ LOG.warning(t, sw.toString());
}
/**
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java
index ad32643437..cca1d4820f 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Entry.java
@@ -351,7 +351,7 @@ public class Entry {
if (s1 != '[' && ! "null".equals(s))
s = '[' + s + ']';
}
- return opt(JsonList.ofText(s, parser));
+ return opt(JsonList.ofString(s, parser));
}
/**
@@ -425,7 +425,7 @@ public class Entry {
if (s1 != '{' && ! "null".equals(s))
s = '{' + s + '}';
}
- return opt(JsonMap.ofText(s, parser));
+ return opt(JsonMap.ofString(s, parser));
}
/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java
index 27824e84b7..6ba1641c91 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMapLoader.java
@@ -50,7 +50,7 @@ public final class BeanMapLoader {
* @throws ParseException Malformed input encountered.
*/
public static <T> BeanMap<T> load(BeanMap<T> m, String input) throws ParseException {
- m.putAll(Json5Map.ofText(input));
+ m.putAll(Json5Map.ofString(input));
return m;
}
@@ -69,7 +69,7 @@ public final class BeanMapLoader {
* @throws IOException Thrown by the reader.
*/
public static <T> BeanMap<T> load(BeanMap<T> m, Reader r, ReaderParser p) throws ParseException, IOException {
- m.putAll(JsonMap.ofText(r, p));
+ m.putAll(JsonMap.ofString(r, p));
return m;
}
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java
index 712839a4f4..dd94a70848 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingContext.java
@@ -4118,7 +4118,7 @@ public class MarshallingContext extends Context implements ConversionFinder, Bea
var str = in.toString();
Json5List list;
if (startsWith(str, '['))
- list = Json5List.ofText(str).setBeanSession(bs);
+ list = Json5List.ofString(str).setBeanSession(bs);
else
list = new Json5List((Object[]) splita(str)).setBeanSession(bs);
return bs.convertToType(list, outType);
@@ -4133,7 +4133,7 @@ public class MarshallingContext extends Context implements ConversionFinder, Bea
return (in, memberOf, session, args) -> {
try {
var bs = beanSessionOrDefault(session);
- var m = Json5Map.ofText(in.toString());
+ var m = Json5Map.ofString(in.toString());
m.setBeanSession(bs);
return bs.convertToType(m, outType);
} catch (Exception e) {
@@ -4197,7 +4197,7 @@ public class MarshallingContext extends Context implements ConversionFinder, Bea
if (!isProbablyJsonArray(str, false))
throw rex("Cannot convert string to {0}: {1}", outType.getName(), str);
var elemType = args.length > 0 ? args[0] : null;
- var l2 = Json5List.ofText(str).setBeanSession(bs);
+ var l2 = Json5List.ofString(str).setBeanSession(bs);
var result = newCollection(outType);
l2.forEach(x -> result.add(elemType != null && x != null ? converter.to(x, elemType) : x));
return result;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java
index 3e8b3f9241..88396d36c5 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/MarshallingSession.java
@@ -1374,7 +1374,7 @@ public class MarshallingSession extends ContextSession implements ConverterSessi
/**
* Bridge implementation of {@link BeanSession#parseToMap(CharSequence)} that delegates to
- * {@link Json5Map#ofText(CharSequence)} paired with this session.
+ * {@link Json5Map#ofString(CharSequence)} paired with this session.
*
* <p>
* Used by {@link BeanPropertyMeta#setPropertyValue} to parse a {@link CharSequence} into a {@link Map} when
@@ -1390,8 +1390,8 @@ public class MarshallingSession extends ContextSession implements ConverterSessi
* @return The parsed {@link Json5Map}.
*/
@Override /* BeanSession */
- public final java.util.Map<?,?> parseToMap(CharSequence value) {
- return Json5Map.ofText(value).session(this);
+ public final Map<?,?> parseToMap(CharSequence value) {
+ return Json5Map.ofString(value).session(this);
}
/**
@@ -1411,7 +1411,7 @@ public class MarshallingSession extends ContextSession implements ConverterSessi
* @return The parsed {@link Json5List}.
*/
@Override /* BeanSession */
- public final java.util.Collection<?> parseToList(CharSequence value) {
+ public final Collection<?> parseToList(CharSequence value) {
return new Json5List(value).setBeanSession(this);
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java
index 1673177574..d073fd4746 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriContext.java
@@ -159,7 +159,7 @@ public class UriContext {
* If input string is not a valid JSON object.
*/
public UriContext(String s) throws ParseException {
- var m = Json5Map.ofText(s);
+ var m = Json5Map.ofString(s);
this.authority = nullIfEmpty(trimSlashes(m.getString(PROP_authority)));
this.contextRoot = nullIfEmpty(trimSlashes(m.getString(PROP_contextRoot)));
this.servletPath = nullIfEmpty(trimSlashes(m.getString(PROP_servletPath)));
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
index e413047a7e..87ff0377d6 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonList.java
@@ -64,24 +64,24 @@ import org.apache.juneau.serializer.*;
* <jv>list</jv> = JsonList.<jsm>of</jsm>(<js>"foo"</js>, 123, <jk>true</jk>); <jc>// Equivalent</jc>
*
* <jc>// Construct a list of integers from JSON</jc>
- * <jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[1,2,3]"</js>);
+ * <jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[1,2,3]"</js>);
*
* <jc>// Construct a list of generic JsonMap objects from JSON</jc>
- * <jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
+ * <jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
*
* <jc>// Construct JSON from JsonList</jc>
- * <jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
+ * <jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
* String <jv>json</jv> = <jv>list</jv>.toString(); <jc>// Produces "[{foo:'bar'},{baz:'bing'}]"</jc>
* <jv>json</jv> = <jv>list</jv>.toString(JsonSerializer.<jsf>DEFAULT</jsf>); <jc>// Equivalent</jc>
* <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>list</jv>); <jc>// Equivalent</jc>
*
* <jc>// Get one of the entries in the list as an Integer</jc>
- * <jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[1,2,3]"</js>);
+ * <jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[1,2,3]"</js>);
* Integer <jv>integer</jv> = <jv>list</jv>.getInt(1);
* <jv>integer</jv> = <jv>list</jv>.get(Integer.<jk>class</jk>, 1); <jc>// Equivalent</jc>
*
* <jc>// Iterate over a list of beans using the elements() method</jc>
- * <jv>list</jv> = JsonList.<jsm>ofText</jsm>(<js>"[{name:'John Smith',age:45}]"</js>);
+ * <jv>list</jv> = JsonList.<jsm>ofString</jsm>(<js>"[{name:'John Smith',age:45}]"</js>);
* <jk>for</jk> (Person <jv>person</jv> : <jv>list</jv>.elements(Person.<jk>class</jk>) {
* <jc>// Do something with p</jc>
* }
@@ -236,7 +236,7 @@ public class JsonList extends MarshalledList {
* @return A new list or <jk>null</jk> if the string was null.
* @throws ParseException Malformed input encountered.
*/
- public static JsonList ofText(CharSequence json) throws ParseException {
+ public static JsonList ofString(CharSequence json) throws ParseException {
return json == null ? null : new JsonList(json);
}
@@ -252,7 +252,7 @@ public class JsonList extends MarshalledList {
@SuppressWarnings({
"java:S1168" // TODO: null input = null output by design. Consider empty JsonList.
})
- public static JsonList ofText(Reader json) throws ParseException {
+ public static JsonList ofString(Reader json) throws ParseException {
return json == null ? null : new JsonList(json);
}
@@ -289,7 +289,7 @@ public class JsonList extends MarshalledList {
* @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static JsonList ofText(CharSequence in, Parser p) throws ParseException {
+ public static JsonList ofString(CharSequence in, Parser p) throws ParseException {
return in == null ? null : new JsonList(in, p);
}
@@ -308,7 +308,7 @@ public class JsonList extends MarshalledList {
@SuppressWarnings({
"java:S1172" // Parameter reserved for future parser context support
})
- public static JsonList ofText(Reader in, Parser p) throws ParseException {
+ public static JsonList ofString(Reader in, Parser p) throws ParseException {
return in == null ? null : new JsonList(in);
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java
index b69dddf231..7496223153 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/JsonMap.java
@@ -58,7 +58,7 @@ import org.apache.juneau.serializer.*;
* JsonMap <jv>map</jv> = JsonMap.<jsm>of</jsm>();
*
* <jc>// Construct a Map from JSON</jc>
- * <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
+ * <jv>map</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
*
* <jc>// Construct a Map using the append method</jc>
* <jv>map</jv> = JsonMap.<jsm>of</jsm>().a(<js>"foo"</js>,<js>"x"</js>).a(<js>"bar"</js>,123).a(<js>"baz"</js>,<jk>true</jk>);
@@ -72,19 +72,19 @@ import org.apache.juneau.serializer.*;
* <jv>map</jv> = JsonMap.<jsm>of</jsm>(<jv>urlParams</jv>, UrlEncodingParser.<jsf>DEFAULT</jsf>);
*
* <jc>// Construct JSON from JsonMap</jc>
- * <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{foo:'bar'},{baz:[123,true]}"</js>);
+ * <jv>map</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{foo:'bar'},{baz:[123,true]}"</js>);
* String <jv>json</jv> = <jv>map</jv>.toString(); <jc>// Produces "{foo:'bar'},{baz:[123,true]}"</jc>
* <jv>json</jv> = <jv>map</jv>.toString(JsonSerializer.<jsf>DEFAULT</jsf>); <jc>// Equivalent</jc>
* <jv>json</jv> = JsonSerializer.<jsf>DEFAULT</jsf>.serialize(<jv>map</jv>); <jc>// Equivalent</jc>
*
* <jc>// Get a map entry as an Integer</jc>
- * <jv>map</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{foo:123}"</js>);
+ * <jv>map</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{foo:123}"</js>);
* Integer <jv>integer</jv> = <jv>map</jv>.getInt(<js>"foo"</js>);
* <jv>integer</jv> = <jv>map</jv>.get(Integer.<jk>class</jk>, <js>"foo"</js>); <jc>// Equivalent</jc>
*
* <jc>// Add an inner map</jc>
- * JsonMap <jv>map1</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{a:1}"</js>);
- * JsonMap <jv>map2</jv> = JsonMap.<jsm>ofText</jsm>(<js>"{b:2}"</js>).setInner(<jv>map1</jv>);
+ * JsonMap <jv>map1</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{a:1}"</js>);
+ * JsonMap <jv>map2</jv> = JsonMap.<jsm>ofString</jsm>(<js>"{b:2}"</js>).setInner(<jv>map1</jv>);
* <jk>int</jk> <jv>_int</jv> = <jv>map2</jv>.getInt(<js>"a"</js>); <jc>// a == 1 </jc>
* </p>
*
@@ -98,7 +98,8 @@ import org.apache.juneau.serializer.*;
public class JsonMap extends MarshalledMap {
@SuppressWarnings({
- "java:S2160" // equals() / hashCode() inherited from JsonMap; map equality is element-based
+ "java:S2160", // equals() / hashCode() inherited from JsonMap; map equality is element-based
+ "java:S110" // Inheritance depth is intentional in the marshalled-map hierarchy.
})
private static class UnmodifiableJsonMap extends JsonMap {
private static final long serialVersionUID = 1L;
@@ -133,7 +134,8 @@ public class JsonMap extends MarshalledMap {
* @serial exclude
*/
@SuppressWarnings({
- "java:S2386" // Public static final field accessed externally, cannot be protected
+ "java:S2386", // Public static final field accessed externally, cannot be protected
+ "java:S110" // Anonymous JsonMap subtype intentionally inherits deep collection hierarchy.
})
public static final JsonMap EMPTY_MAP = new JsonMap() {
@@ -233,7 +235,7 @@ public class JsonMap extends MarshalledMap {
* @return A new map or <jk>null</jk> if the string was null.
* @throws ParseException Malformed input encountered.
*/
- public static JsonMap ofText(CharSequence json) throws ParseException {
+ public static JsonMap ofString(CharSequence json) throws ParseException {
return json == null ? null : new JsonMap(json);
}
@@ -246,7 +248,7 @@ public class JsonMap extends MarshalledMap {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static JsonMap ofText(Reader json) throws ParseException {
+ public static JsonMap ofString(Reader json) throws ParseException {
return json == null ? null : new JsonMap(json);
}
@@ -262,7 +264,7 @@ public class JsonMap extends MarshalledMap {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static JsonMap ofText(CharSequence in, Parser p) throws ParseException {
+ public static JsonMap ofString(CharSequence in, Parser p) throws ParseException {
return in == null ? null : new JsonMap(in, p);
}
@@ -278,7 +280,7 @@ public class JsonMap extends MarshalledMap {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static JsonMap ofText(Reader in, Parser p) throws ParseException {
+ public static JsonMap ofString(Reader in, Parser p) throws ParseException {
return in == null ? null : new JsonMap(in, p);
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java
index f38d86f5b0..2d28ea0364 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledList.java
@@ -197,7 +197,7 @@ public class MarshalledList extends LinkedList<Object> {
* @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static MarshalledList ofText(CharSequence in, Parser p) throws ParseException {
+ public static MarshalledList ofString(CharSequence in, Parser p) throws ParseException {
return in == null ? null : new MarshalledList(in, p);
}
@@ -212,7 +212,7 @@ public class MarshalledList extends LinkedList<Object> {
* @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static MarshalledList ofText(java.io.Reader in, Parser p) throws ParseException {
+ public static MarshalledList ofString(java.io.Reader in, Parser p) throws ParseException {
return in == null ? null : new MarshalledList(in, p);
}
@@ -246,8 +246,7 @@ public class MarshalledList extends LinkedList<Object> {
* @throws ParseException Malformed input encountered.
*/
public MarshalledList(CharSequence in, Parser p) throws ParseException {
- this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
- assertArg(p != null, "Parser must not be null.");
+ this(assertArgNotNull("p", p).getMarshallingContext().getSession());
if (nn(in))
p.parseIntoCollection(in, this, bs().object());
}
@@ -283,8 +282,7 @@ public class MarshalledList extends LinkedList<Object> {
* @throws ParseException Malformed input encountered.
*/
public MarshalledList(java.io.Reader in, Parser p) throws ParseException {
- this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
- assertArg(p != null, "Parser must not be null.");
+ this(assertArgNotNull("p", p).getMarshallingContext().getSession());
p.parseIntoCollection(in, this, bs().object());
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java
index 7c44257f42..cea0087ea5 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/MarshalledMap.java
@@ -190,7 +190,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static MarshalledMap ofText(CharSequence in, Parser p) throws ParseException {
+ public static MarshalledMap ofString(CharSequence in, Parser p) throws ParseException {
return in == null ? null : new MarshalledMap(in, p);
}
@@ -205,7 +205,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static MarshalledMap ofText(java.io.Reader in, Parser p) throws ParseException {
+ public static MarshalledMap ofString(java.io.Reader in, Parser p) throws ParseException {
return in == null ? null : new MarshalledMap(in, p);
}
@@ -252,8 +252,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
* @throws ParseException Malformed input encountered.
*/
public MarshalledMap(CharSequence in, Parser p) throws ParseException {
- this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
- assertArg(p != null, "Parser must not be null.");
+ this(assertArgNotNull("p", p).getMarshallingContext().getSession());
if (ne(in))
p.parseIntoMap(in, this, bs().string(), bs().object());
}
@@ -294,8 +293,7 @@ public class MarshalledMap extends LinkedHashMap<String,Object> {
* @throws ParseException Malformed input encountered.
*/
public MarshalledMap(java.io.Reader in, Parser p) throws ParseException {
- this(p == null ? MarshallingContext.DEFAULT_SESSION : p.getMarshallingContext().getSession());
- assertArg(p != null, "Parser must not be null.");
+ this(assertArgNotNull("p", p).getMarshallingContext().getSession());
p.parseIntoMap(in, this, bs().string(), bs().object());
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java
index 8e3354f86c..5076072034 100755
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/collections/package-info.java
@@ -28,13 +28,13 @@
* — strict-JSON-flavored subclasses. Their {@code toString()} renders as strict
* <a href="https://www.rfc-editor.org/rfc/rfc8259" target="_blank">RFC 8259</a> JSON via
* {@link org.apache.juneau.marshaller.Json#of(Object)}, and their {@code (CharSequence)} /
- * {@code (Reader)} constructors and {@code ofText(...)} factories default to
+ * {@code (Reader)} constructors and {@code ofString(...)} factories default to
* {@link org.apache.juneau.json.JsonParser#DEFAULT}.
* <li>{@link org.apache.juneau.json5.Json5Map} / {@link org.apache.juneau.json5.Json5List}
* (in {@link org.apache.juneau.json5}) — JSON5-flavored siblings of
* {@code JsonMap} / {@code JsonList}. Their {@code toString()} renders as JSON5 via
* {@link org.apache.juneau.marshaller.Json5#of(Object)}, and their {@code (CharSequence)} /
- * {@code (Reader)} constructors and {@code ofText(...)} factories default to
+ * {@code (Reader)} constructors and {@code ofString(...)} factories default to
* {@link org.apache.juneau.json5.Json5Parser#DEFAULT}. These are the drop-in replacement
* for callers that previously relied on {@code JsonMap.toString()} producing JSON5 or on
* {@code new JsonMap("{unquoted:'json5'}")} parsing JSON5.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java
index 8b6c4422d3..d886af6cbe 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5List.java
@@ -49,7 +49,7 @@ import org.apache.juneau.serializer.*;
* Json5List <jv>list</jv> = Json5List.<jsm>of</jsm>();
*
* <jc>// Construct a list of generic Json5Map objects from JSON5</jc>
- * <jv>list</jv> = Json5List.<jsm>ofText</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
+ * <jv>list</jv> = Json5List.<jsm>ofString</jsm>(<js>"[{foo:'bar'},{baz:'bing'}]"</js>);
*
* <jc>// JSON5 round-trip via toString()</jc>
* String <jv>s</jv> = <jv>list</jv>.toString(); <jc>// "[{foo:'bar'},{baz:'bing'}]"</jc>
@@ -200,7 +200,7 @@ public class Json5List extends MarshalledList {
* @return A new list or <jk>null</jk> if the string was null.
* @throws ParseException Malformed input encountered.
*/
- public static Json5List ofText(CharSequence json5) throws ParseException {
+ public static Json5List ofString(CharSequence json5) throws ParseException {
return json5 == null ? null : new Json5List(json5);
}
@@ -214,7 +214,7 @@ public class Json5List extends MarshalledList {
@SuppressWarnings({
"java:S1168" // TODO: null input = null output by design. Consider empty Json5List.
})
- public static Json5List ofText(Reader json5) throws ParseException {
+ public static Json5List ofString(Reader json5) throws ParseException {
return json5 == null ? null : new Json5List(json5);
}
@@ -247,7 +247,7 @@ public class Json5List extends MarshalledList {
* @return A new list or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static Json5List ofText(CharSequence in, Parser p) throws ParseException {
+ public static Json5List ofString(CharSequence in, Parser p) throws ParseException {
return in == null ? null : new Json5List(in, p);
}
@@ -262,7 +262,7 @@ public class Json5List extends MarshalledList {
@SuppressWarnings({
"java:S1172" // Parameter reserved for future parser context support
})
- public static Json5List ofText(Reader in, Parser p) throws ParseException {
+ public static Json5List ofString(Reader in, Parser p) throws ParseException {
return in == null ? null : new Json5List(in);
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java
index df777e52f3..3ba3320268 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/json5/Json5Map.java
@@ -49,7 +49,7 @@ import org.apache.juneau.serializer.*;
* Json5Map <jv>map</jv> = Json5Map.<jsm>of</jsm>();
*
* <jc>// Construct a Map from JSON5</jc>
- * <jv>map</jv> = Json5Map.<jsm>ofText</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
+ * <jv>map</jv> = Json5Map.<jsm>ofString</jsm>(<js>"{a:'A',b:{c:'C',d:123}}"</js>);
*
* <jc>// JSON5 round-trip via toString()</jc>
* String <jv>s</jv> = <jv>map</jv>.toString(); <jc>// "{a:'A',b:{c:'C',d:123}}"</jc>
@@ -201,7 +201,7 @@ public class Json5Map extends MarshalledMap {
* @return A new map or <jk>null</jk> if the string was null.
* @throws ParseException Malformed input encountered.
*/
- public static Json5Map ofText(CharSequence json5) throws ParseException {
+ public static Json5Map ofString(CharSequence json5) throws ParseException {
return json5 == null ? null : new Json5Map(json5);
}
@@ -213,7 +213,7 @@ public class Json5Map extends MarshalledMap {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static Json5Map ofText(Reader json5) throws ParseException {
+ public static Json5Map ofString(Reader json5) throws ParseException {
return json5 == null ? null : new Json5Map(json5);
}
@@ -229,7 +229,7 @@ public class Json5Map extends MarshalledMap {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static Json5Map ofText(CharSequence in, Parser p) throws ParseException {
+ public static Json5Map ofString(CharSequence in, Parser p) throws ParseException {
return in == null ? null : new Json5Map(in, p);
}
@@ -244,7 +244,7 @@ public class Json5Map extends MarshalledMap {
* @return A new map or <jk>null</jk> if the input was <jk>null</jk>.
* @throws ParseException Malformed input encountered.
*/
- public static Json5Map ofText(Reader in, Parser p) throws ParseException {
+ public static Json5Map ofString(Reader in, Parser p) throws ParseException {
return in == null ? null : new Json5Map(in, p);
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java
index fed5269fe1..9fc1bf6342 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/jsonschema/SchemaUtils.java
@@ -78,7 +78,7 @@ public class SchemaUtils {
return JsonMap.of("ignore", true);
if (! isProbablyJsonObject(s, true))
s = "{" + s + "}";
- return new JsonMap(Json5Map.ofText(s));
+ return new JsonMap(Json5Map.ofString(s));
}
if (o instanceof JsonMap o2)
return o2;
@@ -105,7 +105,7 @@ public class SchemaUtils {
return null;
if (! isProbablyJsonObject(s, true))
s = "{" + s + "}";
- return new JsonMap(Json5Map.ofText(s));
+ return new JsonMap(Json5Map.ofString(s));
}
/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Sse.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Sse.java
new file mode 100644
index 0000000000..9123e59494
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshaller/Sse.java
@@ -0,0 +1,154 @@
+/*
+ * 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.marshaller;
+
+import java.io.*;
+import java.lang.reflect.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.ParseException;
+import org.apache.juneau.serializer.SerializeException;
+import org.apache.juneau.sse.*;
+
+/**
+ * Pairs {@link SseSerializer} and {@link SseParser} into a single class with convenience
+ * read/write methods for the <c>text/event-stream</c> wire format.
+ *
+ * <h5 class='figure'>Examples:</h5>
+ * <p class='bjava'>
+ * <jc>// Serialize a list of events using the DEFAULT instance</jc>
+ * List<SseEvent> <jv>events</jv> = List.<jsm>of</jsm>(
+ * <jk>new</jk> SseEvent(<js>"progress"</js>, <js>"step 1"</js>),
+ * <jk>new</jk> SseEvent(<js>"progress"</js>, <js>"step 2"</js>)
+ * );
+ * String <jv>wire</jv> = Sse.<jsm>of</jsm>(<jv>events</jv>);
+ *
+ * <jc>// Parse a wire string back into a list of events</jc>
+ * List<SseEvent> <jv>parsed</jv> = Sse.<jsm>to</jsm>(<jv>wire</jv>, List.<jk>class</jk>, SseEvent.<jk>class</jk>);
+ * </p>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Marshallers">Marshallers</a>
+ * </ul>
+ */
+public class Sse extends CharMarshaller {
+
+ /**
+ * Default reusable instance.
+ */
+ public static final Sse DEFAULT = new Sse();
+
+ /**
+ * Serializes a Java object to an SSE wire string.
+ *
+ * @param object The object to serialize.
+ * @return The serialized output.
+ * @throws SerializeException If a problem occurred trying to convert the output.
+ */
+ public static String of(Object object) throws SerializeException {
+ return DEFAULT.write(object);
+ }
+
+ /**
+ * Serializes a Java object to an SSE output.
+ *
+ * @param object The object to serialize.
+ * @param output The output object.
+ * @return The output object.
+ * @throws SerializeException If a problem occurred trying to convert the output.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static Object of(Object object, Object output) throws SerializeException, IOException {
+ DEFAULT.write(object, output);
+ return output;
+ }
+
+ /**
+ * Parses an SSE input object to the specified Java type.
+ *
+ * @param <T> The class type of the object being created.
+ * @param input The input.
+ * @param type The object type to create.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by underlying stream.
+ */
+ public static <T> T to(Object input, Class<T> type) throws ParseException, IOException {
+ return DEFAULT.read(input, type);
+ }
+
+ /**
+ * Parses an SSE input object to the specified Java type.
+ *
+ * @param <T> The class type of the object to create.
+ * @param input The input.
+ * @param type The object type to create.
+ * @param args The type arguments of the class if it's a collection or map.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @throws IOException Thrown by underlying stream.
+ * @see MarshallingSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
+ */
+ public static <T> T to(Object input, Type type, Type...args) throws ParseException, IOException {
+ return DEFAULT.read(input, type, args);
+ }
+
+ /**
+ * Parses an SSE input string to the specified type.
+ *
+ * @param <T> The class type of the object being created.
+ * @param input The input.
+ * @param type The object type to create.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ */
+ public static <T> T to(String input, Class<T> type) throws ParseException {
+ return DEFAULT.read(input, type);
+ }
+
+ /**
+ * Parses an SSE input string to the specified Java type.
+ *
+ * @param <T> The class type of the object to create.
+ * @param input The input.
+ * @param type The object type to create.
+ * @param args The type arguments of the class if it's a collection or map.
+ * @return The parsed object.
+ * @throws ParseException Malformed input encountered.
+ * @see MarshallingSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
+ */
+ public static <T> T to(String input, Type type, Type...args) throws ParseException {
+ return DEFAULT.read(input, type, args);
+ }
+
+ /**
+ * Constructor.
+ */
+ public Sse() {
+ this(SseSerializer.DEFAULT, SseParser.DEFAULT);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param s The serializer.
+ * @param p The parser.
+ */
+ public Sse(SseSerializer s, SseParser p) {
+ super(s, p);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
index d010f1a721..a27bdc8c1d 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackParser.java
@@ -35,7 +35,12 @@ import org.apache.juneau.commons.bean.BeanPropertyMeta;
*
* <h5 class='topic'>Media types</h5>
*
- * Handles <c>Content-Type</c> types: <bc>octal/msgpack</bc>
+ * Handles <c>Content-Type</c> types: <bc>application/msgpack, octal/msgpack</bc>
+ *
+ * <p>
+ * The legacy media type <bc>octal/msgpack</bc> is retained as a backward-compatibility alias so
+ * that existing wire values still decode. New consumers should prefer the RFC-standard
+ * <bc>application/msgpack</bc> value.
*
* <h5 class='topic'>Description</h5>
* <p>
@@ -93,7 +98,7 @@ public class MsgPackParser extends InputStreamParser implements MsgPackMetaProvi
* Constructor, default settings.
*/
protected Builder() {
- consumes("octal/msgpack");
+ consumes("application/msgpack,octal/msgpack");
}
/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
index 801fd22061..14f2c99e8f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/msgpack/MsgPackSerializer.java
@@ -38,9 +38,14 @@ import org.apache.juneau.commons.bean.BeanPropertyMeta;
*
* <h5 class='section'>Media types:</h5>
*
- * Handles <c>Accept</c> types: <bc>octal/msgpack</bc>
+ * Handles <c>Accept</c> types: <bc>application/msgpack, octal/msgpack</bc>
* <p>
- * Produces <c>Content-Type</c> types: <bc>octal/msgpack</bc>
+ * Produces <c>Content-Type</c> types: <bc>application/msgpack</bc>
+ *
+ * <p>
+ * The legacy media type <bc>octal/msgpack</bc> is retained in the <c>Accept</c> list as a
+ * backward-compatibility alias. The <c>Content-Type</c> emitted by the serializer is always the
+ * RFC-standard <bc>application/msgpack</bc> value.
*
* <h5 class='topic'>Description</h5>
* <p>
@@ -107,7 +112,8 @@ public class MsgPackSerializer extends OutputStreamSerializer implements MsgPack
* Constructor, default settings.
*/
protected Builder() {
- produces("octal/msgpack");
+ produces("application/msgpack");
+ accept("application/msgpack,octal/msgpack");
addBeanTypesMsgPack = env("MsgPackSerializer.addBeanTypesMsgPack", false);
}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEvent.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEvent.java
new file mode 100644
index 0000000000..2cb01c705a
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEvent.java
@@ -0,0 +1,193 @@
+/*
+ * 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.sse;
+
+import java.util.*;
+
+import org.apache.juneau.commons.bean.*;
+
+/**
+ * Represents a single <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">Server-Sent Event</a>
+ * (SSE) frame on the wire.
+ *
+ * <p>
+ * Each event maps to a sequence of <c>field: value</c> lines terminated by a blank line in the
+ * <c>text/event-stream</c> wire format:
+ * <p class='bcode'>
+ * event: progress
+ * data: {"step":4,"total":10}
+ * id: 42
+ * retry: 5000
+ *
+ * </p>
+ *
+ * <p>
+ * The payload field {@link #getData() data} is a <b>pre-serialized</b> {@link String} — the SSE
+ * serializer does not compose with a delegate marshaller. Callers that need a typed payload should
+ * serialize it themselves (e.g. via {@link org.apache.juneau.marshaller.Json#of(Object) Json.of(x)})
+ * and assign the resulting string to {@link #setData(String) setData(...)}. The serializer will
+ * split that string on {@code \n} into multiple <c>data:</c> lines per spec.
+ *
+ * <h5 class='figure'>Example: build and serialize an event:</h5>
+ * <p class='bjava'>
+ * SseEvent <jv>event</jv> = <jk>new</jk> SseEvent()
+ * .setEvent(<js>"progress"</js>)
+ * .setData(Json.<jsm>of</jsm>(Map.of(<js>"step"</js>, 4, <js>"total"</js>, 10)))
+ * .setId(<js>"42"</js>)
+ * .setRetry(5000L);
+ *
+ * String <jv>wire</jv> = Sse.<jsm>of</jsm>(<jv>event</jv>);
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is not thread safe.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * <li class='link'><a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG HTML §9.2 — Server-sent events</a>
+ * </ul>
+ */
+@BeanType(properties = "event,data,id,retry")
+public class SseEvent {
+
+ /** Default event-type name dispatched by the WHATWG <c>EventSource</c> when no <c>event:</c> field is present. */
+ public static final String DEFAULT_EVENT = "message";
+
+ private String event;
+ private String data;
+ private String id;
+ private Long retry;
+
+ /**
+ * Constructor.
+ */
+ public SseEvent() {}
+
+ /**
+ * Constructor.
+ *
+ * @param event The event-type name. Can be <jk>null</jk> (dispatches as {@value #DEFAULT_EVENT}).
+ * @param data The payload string. Can be <jk>null</jk> (no <c>data:</c> lines emitted).
+ */
+ public SseEvent(String event, String data) {
+ this.event = event;
+ this.data = data;
+ }
+
+ /**
+ * Returns the event-type name.
+ *
+ * @return The event-type name, or <jk>null</jk> if not set.
+ */
+ public String getEvent() {
+ return event;
+ }
+
+ /**
+ * Sets the event-type name.
+ *
+ * @param value The event-type name. Can be <jk>null</jk> to unset.
+ * @return This object.
+ */
+ public SseEvent setEvent(String value) {
+ event = value;
+ return this;
+ }
+
+ /**
+ * Returns the data payload (pre-serialized string).
+ *
+ * @return The data payload, or <jk>null</jk> if not set.
+ */
+ public String getData() {
+ return data;
+ }
+
+ /**
+ * Sets the data payload.
+ *
+ * @param value The pre-serialized payload string. Can be <jk>null</jk> to unset.
+ * @return This object.
+ */
+ public SseEvent setData(String value) {
+ data = value;
+ return this;
+ }
+
+ /**
+ * Returns the last-event-id value.
+ *
+ * @return The id, or <jk>null</jk> if not set.
+ */
+ public String getId() {
+ return id;
+ }
+
+ /**
+ * Sets the last-event-id value.
+ *
+ * @param value The id. Can be <jk>null</jk> to unset.
+ * @return This object.
+ */
+ public SseEvent setId(String value) {
+ id = value;
+ return this;
+ }
+
+ /**
+ * Returns the reconnect-delay hint in milliseconds.
+ *
+ * @return The retry value, or <jk>null</jk> if not set.
+ */
+ public Long getRetry() {
+ return retry;
+ }
+
+ /**
+ * Sets the reconnect-delay hint in milliseconds.
+ *
+ * @param value The retry value. Can be <jk>null</jk> to unset.
+ * @return This object.
+ */
+ public SseEvent setRetry(Long value) {
+ retry = value;
+ return this;
+ }
+
+ @Override /* Overridden from Object */
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (!(o instanceof SseEvent o2))
+ return false;
+ return Objects.equals(event, o2.event)
+ && Objects.equals(data, o2.data)
+ && Objects.equals(id, o2.id)
+ && Objects.equals(retry, o2.retry);
+ }
+
+ @Override /* Overridden from Object */
+ public int hashCode() {
+ return Objects.hash(event, data, id, retry);
+ }
+
+ @Override /* Overridden from Object */
+ public String toString() {
+ return "SseEvent[event=" + event + ",data=" + data + ",id=" + id + ",retry=" + retry + "]";
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEventReader.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEventReader.java
new file mode 100644
index 0000000000..8e606eff49
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseEventReader.java
@@ -0,0 +1,270 @@
+/*
+ * 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.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Line-driven {@link Iterator} over the events in a <c>text/event-stream</c>.
+ *
+ * <p>
+ * Wraps a {@link Reader} the caller owns and dispatches one {@link SseEvent} at a time using the
+ * WHATWG parse state machine (<a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">HTML §9.2</a>).
+ * Designed for long-lived SSE streams that outlive any reasonable per-call buffer.
+ *
+ * <h5 class='section'>Why a separate class:</h5>
+ * <p>
+ * No other Juneau marshaller ships a separate iterator-style helper. SSE warrants the deviation
+ * because SSE responses are open until the client disconnects or the server pushes its last event;
+ * forcing callers through the eager {@code List<SseEvent>} path would defeat the entire point of
+ * using SSE.
+ *
+ * <h5 class='figure'>Example:</h5>
+ * <p class='bjava'>
+ * <jk>try</jk> (SseEventReader <jv>r</jv> = <jk>new</jk> SseEventReader(<jv>responseReader</jv>)) {
+ * <jk>while</jk> (<jv>r</jv>.hasNext()) {
+ * SseEvent <jv>e</jv> = <jv>r</jv>.next();
+ * <jc>// Dispatch...</jc>
+ * }
+ * }
+ * </p>
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S6541" // Brain method acceptable for readLine / nextEvent — they encode the SSE lexical state machine
+})
+public class SseEventReader implements Iterator<SseEvent>, Closeable {
+
+ @SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+ private static final String ARG_in = "in";
+
+ /** UTF-8 BOM character that must be stripped exactly once from the start of an SSE stream. */
+ private static final int BOM = 0xFEFF;
+
+ private final Reader in;
+
+ private boolean bomChecked;
+ private boolean eof;
+ private SseEvent peeked;
+
+ private final StringBuilder dataBuf = new StringBuilder();
+ private String eventName;
+ private String lastEventId;
+ private Long retryValue;
+ private boolean haveAnyField;
+
+ private int pendingChar = -1;
+
+ /**
+ * Constructor.
+ *
+ * @param in The reader to consume events from. Must not be <jk>null</jk>. Caller retains ownership;
+ * {@link #close()} closes this reader.
+ */
+ @SuppressWarnings({ "java:S2095", "resource" }) // Reader ownership is transferred to this Closeable and released by close().
+ public SseEventReader(Reader in) {
+ this.in = assertArgNotNull(ARG_in, in);
+ }
+
+ @Override /* Overridden from Iterator */
+ public boolean hasNext() {
+ if (peeked != null)
+ return true;
+ if (eof)
+ return false;
+ try {
+ peeked = nextEvent();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return peeked != null;
+ }
+
+ @Override /* Overridden from Iterator */
+ public SseEvent next() {
+ if (!hasNext())
+ throw new NoSuchElementException();
+ var out = peeked;
+ peeked = null;
+ return out;
+ }
+
+ @Override /* Overridden from Closeable */
+ public void close() throws IOException {
+ in.close();
+ }
+
+ /**
+ * Drains the remaining events into a list, then closes this reader.
+ *
+ * @return The list of events in order.
+ * @throws IOException If an I/O error occurs while reading.
+ */
+ public List<SseEvent> toList() throws IOException {
+ var out = new ArrayList<SseEvent>();
+ try {
+ while (hasNext())
+ out.add(next());
+ } catch (UncheckedIOException e) {
+ throw e.getCause();
+ } finally {
+ close();
+ }
+ return out;
+ }
+
+ private SseEvent nextEvent() throws IOException {
+ while (true) {
+ var line = readLine();
+ if (line == null) {
+ eof = true;
+ if (haveAnyField)
+ return dispatch();
+ return null;
+ }
+ if (line.isEmpty()) {
+ if (haveAnyField)
+ return dispatch();
+ continue;
+ }
+ processField(line);
+ }
+ }
+
+ private SseEvent dispatch() {
+ var out = new SseEvent();
+ out.setEvent(eventName);
+ if (!dataBuf.isEmpty()) {
+ if (dataBuf.charAt(dataBuf.length() - 1) == '\n')
+ dataBuf.deleteCharAt(dataBuf.length() - 1);
+ out.setData(dataBuf.toString());
+ }
+ out.setId(lastEventId);
+ out.setRetry(retryValue);
+ resetEventBuffers();
+ return out;
+ }
+
+ private void resetEventBuffers() {
+ dataBuf.setLength(0);
+ eventName = null;
+ lastEventId = null;
+ retryValue = null;
+ haveAnyField = false;
+ }
+
+ private void processField(String line) {
+ if (line.charAt(0) == ':') {
+ haveAnyField = true;
+ return;
+ }
+ String fieldName;
+ String fieldValue;
+ var colon = line.indexOf(':');
+ if (colon < 0) {
+ fieldName = line;
+ fieldValue = "";
+ } else {
+ fieldName = line.substring(0, colon);
+ fieldValue = line.substring(colon + 1);
+ if (!fieldValue.isEmpty() && fieldValue.charAt(0) == ' ')
+ fieldValue = fieldValue.substring(1);
+ }
+ haveAnyField = true;
+ applyField(fieldName, fieldValue);
+ }
+
+ private void applyField(String fieldName, String fieldValue) {
+ switch (fieldName) {
+ case "event":
+ eventName = fieldValue;
+ break;
+ case "data":
+ dataBuf.append(fieldValue).append('\n');
+ break;
+ case "id":
+ if (fieldValue.indexOf('\u0000') < 0)
+ lastEventId = fieldValue;
+ break;
+ case "retry":
+ retryValue = parseRetry(fieldValue);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private static Long parseRetry(String s) {
+ if (s.isEmpty())
+ return null;
+ for (var i = 0; i < s.length(); i++) {
+ var c = s.charAt(i);
+ if (c < '0' || c > '9')
+ return null;
+ }
+ try {
+ return Long.valueOf(s);
+ } catch (@SuppressWarnings("unused") NumberFormatException e) {
+ return null;
+ }
+ }
+
+ @SuppressWarnings("java:S3776") // SSE line parsing is a deliberate lexical state machine.
+ private String readLine() throws IOException {
+ var sb = new StringBuilder();
+ int c;
+ var sawAny = false;
+ while (true) {
+ if (pendingChar != -1) {
+ c = pendingChar;
+ pendingChar = -1;
+ } else {
+ c = in.read();
+ }
+ if (c == -1) {
+ if (!sawAny)
+ return null;
+ return sb.toString();
+ }
+ if (!bomChecked) {
+ bomChecked = true;
+ if (c == BOM)
+ continue;
+ }
+ sawAny = true;
+ if (c == '\n')
+ return sb.toString();
+ if (c == '\r') {
+ var next = in.read();
+ if (next != -1 && next != '\n')
+ pendingChar = next;
+ return sb.toString();
+ }
+ sb.append((char) c);
+ }
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParser.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParser.java
new file mode 100644
index 0000000000..7f3c28d858
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParser.java
@@ -0,0 +1,133 @@
+/*
+ * 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.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Parses <c>text/event-stream</c> input (WHATWG Server-Sent Events) into Java objects.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Handles <c>Content-Type</c> types: <bc>text/event-stream</bc>.
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * Eager parse path — drains the stream into a {@link java.util.List List<SseEvent>}.
+ * For true line-driven streaming over long-lived connections, use {@link SseEventReader} directly.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is thread safe and reusable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110" // Inheritance depth acceptable for parser hierarchy
+})
+public class SseParser extends ReaderParser {
+
+ /** Default parser, all default settings. */
+ public static final SseParser DEFAULT = new SseParser(create());
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends ReaderParser.Builder {
+
+ private static final Cache<HashKey,SseParser> CACHE = Cache.of(HashKey.class, SseParser.class).build();
+
+ /**
+ * Constructor, default settings.
+ */
+ protected Builder() {
+ consumes(SseSerializer.MEDIA_TYPE);
+ type(SseParser.class);
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The builder to copy from. Must not be <jk>null</jk>.
+ */
+ protected Builder(Builder copyFrom) {
+ super(assertArgNotNull("copyFrom", copyFrom));
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The parser to copy from. Must not be <jk>null</jk>.
+ */
+ protected Builder(SseParser copyFrom) {
+ super(assertArgNotNull("copyFrom", copyFrom));
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public HashKey hashKey() {
+ return HashKey.of(super.hashKey());
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public SseParser build() {
+ return cache(CACHE).build(SseParser.class);
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public Builder copy() {
+ return new Builder(this);
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public SseParser(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from Context */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Context */
+ public SseParserSession.Builder createSession() {
+ return SseParserSession.create(this);
+ }
+
+ @Override /* Overridden from Context */
+ public SseParserSession getSession() {
+ return createSession().build();
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParserSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParserSession.java
new file mode 100644
index 0000000000..be0e30cc1e
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseParserSession.java
@@ -0,0 +1,108 @@
+/*
+ * 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.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link SseParser}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110", // Inheritance depth acceptable for session hierarchy
+ "unchecked" // Type erasure: SseEvent/List<SseEvent> returned via Object cast
+})
+public class SseParserSession extends ReaderParserSession {
+
+ @SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+ private static final String ARG_ctx = "ctx";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends ReaderParserSession.Builder {
+
+ /**
+ * Constructor.
+ *
+ * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+ */
+ protected Builder(SseParser ctx) {
+ super(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ @Override
+ public SseParserSession build() {
+ return new SseParserSession(this);
+ }
+ }
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+ * @return A new builder.
+ */
+ public static Builder create(SseParser ctx) {
+ return new Builder(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected SseParserSession(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from ParserSession */
+ @SuppressWarnings({ "java:S2095", "resource" }) // Reader is owned by ParserPipe and closed via SseEventReader in try-with-resources.
+ protected <T> T doParse(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException {
+ var r = pipe.getReader();
+ if (r == null)
+ return null;
+ List<SseEvent> events;
+ try (var r2 = new SseEventReader(r)) {
+ events = r2.toList();
+ }
+ if (type == null || type.isObject() || type.isCollectionOrArray()) {
+ if (type != null && type.isArray())
+ return (T) events.toArray(new SseEvent[0]);
+ return (T) events;
+ }
+ if (SseEvent.class.equals(type.inner())) {
+ return events.isEmpty() ? null : (T) events.get(0);
+ }
+ if (List.class.isAssignableFrom(type.inner()))
+ return (T) events;
+ throw new ParseException(this, "SseParser cannot produce target type ''{0}''.", type);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializer.java
new file mode 100644
index 0000000000..44c742dedd
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializer.java
@@ -0,0 +1,172 @@
+/*
+ * 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.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+
+import org.apache.juneau.commons.collections.*;
+import org.apache.juneau.serializer.*;
+
+/**
+ * Serializes {@link SseEvent} objects to the <c>text/event-stream</c> wire format defined by the
+ * <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG Server-Sent Events spec</a>.
+ *
+ * <h5 class='topic'>Media types</h5>
+ * <p>
+ * Produces <c>Content-Type</c> types: <bc>text/event-stream</bc>.
+ *
+ * <h5 class='topic'>Description</h5>
+ * <p>
+ * Accepts any of the following as input and emits one wire event per element followed by a blank
+ * line (the spec's "dispatch" boundary):
+ * <ul class='spaced-list'>
+ * <li>A single {@link SseEvent}.
+ * <li>{@link Iterable Iterable<SseEvent>}.
+ * <li>An array {@code SseEvent[]}.
+ * <li>{@link java.util.stream.Stream Stream<SseEvent>}.
+ * </ul>
+ *
+ * <p>
+ * After each event is written, the session calls {@link Writer#flush() Writer.flush()} so that downstream
+ * REST infrastructure flushes the underlying TCP socket and the event is delivered immediately to the
+ * client.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='note'>This class is thread safe and reusable.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110" // Inheritance depth acceptable for serializer hierarchy
+})
+public class SseSerializer extends WriterSerializer {
+
+ @SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+ private static final String ARG_w = "w";
+
+ /**
+ * Wire-format media type produced by this serializer: <c>text/event-stream</c>.
+ */
+ public static final String MEDIA_TYPE = "text/event-stream";
+
+ /** Default serializer, all default settings. */
+ public static final SseSerializer DEFAULT = new SseSerializer(create());
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @return A new builder.
+ */
+ public static Builder create() {
+ return new Builder();
+ }
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends WriterSerializer.Builder {
+
+ private static final Cache<HashKey,SseSerializer> CACHE = Cache.of(HashKey.class, SseSerializer.class).build();
+
+ /**
+ * Constructor, default settings.
+ */
+ protected Builder() {
+ produces(MEDIA_TYPE);
+ type(SseSerializer.class);
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The builder to copy from. Must not be <jk>null</jk>.
+ */
+ protected Builder(Builder copyFrom) {
+ super(assertArgNotNull("copyFrom", copyFrom));
+ }
+
+ /**
+ * Copy constructor.
+ *
+ * @param copyFrom The serializer to copy from. Must not be <jk>null</jk>.
+ */
+ protected Builder(SseSerializer copyFrom) {
+ super(assertArgNotNull("copyFrom", copyFrom));
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public HashKey hashKey() {
+ return HashKey.of(super.hashKey());
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public SseSerializer build() {
+ return cache(CACHE).build(SseSerializer.class);
+ }
+
+ @Override /* Overridden from Context.Builder */
+ public Builder copy() {
+ return new Builder(this);
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ public SseSerializer(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from Context */
+ public Builder copy() {
+ return new Builder(this);
+ }
+
+ @Override /* Overridden from Context */
+ public SseSerializerSession.Builder createSession() {
+ return SseSerializerSession.create(this);
+ }
+
+ @Override /* Overridden from Context */
+ public SseSerializerSession getSession() {
+ return createSession().build();
+ }
+
+ /**
+ * Writes a comment line (heartbeat) to the supplied writer in the SSE wire format and flushes it.
+ *
+ * @param w The writer. Must not be <jk>null</jk>.
+ * @param comment The comment text. {@code null} is treated as an empty string.
+ * @throws IOException If a problem occurred writing to the underlying stream.
+ */
+ @SuppressWarnings({ "java:S2095", "resource" }) // Writer ownership is the caller's; we only write/flush.
+ public static void writeComment(Writer w, String comment) throws IOException {
+ assertArgNotNull(ARG_w, w);
+ var c = comment == null ? "" : comment;
+ for (var line : c.split("\n", -1))
+ w.write(": " + line + "\n");
+ w.write("\n");
+ w.flush();
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializerSession.java
new file mode 100644
index 0000000000..898695e7b7
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/SseSerializerSession.java
@@ -0,0 +1,158 @@
+/*
+ * 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.sse;
+
+import static org.apache.juneau.commons.utils.AssertionUtils.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.serializer.*;
+
+/**
+ * Session object that lives for the duration of a single use of {@link SseSerializer}.
+ *
+ * <h5 class='section'>Notes:</h5><ul>
+ * <li class='warn'>This class is not thread safe and is typically discarded after one use.
+ * </ul>
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+@SuppressWarnings({
+ "java:S110" // Inheritance depth acceptable for session hierarchy
+})
+public class SseSerializerSession extends WriterSerializerSession {
+
+ @SuppressWarnings("java:S115") // Match AssertionUtils arg-name style used throughout Juneau.
+ private static final String ARG_ctx = "ctx";
+
+ /**
+ * Builder class.
+ */
+ public static class Builder extends WriterSerializerSession.Builder {
+
+ /**
+ * Constructor.
+ *
+ * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+ */
+ protected Builder(SseSerializer ctx) {
+ super(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ @Override
+ public SseSerializerSession build() {
+ return new SseSerializerSession(this);
+ }
+ }
+
+ /**
+ * Creates a new builder for this object.
+ *
+ * @param ctx The context creating this session. Cannot be <jk>null</jk>.
+ * @return A new builder.
+ */
+ public static Builder create(SseSerializer ctx) {
+ return new Builder(assertArgNotNull(ARG_ctx, ctx));
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param builder The builder for this object.
+ */
+ protected SseSerializerSession(Builder builder) {
+ super(builder);
+ }
+
+ @Override /* Overridden from SerializerSession */
+ @SuppressWarnings({ "java:S2095", "resource" }) // Writer is owned by SerializerPipe/caller and must not be closed by this session.
+ protected void doSerialize(SerializerPipe pipe, Object o) throws IOException, SerializeException {
+ var w = pipe.getWriter();
+ if (o == null)
+ return;
+ if (o instanceof SseEvent e) {
+ writeEvent(w, e);
+ } else if (o instanceof Iterable<?> i) {
+ for (var item : i)
+ writeIfEvent(w, item);
+ } else if (o instanceof Stream<?> s) {
+ try (var s2 = s) {
+ s2.forEach(item -> writeIfEventUnchecked(w, item));
+ }
+ } else if (o instanceof SseEvent[] arr) {
+ for (var e : arr)
+ writeEvent(w, e);
+ } else if (o.getClass().isArray()) {
+ var arr = (Object[]) o;
+ for (var item : arr)
+ writeIfEvent(w, item);
+ } else {
+ throw new SerializeException("SseSerializer cannot serialize objects of type ''{0}''.", o.getClass().getName());
+ }
+ }
+
+ private static void writeIfEvent(Writer w, Object item) throws IOException, SerializeException {
+ if (item == null)
+ return;
+ if (!(item instanceof SseEvent e))
+ throw new SerializeException("SseSerializer encountered non-SseEvent element of type ''{0}''.", item.getClass().getName());
+ writeEvent(w, e);
+ }
+
+ private static void writeIfEventUnchecked(Writer w, Object item) {
+ try {
+ writeIfEvent(w, item);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ } catch (SerializeException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private static void writeEvent(Writer w, SseEvent e) throws IOException {
+ if (e == null)
+ return;
+ if (e.getEvent() != null && !e.getEvent().isEmpty())
+ writeField(w, "event", e.getEvent());
+ if (e.getData() != null) {
+ for (var line : splitData(e.getData()))
+ writeField(w, "data", line);
+ }
+ if (e.getId() != null)
+ writeField(w, "id", e.getId());
+ if (e.getRetry() != null)
+ w.write("retry: " + e.getRetry() + "\n");
+ w.write("\n");
+ w.flush();
+ }
+
+ private static void writeField(Writer w, String name, String value) throws IOException {
+ w.write(name);
+ w.write(": ");
+ w.write(value);
+ w.write('\n');
+ }
+
+ static List<String> splitData(String s) {
+ var parts = s.split("\n", -1);
+ return Arrays.asList(parts);
+ }
+}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/package-info.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/package-info.java
new file mode 100644
index 0000000000..6f493d86c2
--- /dev/null
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/sse/package-info.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+/**
+ * Server-Sent Events (SSE) marshalling support — {@code text/event-stream}.
+ *
+ * <p>
+ * Provides {@link org.apache.juneau.sse.SseSerializer} and {@link org.apache.juneau.sse.SseParser}
+ * for the
+ * <a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG Server-Sent Events</a>
+ * wire format. The serializer accepts a single {@link org.apache.juneau.sse.SseEvent}, an
+ * {@link java.lang.Iterable Iterable}, a {@link java.util.stream.Stream Stream}, or an
+ * {@code SseEvent[]} array and emits one event per element with a {@link java.io.Writer#flush()
+ * flush()} after each one — that is what makes server push actually work behind
+ * {@code SerializedPojoProcessor}.
+ *
+ * <p>
+ * SSE streams are typically long-lived. For true line-driven streaming over a {@link java.io.Reader}
+ * that the caller owns, use the {@link org.apache.juneau.sse.SseEventReader} iterator.
+ *
+ * <h5 class='section'>See Also:</h5><ul>
+ * <li class='link'><a class="doclink" href="https://html.spec.whatwg.org/multipage/server-sent-events.html">WHATWG HTML §9.2 — Server-sent events</a>
+ * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SseBasics">SSE Basics</a>
+ * </ul>
+ */
+package org.apache.juneau.sse;
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
index b86b2fa876..a3836b161f 100644
--- a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/RootResources.java
@@ -38,6 +38,7 @@ import org.apache.juneau.serializer.annotation.*;
DtoExamples.class,
UtilityBeansResource.class,
HtmlBeansResource.class,
+ SseDemoResource.class,
ConfigResource.class,
LogsResource.class,
ShutdownResource.class
diff --git a/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/SseDemoResource.java b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/SseDemoResource.java
new file mode 100644
index 0000000000..abca46bada
--- /dev/null
+++ b/juneau-examples/juneau-examples-rest/src/main/java/org/apache/juneau/examples/rest/SseDemoResource.java
@@ -0,0 +1,92 @@
+/*
+ * 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.examples.rest;
+
+import java.time.*;
+import java.time.format.*;
+import java.util.*;
+
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.servlet.*;
+import org.apache.juneau.sse.*;
+
+/**
+ * Sample REST resource that emits a stream of Server-Sent Events.
+ *
+ * <p>
+ * The {@link #stream()} endpoint returns a lazy {@link Iterable} of {@link SseEvent} objects whose
+ * iterator sleeps {@value #SLEEP_MILLIS} milliseconds between calls to {@code next()}. Combined with the
+ * per-event {@code Writer.flush()} performed by {@link SseSerializer}, this allows the per-event
+ * arrival timing to be observed with a streaming HTTP client such as {@code curl -N}.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ * $ curl -N -H 'Accept: text/event-stream' http://localhost:10000/sseDemo/stream
+ * event: tick-1
+ * data: 2026-05-20T11:08:00.123Z
+ *
+ * event: tick-2
+ * data: 2026-05-20T11:08:00.624Z
+ * ...
+ * </p>
+ */
+@Rest(
+ path = "/sseDemo",
+ title = "SSE demo",
+ description = "Emits a stream of Server-Sent Events with a short delay between events."
+)
+public class SseDemoResource extends BasicRestObject {
+
+ private static final int EVENT_COUNT = 10;
+ private static final long SLEEP_MILLIS = 500L;
+
+ /**
+ * Streams {@value #EVENT_COUNT} SSE events with a {@value #SLEEP_MILLIS}-ms delay between
+ * successive events. Each event carries a UTC timestamp in its {@code data:} field so that
+ * per-event flush behavior is observable from the client's wall clock.
+ *
+ * @return A lazy iterable of {@link SseEvent} objects.
+ */
+ @RestGet(path = "/stream", serializers = SseSerializer.class)
+ public Iterable<SseEvent> stream() {
+ return () -> new Iterator<>() {
+ private int i = 0;
+
+ @Override
+ public boolean hasNext() {
+ return i < EVENT_COUNT;
+ }
+
+ @Override
+ public SseEvent next() {
+ if (i >= EVENT_COUNT)
+ throw new NoSuchElementException();
+ if (i > 0) {
+ try {
+ Thread.sleep(SLEEP_MILLIS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException("Interrupted while sleeping between SSE events", e);
+ }
+ }
+ i++;
+ var ts = DateTimeFormatter.ISO_INSTANT.format(Instant.now());
+ return new SseEvent("tick-" + i, ts).setId(String.valueOf(i));
+ }
+ };
+ }
+}
diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
index 95ef320b5c..3be0721db8 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestClient.java
@@ -3703,10 +3703,10 @@ public class RestClient extends MarshallingContextable implements HttpClient, Cl
* bean context property setters (e.g. {@link #swaps(Class...)}) defined on this builder class.
* </ul>
* <p>
- * <c>Accept</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+ * <c>Accept</c> request header will be set to <js>"application/msgpack"</js> unless overridden
* via {@link #headers()}, or per-request via {@link RestRequest#header(Header)}.
* <p>
- * <c>Content-Type</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+ * <c>Content-Type</c> request header will be set to <js>"application/msgpack"</js> unless overridden
* via {@link #headers()}, or per-request via {@link RestRequest#header(Header)}.
* <p>
* Can be combined with other marshaller setters such as {@link #json()} to provide support for multiple languages.
@@ -6582,7 +6582,7 @@ public class RestClient extends MarshallingContextable implements HttpClient, Cl
try {
var req = request(method, uri, ne(content));
if (nn(headers))
- Json5Map.ofText(headers).forEach((k, v) -> req.header(stringHeader(k, s(v))));
+ Json5Map.ofString(headers).forEach((k, v) -> req.header(stringHeader(k, s(v))));
if (ne(content))
req.contentString(content);
return req;
diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java
index faa9cab2c5..d9c85f739b 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestRequest.java
@@ -1445,10 +1445,10 @@ public class RestRequest extends MarshallingSession implements HttpUriRequest, C
* bean context property setters (e.g. {@link RestClient.Builder#swaps(Class...)}) defined on this builder class.
* </ul>
* <p>
- * <c>Accept</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+ * <c>Accept</c> request header will be set to <js>"application/msgpack"</js> unless overridden
* by {@link RestRequest#header(String,Object)} or {@link RestRequest#accept(String)}.
* <p>
- * <c>Content-Type</c> request header will be set to <js>"octal/msgpack"</js> unless overridden
+ * <c>Content-Type</c> request header will be set to <js>"application/msgpack"</js> unless overridden
* by {@link RestRequest#header(String,Object)} or {@link RestRequest#contentType(String)}.
* <p>
* Identical to calling <c>serializer(MsgPackSerializer.<jk>class</jk>).parser(MsgPackParser.<jk>class</jk>)</c>.
diff --git a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java
index 4a368915cc..5e7207ea23 100644
--- a/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java
+++ b/juneau-rest/juneau-rest-client-classic/src/main/java/org/apache/juneau/rest/client/classic/RestResponse.java
@@ -401,36 +401,49 @@ public class RestResponse implements HttpResponse, AutoCloseable {
isClosed = true;
try {
- EntityUtils.consumeQuietly(response.getEntity());
-
- if (! request.isLoggingSuppressed() && (request.isDebug() || client.logRequestsPredicate.test(request, this))) {
- if (client.logRequests == DetailLevel.SIMPLE) {
- client.log(client.logRequestsLevel, "HTTP {0} {1}, {2}", request.getMethod(), request.getURI(), this.getStatusLine());
- } else if (request.isDebug() || client.logRequests == DetailLevel.FULL) {
- var output = getContent().asString();
- var sb = new StringBuilder();
- sb.append("\n=== HTTP Call (outgoing) ======================================================");
- sb.append("\n=== REQUEST ===\n");
- sb.append(request.getMethod()).append(" ").append(request.getURI());
- sb.append("\n---request headers---");
- request.getHeaders().forEach(x -> sb.append("\n\t").append(x));
- if (request.hasHttpEntity()) {
- sb.append("\n---request entity---");
- var e = request.getHttpEntity();
- if (nn(e.getContentType()))
- sb.append("\n\t").append(e.getContentType());
- if (e.isRepeatable()) {
- appendRequestContent(sb, e);
+ // Render FULL/SIMPLE request logging BEFORE consuming the underlying entity.
+ //
+ // FULL logging reads the response body via getContent().asString(), which reads
+ // from the same HttpEntity returned by response.getEntity(). If consumeQuietly()
+ // runs first it closes the underlying network input stream, leaving the entity in
+ // a state where any subsequent read throws "Stream closed". Running logging first
+ // lets it cache the body bytes into ResponseContent.body so both this log render
+ // and any later caller (e.g. user-level getContent().asString()) succeed.
+ //
+ // EntityUtils.consumeQuietly() is invoked in the finally block so the entity is
+ // still drained on the non-logged and exceptional paths.
+ try {
+ if (! request.isLoggingSuppressed() && (request.isDebug() || client.logRequestsPredicate.test(request, this))) {
+ if (client.logRequests == DetailLevel.SIMPLE) {
+ client.log(client.logRequestsLevel, "HTTP {0} {1}, {2}", request.getMethod(), request.getURI(), this.getStatusLine());
+ } else if (request.isDebug() || client.logRequests == DetailLevel.FULL) {
+ var output = getContent().asString();
+ var sb = new StringBuilder();
+ sb.append("\n=== HTTP Call (outgoing) ======================================================");
+ sb.append("\n=== REQUEST ===\n");
+ sb.append(request.getMethod()).append(" ").append(request.getURI());
+ sb.append("\n---request headers---");
+ request.getHeaders().forEach(x -> sb.append("\n\t").append(x));
+ if (request.hasHttpEntity()) {
+ sb.append("\n---request entity---");
+ var e = request.getHttpEntity();
+ if (nn(e.getContentType()))
+ sb.append("\n\t").append(e.getContentType());
+ if (e.isRepeatable()) {
+ appendRequestContent(sb, e);
+ }
}
+ sb.append("\n=== RESPONSE ===\n").append(getStatusLine());
+ sb.append("\n---response headers---");
+ for (var h : getAllHeaders())
+ sb.append("\n\t").append(h);
+ sb.append("\n---response content---\n").append(output);
+ sb.append("\n=== END =======================================================================");
+ client.log(client.logRequestsLevel, sb.toString());
}
- sb.append("\n=== RESPONSE ===\n").append(getStatusLine());
- sb.append("\n---response headers---");
- for (var h : getAllHeaders())
- sb.append("\n\t").append(h);
- sb.append("\n---response content---\n").append(output);
- sb.append("\n=== END =======================================================================");
- client.log(client.logRequestsLevel, sb.toString());
}
+ } finally {
+ EntityUtils.consumeQuietly(response.getEntity());
}
for (var r : request.interceptors) {
diff --git a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java
index 852c6d1101..e3fc9ca0b4 100644
--- a/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java
+++ b/juneau-rest/juneau-rest-common/src/main/java/org/apache/juneau/http/response/BasicHttpException.java
@@ -47,6 +47,7 @@ import org.apache.juneau.http.header.*;
*
* @since 9.2.1
*/
+@SuppressWarnings({"java:S3008", "java:S1948", "java:S1165"})
public class BasicHttpException extends RuntimeException implements HttpResponseMessage {
private static final long serialVersionUID = 1L;
diff --git a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java
index 3b95bb36ad..ce2a283c14 100644
--- a/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java
+++ b/juneau-rest/juneau-rest-mock/src/main/java/org/apache/juneau/rest/mock/classic/MockConsole.java
@@ -87,7 +87,7 @@ import org.apache.juneau.assertions.*;
})
public class MockConsole extends PrintStream {
- private static final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ private final ByteArrayOutputStream baos;
/**
* Creator.
@@ -102,7 +102,12 @@ public class MockConsole extends PrintStream {
* Constructor.
*/
public MockConsole() {
+ this(new ByteArrayOutputStream());
+ }
+
+ private MockConsole(ByteArrayOutputStream baos) {
super(baos);
+ this.baos = baos;
}
/**
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
index 6d03fee97d..2ba55b9197 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/swagger/BasicSwaggerProviderSession.java
@@ -1025,7 +1025,7 @@ public class BasicSwaggerProviderSession {
s = resolve(s);
if (! isProbablyJsonArray(s, true))
s = "[" + s + "]";
- return Json5List.ofText(s);
+ return Json5List.ofString(s);
} catch (ParseException e) {
throw new SwaggerException(e, "Malformed swagger JSON array encountered in " + location + ".", locationArgs);
}
@@ -1058,7 +1058,7 @@ public class BasicSwaggerProviderSession {
return Json5Map.of(SWAGGER_ignore, true);
if (! isProbablyJsonObject(o2, true))
o2 = "{" + o2 + "}";
- return Json5Map.ofText(o2);
+ return Json5Map.ofString(o2);
}
if (o instanceof Json5Map o2)
return o2;
diff --git a/juneau-utest/pom.xml b/juneau-utest/pom.xml
index 7af9bd1459..91d7ae1ba4 100644
--- a/juneau-utest/pom.xml
+++ b/juneau-utest/pom.xml
@@ -65,11 +65,26 @@
<artifactId>juneau-bean-common</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean-hal</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-bean-html5</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean-jsonapi</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean-jsonpatch</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-bean-jsonschema</artifactId>
@@ -90,6 +105,11 @@
<artifactId>juneau-bean-mcp</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-bean-rfc7807</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.juneau</groupId>
<artifactId>juneau-rest-mock</artifactId>
diff --git a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
index 8599808d2c..0a69fcb4ed 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/BeanMap_Test.java
@@ -223,11 +223,11 @@ class BeanMap_Test extends TestBase {
var m = bc.toBeanMap(t);
// Non-initialized list fields.
- m.put("l1", JsonList.ofText("[1,2,3]"));
- m.put("al1", JsonList.ofText("[1,2,3]"));
- m.put("ll1", JsonList.ofText("[1,2,3]"));
- m.put("c1", JsonList.ofText("[1,2,3]"));
- m.put("jl1", JsonList.ofText("[1,2,3]"));
+ m.put("l1", JsonList.ofString("[1,2,3]"));
+ m.put("al1", JsonList.ofString("[1,2,3]"));
+ m.put("ll1", JsonList.ofString("[1,2,3]"));
+ m.put("c1", JsonList.ofString("[1,2,3]"));
+ m.put("jl1", JsonList.ofString("[1,2,3]"));
// al1 should be initialized with an ArrayList, since it's not a superclass of JsonList.
// The rest are proper superclasses of JsonList.
@@ -238,10 +238,10 @@ class BeanMap_Test extends TestBase {
);
// Non-initialized map fields.
- m.put("m1", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("hm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("jm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("tm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
+ m.put("m1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("hm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("jm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("tm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
// tm1 should be initialized with TreeMap, since it's not a superclass of JsonMap.
// The rest are proper superclasses of JsonMap
@@ -252,15 +252,15 @@ class BeanMap_Test extends TestBase {
);
// Initialized fields should reuse existing field value.
- m.put("l2", JsonList.ofText("[1,2,3]"));
- m.put("al2", JsonList.ofText("[1,2,3]"));
- m.put("ll2", JsonList.ofText("[1,2,3]"));
- m.put("c2", JsonList.ofText("[1,2,3]"));
- m.put("m2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("hm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("tm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("jm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("jl2", JsonList.ofText("[1,2,3]"));
+ m.put("l2", JsonList.ofString("[1,2,3]"));
+ m.put("al2", JsonList.ofString("[1,2,3]"));
+ m.put("ll2", JsonList.ofString("[1,2,3]"));
+ m.put("c2", JsonList.ofString("[1,2,3]"));
+ m.put("m2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("hm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("tm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("jm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("jl2", JsonList.ofString("[1,2,3]"));
assertBean(
m,
@@ -298,11 +298,11 @@ class BeanMap_Test extends TestBase {
var m = bc.toBeanMap(t);
// Non-initialized list fields.
- m.put("l1", JsonList.ofText("[1,2,3]"));
- m.put("al1", JsonList.ofText("[1,2,3]"));
- m.put("ll1", JsonList.ofText("[1,2,3]"));
- m.put("c1", JsonList.ofText("[1,2,3]"));
- m.put("jl1", JsonList.ofText("[1,2,3]"));
+ m.put("l1", JsonList.ofString("[1,2,3]"));
+ m.put("al1", JsonList.ofString("[1,2,3]"));
+ m.put("ll1", JsonList.ofString("[1,2,3]"));
+ m.put("c1", JsonList.ofString("[1,2,3]"));
+ m.put("jl1", JsonList.ofString("[1,2,3]"));
// al1 should be initialized with an ArrayList, since it's not a superclass of JsonList.
// The rest are proper superclasses of JsonList.
@@ -313,10 +313,10 @@ class BeanMap_Test extends TestBase {
);
// Non-initialized map fields.
- m.put("m1", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("hm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("jm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("tm1", JsonMap.ofText("{\"foo\":\"bar\"}"));
+ m.put("m1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("hm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("jm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("tm1", JsonMap.ofString("{\"foo\":\"bar\"}"));
// tm1 should be initialized with TreeMap, since it's not a superclass of JsonMap.
// The rest are proper superclasses of JsonMap
@@ -327,15 +327,15 @@ class BeanMap_Test extends TestBase {
);
// Initialized fields should reuse existing field value.
- m.put("l2", JsonList.ofText("[1,2,3]"));
- m.put("al2", JsonList.ofText("[1,2,3]"));
- m.put("ll2", JsonList.ofText("[1,2,3]"));
- m.put("c2", JsonList.ofText("[1,2,3]"));
- m.put("m2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("hm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("tm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("jm2", JsonMap.ofText("{\"foo\":\"bar\"}"));
- m.put("jl2", JsonList.ofText("[1,2,3]"));
+ m.put("l2", JsonList.ofString("[1,2,3]"));
+ m.put("al2", JsonList.ofString("[1,2,3]"));
+ m.put("ll2", JsonList.ofString("[1,2,3]"));
+ m.put("c2", JsonList.ofString("[1,2,3]"));
+ m.put("m2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("hm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("tm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("jm2", JsonMap.ofString("{\"foo\":\"bar\"}"));
+ m.put("jl2", JsonList.ofString("[1,2,3]"));
assertBean(
m,
@@ -424,7 +424,7 @@ class BeanMap_Test extends TestBase {
@Test void a05_arrayProperties() {
var t = new D1();
var m = bc.toBeanMap(t);
- m.put("b", JsonMap.ofText("{\"s\":\"foo\"}"));
+ m.put("b", JsonMap.ofString("{\"s\":\"foo\"}"));
assertNotNull(t.b);
assertEquals("foo", t.b.s);
@@ -438,16 +438,16 @@ class BeanMap_Test extends TestBase {
assertEquals("default", t.b.s);
var p = Json5Parser.create().beanDictionary(D2.class).build();
- m.put("lb1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("lb1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "lb1{class{simpleName}},lb1{0{class{simpleName}}},lb1{0{s}}", "{{ArrayList}},{{{D2}}},{{foobar}}");
- m.put("lb2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("lb2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "lb2{class{simpleName}},lb2{0{class{simpleName}}},lb2{0{s}}", "{{ArrayList}},{{{D2}}},{{foobar}}");
- m.put("ab1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("ab1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "ab1{class{simpleName}},ab1{0{class{simpleName},s}}", "{{D2[]}},{{{D2},foobar}}");
- m.put("ab2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("ab2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "ab2{class{simpleName}},ab2{0{class{simpleName},s}}", "{{D2[]}},{{{D2},foobar}}");
}
@@ -467,7 +467,7 @@ class BeanMap_Test extends TestBase {
@Test void a06_arrayProperties_usingConfig() {
var t = new D1c();
var m = bc.toBeanMap(t);
- m.put("b", JsonMap.ofText("{\"s\":\"foo\"}"));
+ m.put("b", JsonMap.ofString("{\"s\":\"foo\"}"));
assertNotNull(t.b);
assertBean(t.b, "s", "foo");
@@ -481,16 +481,16 @@ class BeanMap_Test extends TestBase {
assertBean(t.b, "s", "default");
var p = Json5Parser.create().beanDictionary(D2c.class).applyAnnotations(D1cConfig.class).build();
- m.put("lb1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("lb1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "lb1{class{simpleName}},lb1{0{class{simpleName}}},lb1{0{s}}", "{{ArrayList}},{{{D2c}}},{{foobar}}");
- m.put("lb2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("lb2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "lb2{class{simpleName}},lb2{0{class{simpleName}}},lb2{0{s}}", "{{ArrayList}},{{{D2c}}},{{foobar}}");
- m.put("ab1", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("ab1", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "ab1{class{simpleName}},ab1{0{class{simpleName},s}}", "{{D2c[]}},{{{D2c},foobar}}");
- m.put("ab2", JsonList.ofText("[{_type:'D2',s:'foobar'}]", p));
+ m.put("ab2", JsonList.ofString("[{_type:'D2',s:'foobar'}]", p));
assertBean(t, "ab2{class{simpleName}},ab2{0{class{simpleName},s}}", "{{D2c[]}},{{{D2c},foobar}}");
}
@@ -517,13 +517,13 @@ class BeanMap_Test extends TestBase {
@Test void a07_arrayPropertiesInJsonList() {
var t = new E();
var m = bc.toBeanMap(t);
- m.put("s", JsonList.ofText("[\"foo\"]"));
- m.put("s2", JsonList.ofText("[[\"foo\"]]"));
- m.put("i", JsonList.ofText("[1,2,3]"));
- m.put("i2", JsonList.ofText("[[1,2,3],[4,5,6]]"));
+ m.put("s", JsonList.ofString("[\"foo\"]"));
+ m.put("s2", JsonList.ofString("[[\"foo\"]]"));
+ m.put("i", JsonList.ofString("[1,2,3]"));
+ m.put("i2", JsonList.ofString("[[1,2,3],[4,5,6]]"));
assertEquals("{s:['foo'],s2:[['foo']],i:[1,2,3],i2:[[1,2,3],[4,5,6]]}", Json5Serializer.DEFAULT.serialize(t));
- m.put("i", JsonList.ofText("[null,null,null]"));
- m.put("i2", JsonList.ofText("[[null,null,null],[null,null,null]]"));
+ m.put("i", JsonList.ofString("[null,null,null]"));
+ m.put("i2", JsonList.ofString("[[null,null,null],[null,null,null]]"));
assertEquals("{s:['foo'],s2:[['foo']],i:[0,0,0],i2:[[0,0,0],[0,0,0]]}", Json5Serializer.DEFAULT.serialize(t));
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java b/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java
index 363d650506..7026cfcee7 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/DataConversion_Test.java
@@ -71,7 +71,7 @@ class DataConversion_Test extends TestBase {
assertEquals("{}", m.getString("x"));
// *** JsonMap ***
- m.put("x", Json5Map.ofText("{foo:123}"));
+ m.put("x", Json5Map.ofString("{foo:123}"));
assertEquals("{foo:123}", m.getString("x"));
// *** Collection ***
@@ -81,7 +81,7 @@ class DataConversion_Test extends TestBase {
assertEquals("[123]", m.getString("x"));
// *** JsonList ***
- m.put("x", JsonList.ofText("[123]"));
+ m.put("x", JsonList.ofString("[123]"));
assertEquals("[123]", m.getString("x"));
assertSize(1, m.getList("x"));
diff --git a/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java
index d375b74c79..b34ff0836e 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/JsonList_Test.java
@@ -92,7 +92,7 @@ class JsonList_Test extends TestBase {
r = l.getAt("0/foo", String.class);
assertEquals("bing", r);
- l.postAt("", Json5Map.ofText("{a:'b'}"));
+ l.postAt("", Json5Map.ofString("{a:'b'}"));
r = l.getAt("2/a", String.class);
assertEquals("b", r);
diff --git a/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java
index 33d6812a0f..4a1f926c2f 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/JsonMap_Test.java
@@ -37,207 +37,207 @@ class JsonMap_Test extends TestBase {
@Test void a01_basic() throws Exception {
var in = "{A:'asdf'}";
- checkStep(1, in, Json5Map.ofText(in).getString("A"), "asdf");
+ checkStep(1, in, Json5Map.ofString(in).getString("A"), "asdf");
in = "{A:{B:'asdf'}}";
- checkStep(2, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdf");
- checkStep(3, in, Json5Map.ofText(in).getString("A"), "{B:'asdf'}");
+ checkStep(2, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdf");
+ checkStep(3, in, Json5Map.ofString(in).getString("A"), "{B:'asdf'}");
in = "{A:{B:'asdf'+\"asdf\"}}";
- checkStep(4, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdfasdf");
- checkStep(5, in, Json5Map.ofText(in).getString("A"), "{B:'asdfasdf'}");
+ checkStep(4, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdfasdf");
+ checkStep(5, in, Json5Map.ofString(in).getString("A"), "{B:'asdfasdf'}");
in = "{A:{B:'asdf' + \n\t \"asdf\"}}";
- checkStep(6, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdfasdf");
- checkStep(7, in, Json5Map.ofText(in).getString("A"), "{B:'asdfasdf'}");
+ checkStep(6, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdfasdf");
+ checkStep(7, in, Json5Map.ofString(in).getString("A"), "{B:'asdfasdf'}");
in = "{A:{B:'asdf\"asdf', C:\"asdf'asdf\", D : \"asdf\\\"asdf\", E: 'asdf\\\'asdf', F:\"asdf\\\'asdf\", G:'asdf\\\"asdf'}}";
- checkStep(8, in, getDeepString(Json5Map.ofText(in), "A/B"), "asdf\"asdf");
- checkStep(9, in, getDeepString(Json5Map.ofText(in), "A/C"), "asdf'asdf");
- checkStep(10, in, getDeepString(Json5Map.ofText(in), "A/D"), "asdf\"asdf");
- checkStep(11, in, getDeepString(Json5Map.ofText(in), "A/E"), "asdf'asdf");
- checkStep(12, in, getDeepString(Json5Map.ofText(in), "A/F"), "asdf'asdf");
- checkStep(13, in, getDeepString(Json5Map.ofText(in), "A/G"), "asdf\"asdf");
+ checkStep(8, in, getDeepString(Json5Map.ofString(in), "A/B"), "asdf\"asdf");
+ checkStep(9, in, getDeepString(Json5Map.ofString(in), "A/C"), "asdf'asdf");
+ checkStep(10, in, getDeepString(Json5Map.ofString(in), "A/D"), "asdf\"asdf");
+ checkStep(11, in, getDeepString(Json5Map.ofString(in), "A/E"), "asdf'asdf");
+ checkStep(12, in, getDeepString(Json5Map.ofString(in), "A/F"), "asdf'asdf");
+ checkStep(13, in, getDeepString(Json5Map.ofString(in), "A/G"), "asdf\"asdf");
in = "{A:123, B: 123}";
- checkStep(16, in, Json5Map.ofText(in).getInt("A").toString(), "123");
- checkStep(17, in, Json5Map.ofText(in).getInt("B").toString(), "123");
+ checkStep(16, in, Json5Map.ofString(in).getInt("A").toString(), "123");
+ checkStep(17, in, Json5Map.ofString(in).getInt("B").toString(), "123");
in = "{A:true, B: true, C:false, D: false}";
- checkStep(18, in, Json5Map.ofText(in).getBoolean("A").toString(), "true");
- checkStep(19, in, Json5Map.ofText(in).getBoolean("B").toString(), "true");
- checkStep(20, in, Json5Map.ofText(in).getBoolean("C").toString(), "false");
- checkStep(21, in, Json5Map.ofText(in).getBoolean("D").toString(), "false");
+ checkStep(18, in, Json5Map.ofString(in).getBoolean("A").toString(), "true");
+ checkStep(19, in, Json5Map.ofString(in).getBoolean("B").toString(), "true");
+ checkStep(20, in, Json5Map.ofString(in).getBoolean("C").toString(), "false");
+ checkStep(21, in, Json5Map.ofString(in).getBoolean("D").toString(), "false");
in = "{'AAA':{\"BBB\":\"CCC\",'DDD':false}}";
- checkStep(31, in, getDeepString(Json5Map.ofText(in), "AAA/BBB"), "CCC");
- checkStep(32, in, getDeepBoolean(Json5Map.ofText(in), "AAA/DDD").toString(), "false");
+ checkStep(31, in, getDeepString(Json5Map.ofString(in), "AAA/BBB"), "CCC");
+ checkStep(32, in, getDeepBoolean(Json5Map.ofString(in), "AAA/DDD").toString(), "false");
in = " \n\n\t { 'AAA' : { \"BBB\" : \"CCC\" , 'DDD' : false } } \n\t";
- checkStep(33, in, getDeepString(Json5Map.ofText(in), "AAA/BBB"), "CCC");
- checkStep(34, in, getDeepBoolean(Json5Map.ofText(in), "AAA/DDD").toString(), "false");
+ checkStep(33, in, getDeepString(Json5Map.ofString(in), "AAA/BBB"), "CCC");
+ checkStep(34, in, getDeepBoolean(Json5Map.ofString(in), "AAA/DDD").toString(), "false");
in = "/*x*/{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(100, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(100, in, Json5Map.ofString(in).getString("A"), "B");
in = "{/*x*/A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(101, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(101, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A/*x*/:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(102, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(102, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:/*x*/'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(103, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(103, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:'/*x*/B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(104, in, Json5Map.ofText(in).getString("A"), "/*x*/B");
+ checkStep(104, in, Json5Map.ofString(in).getString("A"), "/*x*/B");
in = "{A:'B/*x*/','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(105, in, Json5Map.ofText(in).getString("A"), "B/*x*/");
+ checkStep(105, in, Json5Map.ofString(in).getString("A"), "B/*x*/");
in = "{A:'B'/*x*/,'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(106, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(106, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:'B',/*x*/'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(107, in, Json5Map.ofText(in).getString("C"), "1");
+ checkStep(107, in, Json5Map.ofString(in).getString("C"), "1");
in = "{A:'B','C':/*x*/1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(108, in, Json5Map.ofText(in).getString("C"), "1");
+ checkStep(108, in, Json5Map.ofString(in).getString("C"), "1");
in = "{A:'B','C':1/*x*/,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(109, in, Json5Map.ofText(in).getString("C"), "1");
+ checkStep(109, in, Json5Map.ofString(in).getString("C"), "1");
in = "{A:'B','C':1,/*x*/\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(110, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(110, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"/*x*/E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(111, in, Json5Map.ofText(in).getList("/*x*/E").getString(0), "1");
+ checkStep(111, in, Json5Map.ofString(in).getList("/*x*/E").getString(0), "1");
in = "{A:'B','C':1,\"E/*x*/\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(112, in, Json5Map.ofText(in).getList("E/*x*/").getString(0), "1");
+ checkStep(112, in, Json5Map.ofString(in).getList("E/*x*/").getString(0), "1");
in = "{A:'B','C':1,\"E\"/*x*/:[1,2,3],G:['g1','g2','g3']}";
- checkStep(113, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(113, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":/*x*/[1,2,3],G:['g1','g2','g3']}";
- checkStep(114, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(114, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":[/*x*/1,2,3],G:['g1','g2','g3']}";
- checkStep(115, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(115, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":[1/*x*/,2,3],G:['g1','g2','g3']}";
- checkStep(116, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(116, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":[1,/*x*/2,3],G:['g1','g2','g3']}";
- checkStep(117, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+ checkStep(117, in, Json5Map.ofString(in).getList("E").getString(1), "2");
in = "{A:'B','C':1,\"E\":[1,2/*x*/,3],G:['g1','g2','g3']}";
- checkStep(118, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+ checkStep(118, in, Json5Map.ofString(in).getList("E").getString(1), "2");
in = "{A:'B','C':1,\"E\":[1,2,/*x*/3],G:['g1','g2','g3']}";
- checkStep(119, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+ checkStep(119, in, Json5Map.ofString(in).getList("E").getString(2), "3");
in = "{A:'B','C':1,\"E\":[1,2,3]/*x*/,G:['g1','g2','g3']}";
- checkStep(120, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+ checkStep(120, in, Json5Map.ofString(in).getList("E").getString(2), "3");
in = "{A:'B','C':1,\"E\":[1,2,3],/*x*/G:['g1','g2','g3']}";
- checkStep(121, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+ checkStep(121, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:[/*x*/'g1','g2','g3']}";
- checkStep(122, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+ checkStep(122, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['/*x*/g1','g2','g3']}";
- checkStep(123, in, Json5Map.ofText(in).getList("G").getString(0), "/*x*/g1");
+ checkStep(123, in, Json5Map.ofString(in).getList("G").getString(0), "/*x*/g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1'/*x*/,'g2','g3']}";
- checkStep(124, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+ checkStep(124, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1',/*x*/'g2','g3']}";
- checkStep(125, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+ checkStep(125, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2'/*x*/,'g3']}";
- checkStep(126, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+ checkStep(126, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2',/*x*/'g3']}";
- checkStep(127, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(127, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3'/*x*/]}";
- checkStep(128, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(128, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']/*x*/}";
- checkStep(129, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(129, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}/*x*/";
- checkStep(130, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(130, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "/*\tx\t*///\tx\t\n\t/*\tx\t*/{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(201, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(201, in, Json5Map.ofString(in).getString("A"), "B");
in = "{/*\tx\t*///\tx\t\n\t/*\tx\t*/A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(202, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(202, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A/*\tx\t*///\tx\t\n\t/*\tx\t*/:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(203, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(203, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:/*\tx\t*///\tx\t\n\t/*\tx\t*/'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(204, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(204, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:'/*\tx\t*///\tx\t\n\t/*\tx\t*/B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(205, in, Json5Map.ofText(in).getString("A"), "/*\tx\t*///\tx\t\n\t/*\tx\t*/B");
+ checkStep(205, in, Json5Map.ofString(in).getString("A"), "/*\tx\t*///\tx\t\n\t/*\tx\t*/B");
in = "{A:'B/*\tx\t*///\tx\t\n\t/*\tx\t*/','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(206, in, Json5Map.ofText(in).getString("A"), "B/*\tx\t*///\tx\t\n\t/*\tx\t*/");
+ checkStep(206, in, Json5Map.ofString(in).getString("A"), "B/*\tx\t*///\tx\t\n\t/*\tx\t*/");
in = "{A:'B'/*\tx\t*///\tx\t\n\t/*\tx\t*/,'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(207, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(207, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:'B',/*\tx\t*///\tx\t\n\t/*\tx\t*/'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(208, in, Json5Map.ofText(in).getString("C"), "1");
+ checkStep(208, in, Json5Map.ofString(in).getString("C"), "1");
in = "{A:'B','C':/*\tx\t*///\tx\t\n\t/*\tx\t*/1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(209, in, Json5Map.ofText(in).getString("C"), "1");
+ checkStep(209, in, Json5Map.ofString(in).getString("C"), "1");
in = "{A:'B','C':1/*\tx\t*///\tx\t\n\t/*\tx\t*/,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(210, in, Json5Map.ofText(in).getString("C"), "1");
+ checkStep(210, in, Json5Map.ofString(in).getString("C"), "1");
in = "{A:'B','C':1,/*\tx\t*///\tx\t\n\t/*\tx\t*/\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(211, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(211, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"/*\tx\t*///\tx\t\n\t/*\tx\t*/E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(212, in, Json5Map.ofText(in).getList("/*\tx\t*///\tx\t\n\t/*\tx\t*/E").getString(0), "1");
+ checkStep(212, in, Json5Map.ofString(in).getList("/*\tx\t*///\tx\t\n\t/*\tx\t*/E").getString(0), "1");
in = "{A:'B','C':1,\"E/*\tx\t*///\tx\t\n\t/*\tx\t*/\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(213, in, Json5Map.ofText(in).getList("E/*\tx\t*///\tx\t\n\t/*\tx\t*/").getString(0), "1");
+ checkStep(213, in, Json5Map.ofString(in).getList("E/*\tx\t*///\tx\t\n\t/*\tx\t*/").getString(0), "1");
in = "{A:'B','C':1,\"E\"/*\tx\t*///\tx\t\n\t/*\tx\t*/:[1,2,3],G:['g1','g2','g3']}";
- checkStep(214, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(214, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":/*\tx\t*///\tx\t\n\t/*\tx\t*/[1,2,3],G:['g1','g2','g3']}";
- checkStep(215, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(215, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":[/*\tx\t*///\tx\t\n\t/*\tx\t*/1,2,3],G:['g1','g2','g3']}";
- checkStep(216, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(216, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":[1/*\tx\t*///\tx\t\n\t/*\tx\t*/,2,3],G:['g1','g2','g3']}";
- checkStep(217, in, Json5Map.ofText(in).getList("E").getString(0), "1");
+ checkStep(217, in, Json5Map.ofString(in).getList("E").getString(0), "1");
in = "{A:'B','C':1,\"E\":[1,/*\tx\t*///\tx\t\n\t/*\tx\t*/2,3],G:['g1','g2','g3']}";
- checkStep(218, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+ checkStep(218, in, Json5Map.ofString(in).getList("E").getString(1), "2");
in = "{A:'B','C':1,\"E\":[1,2/*\tx\t*///\tx\t\n\t/*\tx\t*/,3],G:['g1','g2','g3']}";
- checkStep(219, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+ checkStep(219, in, Json5Map.ofString(in).getList("E").getString(1), "2");
in = "{A:'B','C':1,\"E\":[1,2,/*\tx\t*///\tx\t\n\t/*\tx\t*/3],G:['g1','g2','g3']}";
- checkStep(220, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+ checkStep(220, in, Json5Map.ofString(in).getList("E").getString(2), "3");
in = "{A:'B','C':1,\"E\":[1,2,3]/*\tx\t*///\tx\t\n\t/*\tx\t*/,G:['g1','g2','g3']}";
- checkStep(221, in, Json5Map.ofText(in).getList("E").getString(2), "3");
+ checkStep(221, in, Json5Map.ofString(in).getList("E").getString(2), "3");
in = "{A:'B','C':1,\"E\":[1,2,3],/*\tx\t*///\tx\t\n\t/*\tx\t*/G:['g1','g2','g3']}";
- checkStep(222, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+ checkStep(222, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:[/*\tx\t*///\tx\t\n\t/*\tx\t*/'g1','g2','g3']}";
- checkStep(223, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+ checkStep(223, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['/*\tx\t*///\tx\t\n\t/*\tx\t*/g1','g2','g3']}";
- checkStep(224, in, Json5Map.ofText(in).getList("G").getString(0), "/*\tx\t*///\tx\t\n\t/*\tx\t*/g1");
+ checkStep(224, in, Json5Map.ofString(in).getList("G").getString(0), "/*\tx\t*///\tx\t\n\t/*\tx\t*/g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1'/*\tx\t*///\tx\t\n\t/*\tx\t*/,'g2','g3']}";
- checkStep(225, in, Json5Map.ofText(in).getList("G").getString(0), "g1");
+ checkStep(225, in, Json5Map.ofString(in).getList("G").getString(0), "g1");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1',/*\tx\t*///\tx\t\n\t/*\tx\t*/'g2','g3']}";
- checkStep(226, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+ checkStep(226, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2'/*\tx\t*///\tx\t\n\t/*\tx\t*/,'g3']}";
- checkStep(227, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+ checkStep(227, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2',/*\tx\t*///\tx\t\n\t/*\tx\t*/'g3']}";
- checkStep(228, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(228, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3'/*\tx\t*///\tx\t\n\t/*\tx\t*/]}";
- checkStep(229, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(229, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']/*\tx\t*///\tx\t\n\t/*\tx\t*/}";
- checkStep(230, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(230, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}/*\tx\t*///\tx\t\n\t/*\tx\t*/";
- checkStep(231, in, Json5Map.ofText(in).getList("G").getString(2), "g3");
+ checkStep(231, in, Json5Map.ofString(in).getList("G").getString(2), "g3");
in = "{ /* x */ // x \n /* x */ A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(240, in, Json5Map.ofText(in).getString("A"), "B");
+ checkStep(240, in, Json5Map.ofString(in).getString("A"), "B");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(301, in, Json5Map.ofText(in).getString("A", "default"), "B");
+ checkStep(301, in, Json5Map.ofString(in).getString("A", "default"), "B");
in = "{/*A:'B',*/'C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(302, in, Json5Map.ofText(in).getString("A", "default"), "default");
+ checkStep(302, in, Json5Map.ofString(in).getString("A", "default"), "default");
in = "{A:'B',/*'C':1,*/\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(303, in, Json5Map.ofText(in).getString("C", "default"), "default");
+ checkStep(303, in, Json5Map.ofString(in).getString("C", "default"), "default");
in = "{A:'B','C':1,/*\"E\":[1,2,3],*/G:['g1','g2','g3']}";
- checkStep(304, in, Json5Map.ofText(in).getString("E", "default"), "default");
+ checkStep(304, in, Json5Map.ofString(in).getString("E", "default"), "default");
in = "{A:'B','C':1,\"E\":[/*1,*/2,3],G:['g1','g2','g3']}";
- checkStep(305, in, Json5Map.ofText(in).getList("E").getString(0), "2");
+ checkStep(305, in, Json5Map.ofString(in).getList("E").getString(0), "2");
in = "{A:'B','C':1,\"E\":[1,/*2,*/3],G:['g1','g2','g3']}";
- checkStep(306, in, Json5Map.ofText(in).getList("E").getString(1), "3");
+ checkStep(306, in, Json5Map.ofString(in).getList("E").getString(1), "3");
in = "{A:'B','C':1,\"E\":[1,2/*,3*/],G:['g1','g2','g3']}";
- checkStep(307, in, Json5Map.ofText(in).getList("E").getString(1), "2");
+ checkStep(307, in, Json5Map.ofString(in).getList("E").getString(1), "2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:[/*'g1',*/'g2','g3']}";
- checkStep(308, in, Json5Map.ofText(in).getList("G").getString(0), "g2");
+ checkStep(308, in, Json5Map.ofString(in).getList("G").getString(0), "g2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1'/*,'g2'*/,'g3']}";
- checkStep(309, in, Json5Map.ofText(in).getList("G").getString(1), "g3");
+ checkStep(309, in, Json5Map.ofString(in).getList("G").getString(1), "g3");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2'/*,'g3'*/]}";
- checkStep(310, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+ checkStep(310, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
in = "{A:'B','C':1,\"E\":[1,2,3],G:['g1','g2','g3']}";
- checkStep(310, in, Json5Map.ofText(in).getList("G").getString(1), "g2");
+ checkStep(310, in, Json5Map.ofString(in).getList("G").getString(1), "g2");
// Check keys that contain array indexes
in = "{A:{B:[{C:'c0'},{C:'c1'},{C:'c2'}]}}";
- checkStep(401, in, getDeepString(Json5Map.ofText(in), "A/B/0/C"), "c0");
- checkStep(402, in, getDeepString(Json5Map.ofText(in), "A/B/1/C"), "c1");
- checkStep(403, in, getDeepString(Json5Map.ofText(in), "A/B/2/C"), "c2");
+ checkStep(401, in, getDeepString(Json5Map.ofString(in), "A/B/0/C"), "c0");
+ checkStep(402, in, getDeepString(Json5Map.ofString(in), "A/B/1/C"), "c1");
+ checkStep(403, in, getDeepString(Json5Map.ofString(in), "A/B/2/C"), "c2");
// Check extended unicode characters.
in = "{'𤭢𤭢':'𤭢𤭢'}";
- checkStep(1, in, Json5Map.ofText(in).getString("𤭢𤭢"), "𤭢𤭢");
+ checkStep(1, in, Json5Map.ofString(in).getString("𤭢𤭢"), "𤭢𤭢");
}
private static String getDeepString(Json5Map m, String url) {
@@ -261,8 +261,8 @@ class JsonMap_Test extends TestBase {
// testComparison
//====================================================================================================
@Test void a02_comparison() throws Exception {
- var m1 = Json5Map.ofText("{ firstName:'John', lastName:'Smith', age:123, isDeceased:false }");
- var m2 = Json5Map.ofText("{ age:123, isDeceased:false, lastName:'Smith', firstName:'John' }");
+ var m1 = Json5Map.ofString("{ firstName:'John', lastName:'Smith', age:123, isDeceased:false }");
+ var m2 = Json5Map.ofString("{ age:123, isDeceased:false, lastName:'Smith', firstName:'John' }");
assertEquals(m1, m2);
}
@@ -271,8 +271,8 @@ class JsonMap_Test extends TestBase {
// testParent
//====================================================================================================
@Test void a03_parent() throws Exception {
- var m1 = Json5Map.ofText("{a:1}");
- var m2 = Json5Map.ofText("{b:2}").inner(m1);
+ var m1 = Json5Map.ofString("{a:1}");
+ var m2 = Json5Map.ofString("{b:2}").inner(m1);
assertEquals(Integer.valueOf(1), m2.getInt("a"));
}
@@ -281,13 +281,13 @@ class JsonMap_Test extends TestBase {
// testUpdatability
//====================================================================================================
@Test void a04_updatability() throws Exception {
- var m = Json5Map.ofText("{a:[{b:'c'}]}");
+ var m = Json5Map.ofString("{a:[{b:'c'}]}");
var l = m.getList("a");
var m2 = l.getMap(0);
m2.put("b", "x");
assertBean(m, "a", "[{b=x}]");
- m = Json5Map.ofText("{a:[{b:'c'}]}");
+ m = Json5Map.ofString("{a:[{b:'c'}]}");
for (var m3 : m.getList("a").elements(Json5Map.class))
m3.put("b", "y");
@@ -298,7 +298,7 @@ class JsonMap_Test extends TestBase {
// testAtMethods
//====================================================================================================
@Test void a05_atMethods() throws Exception {
- var m = Json5Map.ofText("{a:[{b:'c'}]}");
+ var m = Json5Map.ofString("{a:[{b:'c'}]}");
var r = m.getAt("a/0/b", String.class);
assertEquals("c", r);
@@ -319,14 +319,14 @@ class JsonMap_Test extends TestBase {
// JsonMap(Reader)
//====================================================================================================
@Test void a06_fromReader() throws Exception {
- assertBean(Json5Map.ofText(reader("{foo:'bar'}")), "foo", "bar");
+ assertBean(Json5Map.ofString(reader("{foo:'bar'}")), "foo", "bar");
}
//====================================================================================================
// testGetMap
//====================================================================================================
@Test void a07_getMap() throws Exception {
- var m = Json5Map.ofText("{a:{1:'true',2:'false'}}");
+ var m = Json5Map.ofString("{a:{1:'true',2:'false'}}");
var m2 = m.getMap("a", Integer.class, Boolean.class, null);
assertJson("{'1':true,'2':false}", m2);
assertEquals(Integer.class, m2.keySet().iterator().next().getClass());
@@ -348,7 +348,7 @@ class JsonMap_Test extends TestBase {
// testGetList
//====================================================================================================
@Test void a08_getList() throws Exception {
- var m = Json5Map.ofText("{a:['123','456']}");
+ var m = Json5Map.ofString("{a:['123','456']}");
var l2 = m.getList("a", Integer.class, null);
assertList(l2, "123", "456");
assertEquals(Integer.class, l2.iterator().next().getClass());
@@ -368,7 +368,7 @@ class JsonMap_Test extends TestBase {
// toX serialization methods
//====================================================================================================
@Test void a09_toX() {
- var m = Json5Map.ofText("{b:'2',a:'1'}");
+ var m = Json5Map.ofString("{b:'2',a:'1'}");
// toJson5 — JSON5 (unquoted keys where possible), same as toString()
assertString("{b:'2',a:'1'}", m.toJson5());
diff --git a/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java b/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java
index e9f866d09b..f6af8c070b 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/MarshalledConfig_Test.java
@@ -314,7 +314,7 @@ class MarshalledConfig_Test extends TestBase {
assertEquals("{name:'x',age:123}", bc.convertToType(o, String.class));
// List of Maps to array of beans.
- var o2 = JsonList.of(JsonMap.ofText("{\"name\":\"x\",\"age\":1}"), JsonMap.ofText("{\"name\":\"y\",\"age\":2}"));
+ var o2 = JsonList.of(JsonMap.ofString("{\"name\":\"x\",\"age\":1}"), JsonMap.ofString("{\"name\":\"y\",\"age\":2}"));
assertEquals(1, bc.convertToType(o2, ReadOnlyPerson[].class)[0].getAge());
}
@@ -346,7 +346,7 @@ class MarshalledConfig_Test extends TestBase {
assertEquals("{name:'x',age:123}", bc.convertToType(o, String.class));
// List of Maps to array of beans.
- var o2 = JsonList.of(JsonMap.ofText("{\"name\":\"x\",\"age\":1}"), JsonMap.ofText("{\"name\":\"y\",\"age\":2}"));
+ var o2 = JsonList.of(JsonMap.ofString("{\"name\":\"x\",\"age\":1}"), JsonMap.ofString("{\"name\":\"y\",\"age\":2}"));
assertEquals(1, bc.convertToType(o2, ReadOnlyPerson2[].class)[0].getAge());
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java
index 3f1ad2ba59..3bd15c6556 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/ObjectSwap_Test.java
@@ -39,7 +39,7 @@ class ObjectSwap_Test extends TestBase {
r = p.parse(r, String.class);
assertEquals("foobar", r);
- var m = Json5Map.ofText("{foo:'bar'}");
+ var m = Json5Map.ofString("{foo:'bar'}");
r = s.serialize(m);
assertEquals("{xfoox:'xbarx'}", r);
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java
index 26ddf62bad..90ed463215 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/JsonMaps_RoundTripTest.java
@@ -42,7 +42,7 @@ class JsonMaps_RoundTripTest extends RoundTripTest_Base {
@ParameterizedTest
@MethodSource("testers")
void a01_basic(RoundTrip_Tester t) throws Exception {
- var x1 = new A(JsonMap.ofText("{\"f1\":\"a\",\"f2\":2}"));
+ var x1 = new A(JsonMap.ofString("{\"f1\":\"a\",\"f2\":2}"));
x1 = t.roundTrip(x1, A.class);
assertBean(x1, "f1,f2", "a,2");
@@ -50,11 +50,11 @@ class JsonMaps_RoundTripTest extends RoundTripTest_Base {
x2 = t.roundTrip(x2, A[].class);
assertBean(x2, "length,#{f1,f2}", "1,[{a,2}]");
- var x3 = l(new A(JsonMap.ofText("{\"f1\":\"a\",\"f2\":2}")));
+ var x3 = l(new A(JsonMap.ofString("{\"f1\":\"a\",\"f2\":2}")));
x3 = t.roundTrip(x3, List.class, A.class);
assertBean(x3, "size,0{f1,f2}", "1,{a,2}");
- var x4 = m("a",new A(JsonMap.ofText("{\"f1\":\"a\",\"f2\":2}")));
+ var x4 = m("a",new A(JsonMap.ofString("{\"f1\":\"a\",\"f2\":2}")));
x4 = t.roundTrip(x4, Map.class, String.class, A.class);
assertBean(x4, "size,a{f1,f2}", "1,{a,2}");
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java
index e0b2b494d0..f3d3511339 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/SimpleObjects_RoundTripTest.java
@@ -450,7 +450,7 @@ class SimpleObjects_RoundTripTest extends RoundTripTest_Base {
void a51_jsonMap(RoundTrip_Tester t) throws Exception {
if (isRdf(t))
return;
- var x = JsonMap.ofText("{\"a\":\"b\",\"c\":123,\"d\":false,\"e\":null,\"f\":[123,\"abc\",true,false,null]}");
+ var x = JsonMap.ofString("{\"a\":\"b\",\"c\":123,\"d\":false,\"e\":null,\"f\":[123,\"abc\",true,false,null]}");
x = t.roundTrip(x);
assertEquals("b", x.get("a"));
assertEquals(123, ((Number)x.get("c")).intValue());
diff --git a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java
index 9c7faafe7a..56d4df68c5 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/a/rttests/TrimStrings_RoundTripTest.java
@@ -54,8 +54,8 @@ class TrimStrings_RoundTripTest extends RoundTripTest_Base {
a = p2.parse(s.serialize(in), String.class);
assertEquals(json(a), json(e));
- in = JsonMap.ofText("{\" foo \": \" bar \"}");
- e = JsonMap.ofText("{\"foo\":\"bar\"}");
+ in = JsonMap.ofString("{\" foo \": \" bar \"}");
+ e = JsonMap.ofString("{\"foo\":\"bar\"}");
a = p.parse(s2.serialize(in), JsonMap.class);
assertEquals(json(a), json(e));
a = p2.parse(s.serialize(in), JsonMap.class);
@@ -89,16 +89,16 @@ class TrimStrings_RoundTripTest extends RoundTripTest_Base {
public A init1() throws Exception {
f1 = " f1 ";
f2 = a(" f2a ", " f2b ");
- f3 = JsonList.ofText("[\" f3a \",\" f3b \"]");
- f4 = JsonMap.ofText("{\" foo \":\" bar \"}");
+ f3 = JsonList.ofString("[\" f3a \",\" f3b \"]");
+ f4 = JsonMap.ofString("{\" foo \":\" bar \"}");
return this;
}
public A init2() throws Exception {
f1 = "f1";
f2 = a("f2a", "f2b");
- f3 = JsonList.ofText("[\"f3a\",\"f3b\"]");
- f4 = JsonMap.ofText("{\"foo\":\"bar\"}");
+ f3 = JsonList.ofString("[\"f3a\",\"f3b\"]");
+ f4 = JsonMap.ofString("{\"foo\":\"bar\"}");
return this;
}
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/hal/Hal_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/hal/Hal_RoundTrip_Test.java
new file mode 100644
index 0000000000..576c2158b8
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/hal/Hal_RoundTrip_Test.java
@@ -0,0 +1,350 @@
+/*
+ * 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.bean.hal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@code juneau-bean-hal} wire beans.
+ */
+class Hal_RoundTrip_Test {
+
+ private static final JsonSerializer HAL_JSON = JsonSerializer.DEFAULT;
+ private static final JsonParser HAL_PARSER = JsonParser.DEFAULT;
+
+ private static void assertJsonRoundTrip(Object bean, Class<?> type) {
+ var j1 = HAL_JSON.serialize(bean);
+ var copy = HAL_PARSER.parse(j1, type);
+ var j2 = HAL_JSON.serialize(copy);
+ assertEquals(j1, j2, () -> "Round-trip JSON mismatch for " + type.getName() + ": " + j1);
+ }
+
+ @Nested
+ class A_HalLink {
+
+ @Test
+ void allFields_roundTrip() {
+ var link = new HalLink()
+ .setHref("/orders/123")
+ .setTemplated(false)
+ .setType("application/hal+json")
+ .setDeprecation("https://api.example.com/deprecations/links")
+ .setName("acme")
+ .setProfile("https://api.example.com/profiles/order")
+ .setTitle("Order #123")
+ .setHreflang("en");
+ assertJsonRoundTrip(link, HalLink.class);
+ }
+
+ @Test
+ void hrefConstructor_andToStringWorks() {
+ var link = new HalLink("/u");
+ assertEquals("/u", link.getHref());
+ assertNotNull(link.toString());
+ }
+
+ @Test
+ void emptyConstructor_fieldsAreNull() {
+ var link = new HalLink();
+ assertNull(link.getHref());
+ assertNull(link.getTemplated());
+ assertNull(link.getType());
+ assertNull(link.getDeprecation());
+ assertNull(link.getName());
+ assertNull(link.getProfile());
+ assertNull(link.getTitle());
+ assertNull(link.getHreflang());
+ }
+ }
+
+ @Nested
+ class B_HalResource_links {
+
+ @Test
+ void singleLink_roundTripsAsObject() {
+ var r = new HalResource().addLink("self", new HalLink("/orders/123"));
+ var j = HAL_JSON.serialize(r);
+ // Single-link form ⇒ JSON object, not array.
+ assertTrue(j.contains("\"self\":{"), () -> "Expected single-object form for self link: " + j);
+ assertJsonRoundTrip(r, HalResource.class);
+ }
+
+ @Test
+ void multiLink_roundTripsAsArray() {
+ var r = new HalResource()
+ .addLinks("curies",
+ new HalLink("https://acme.example/{rel}").setName("acme").setTemplated(true),
+ new HalLink("https://acme.example/v2/{rel}").setName("acme2").setTemplated(true)
+ );
+ var j = HAL_JSON.serialize(r);
+ assertTrue(j.contains("\"curies\":["), () -> "Expected multi-link array form for curies: " + j);
+ assertJsonRoundTrip(r, HalResource.class);
+ }
+
+ @Test
+ void addLinks_promotesSingleToArray() {
+ var r = new HalResource()
+ .addLink("alt", new HalLink("/a"))
+ .addLinks("alt", new HalLink("/b"));
+ var raw = r.getLinks().get("alt");
+ assertInstanceOf(HalLinkArray.class, raw);
+ assertEquals(2, ((HalLinkArray) raw).size());
+ assertJsonRoundTrip(r, HalResource.class);
+ }
+
+ @Test
+ void addLinks_extendsExistingArray() {
+ var r = new HalResource()
+ .addLinks("alt", new HalLink("/a"), new HalLink("/b"))
+ .addLinks("alt", new HalLink("/c"));
+ var raw = r.getLinks().get("alt");
+ assertInstanceOf(HalLinkArray.class, raw);
+ assertEquals(3, ((HalLinkArray) raw).size());
+ }
+
+ @Test
+ void setLinks_replacesContent() {
+ var r = new HalResource()
+ .addLink("self", new HalLink("/x"))
+ .setLinks(new LinkedHashMap<>(Map.of("self", new HalLink("/y"))));
+ assertEquals("/y", ((HalLink) r.getLinks().get("self")).getHref());
+ }
+ }
+
+ @Nested
+ class C_HalResource_embedded {
+
+ @Test
+ void singleEmbedded_roundTripsAsObject() {
+ var inner = new HalResource()
+ .addLink("self", new HalLink("/items/1"))
+ .set("name", "widget");
+ var r = new HalResource()
+ .addLink("self", new HalLink("/orders/123"))
+ .addEmbedded("item", inner);
+ var j = HAL_JSON.serialize(r);
+ assertTrue(j.contains("\"item\":{"), () -> "Expected single-embedded object form: " + j);
+ assertJsonRoundTrip(r, HalResource.class);
+ }
+
+ @Test
+ void multiEmbedded_roundTripsAsArray() {
+ var r = new HalResource()
+ .addLink("self", new HalLink("/orders/123"))
+ .addEmbedded("items",
+ new HalResource().addLink("self", new HalLink("/items/1")).set("name", "a"),
+ new HalResource().addLink("self", new HalLink("/items/2")).set("name", "b")
+ );
+ var j = HAL_JSON.serialize(r);
+ assertTrue(j.contains("\"items\":["), () -> "Expected multi-embedded array form: " + j);
+ assertJsonRoundTrip(r, HalResource.class);
+ }
+
+ @Test
+ void addEmbedded_promotesSingleToArray() {
+ var r = new HalResource()
+ .addEmbedded("item", new HalResource().set("k", "v1"))
+ .addEmbedded("item", new HalResource[] { new HalResource().set("k", "v2") });
+ var raw = r.getEmbedded().get("item");
+ assertInstanceOf(HalResourceArray.class, raw);
+ assertEquals(2, ((HalResourceArray) raw).size());
+ }
+
+ @Test
+ void addEmbedded_extendsExistingArray() {
+ var r = new HalResource()
+ .addEmbedded("items", new HalResource[] { new HalResource().set("k", "a") })
+ .addEmbedded("items", new HalResource[] { new HalResource().set("k", "b") });
+ var raw = r.getEmbedded().get("items");
+ assertInstanceOf(HalResourceArray.class, raw);
+ assertEquals(2, ((HalResourceArray) raw).size());
+ }
+
+ @Test
+ void addEmbeddedSingle_twiceUnderDifferentRelations_keepsBothInExistingMap() {
+ var r = new HalResource()
+ .addEmbedded("a", new HalResource().set("k", "va"))
+ .addEmbedded("b", new HalResource().set("k", "vb"));
+ assertEquals(2, r.getEmbedded().size());
+ }
+
+ @Test
+ void setEmbedded_replacesContent() {
+ var r = new HalResource()
+ .addEmbedded("a", new HalResource().set("k", "v"))
+ .setEmbedded(new LinkedHashMap<>(Map.of("b", new HalResource().set("k", "w"))));
+ assertNull(r.getEmbedded().get("a"));
+ assertNotNull(r.getEmbedded().get("b"));
+ }
+ }
+
+ @Nested
+ class D_HalResource_extras {
+
+ @Test
+ void payload_isFlat_notNestedUnderProperties() {
+ var r = new HalResource()
+ .addLink("self", new HalLink("/orders/123"))
+ .set("total", 99.50)
+ .set("currency", "USD");
+ var j = HAL_JSON.serialize(r);
+ assertTrue(j.contains("\"total\":99.5"), () -> j);
+ assertTrue(j.contains("\"currency\":\"USD\""), () -> j);
+ assertJsonRoundTrip(r, HalResource.class);
+ }
+
+ @Test
+ void getOnUnknownReturnsNull() {
+ var r = new HalResource();
+ assertNull(r.get("nope"));
+ assertTrue(r.extraKeys().isEmpty());
+ }
+
+ @Test
+ void getAfterSetReturnsValue() {
+ var r = new HalResource().set("k", "v");
+ assertEquals("v", r.get("k"));
+ assertTrue(r.extraKeys().contains("k"));
+ }
+ }
+
+ @Nested
+ class E_TopLevelArrayBeans {
+
+ @Test
+ void halLinkArray_constructionAndAddAll() {
+ var arr = new HalLinkArray(new HalLink("/a"));
+ arr.addAll(new HalLink("/b"), new HalLink("/c"));
+ assertEquals(3, arr.size());
+ }
+
+ @Test
+ void halResourceArray_constructionAndAddAll() {
+ var arr = new HalResourceArray(new HalResource());
+ arr.addAll(new HalResource(), new HalResource());
+ assertEquals(3, arr.size());
+ }
+
+ @Test
+ void halLinkArray_serializesAsJsonArray() {
+ var arr = new HalLinkArray(new HalLink("/x"), new HalLink("/y"));
+ var j = HAL_JSON.serialize(arr);
+ assertTrue(j.startsWith("["), j);
+ assertJsonRoundTrip(arr, HalLinkArray.class);
+ }
+ }
+
+ @Nested
+ class F_Swaps_directInvocation {
+
+ @Test
+ void linkOrArraySwap_nullIsNull() {
+ var s = new HalLinkOrArraySwap();
+ assertNull(s.unswap(null, null, null));
+ }
+
+ @Test
+ void linkOrArraySwap_swapIsIdentity() {
+ var s = new HalLinkOrArraySwap();
+ var input = new Object();
+ assertSame(input, s.swap(null, input));
+ }
+
+ @Test
+ void linkOrArraySwap_nonMapPassesThrough() {
+ var s = new HalLinkOrArraySwap();
+ var input = "not-a-map";
+ assertSame(input, s.unswap(null, input, null));
+ }
+
+ @Test
+ void linkOrArraySwap_mapWithScalarPassesValueThrough() {
+ // Round-trip via parser to exercise the "else" branch (scalar entries inside a links map).
+ var json = "{\"_links\":{\"x\":\"a-string-not-a-link\"}}";
+ var r = JsonParser.DEFAULT.parse(json, HalResource.class);
+ assertEquals("a-string-not-a-link", r.getLinks().get("x"));
+ }
+
+ @Test
+ void resourceOrArraySwap_nullIsNull() {
+ var s = new HalResourceOrArraySwap();
+ assertNull(s.unswap(null, null, null));
+ }
+
+ @Test
+ void resourceOrArraySwap_swapIsIdentity() {
+ var s = new HalResourceOrArraySwap();
+ var input = new Object();
+ assertSame(input, s.swap(null, input));
+ }
+
+ @Test
+ void resourceOrArraySwap_nonMapPassesThrough() {
+ var s = new HalResourceOrArraySwap();
+ var input = "not-a-map";
+ assertSame(input, s.unswap(null, input, null));
+ }
+
+ @Test
+ void resourceOrArraySwap_mapWithScalarPassesValueThrough() {
+ var json = "{\"_embedded\":{\"x\":\"a-string-not-a-resource\"}}";
+ var r = JsonParser.DEFAULT.parse(json, HalResource.class);
+ assertEquals("a-string-not-a-resource", r.getEmbedded().get("x"));
+ }
+ }
+
+ @Nested
+ class G_Resource_toString {
+
+ @Test
+ void toString_doesNotThrow() {
+ assertNotNull(new HalResource().addLink("self", new HalLink("/a")).toString());
+ }
+
+ @Test
+ void halLink_toString_doesNotThrow() {
+ assertNotNull(new HalLink("/a").toString());
+ }
+ }
+
+ @Nested
+ class H_Resource_CompoundExample {
+
+ @Test
+ void compoundDocument_roundTrip() {
+ var order = new HalResource()
+ .addLink("self", new HalLink("/orders/123"))
+ .addLink("warehouse", new HalLink("/warehouse/56"))
+ .addLink("invoice", new HalLink("/invoices/873"))
+ .addLinks("curies", new HalLink("https://acme.example/{rel}").setName("acme").setTemplated(true))
+ .set("currency", "USD")
+ .set("status", "shipped")
+ .set("total", 30.00)
+ .addEmbedded("customer",
+ new HalResource()
+ .addLink("self", new HalLink("/customers/7"))
+ .set("name", "Kelsey")
+ );
+ assertJsonRoundTrip(order, HalResource.class);
+ }
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonapi/JsonApi_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonapi/JsonApi_RoundTrip_Test.java
new file mode 100644
index 0000000000..10c90872ba
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonapi/JsonApi_RoundTrip_Test.java
@@ -0,0 +1,446 @@
+/*
+ * 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.bean.jsonapi;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.list;
+import static org.apache.juneau.commons.utils.CollectionUtils.map;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@code juneau-bean-jsonapi} wire beans.
+ */
+class JsonApi_RoundTrip_Test {
+
+ private static final JsonSerializer SER = JsonSerializer.DEFAULT;
+ private static final JsonParser PAR = JsonParser.DEFAULT;
+
+ private static void assertJsonRoundTrip(Object bean, Class<?> type) {
+ var j1 = SER.serialize(bean);
+ var copy = PAR.parse(j1, type);
+ var j2 = SER.serialize(copy);
+ assertEquals(j1, j2, () -> "Round-trip JSON mismatch for " + type.getName() + ": " + j1);
+ }
+
+ @Nested
+ class A_Link {
+
+ @Test
+ void allFields_roundTrip() {
+ var link = new JsonApiLink()
+ .setHref("https://example.com/articles/1")
+ .setRel("canonical")
+ .setDescribedby("https://example.com/schemas/article")
+ .setTitle("Article 1")
+ .setType("application/vnd.api+json")
+ .setHreflang("en")
+ .setMeta(map("k", "v"));
+ assertJsonRoundTrip(link, JsonApiLink.class);
+ }
+
+ @Test
+ void hrefConstructor_andToString() {
+ var link = new JsonApiLink("/u");
+ assertEquals("/u", link.getHref());
+ assertNotNull(link.toString());
+ }
+
+ @Test
+ void emptyConstructor_allFieldsNull() {
+ var l = new JsonApiLink();
+ assertNull(l.getHref());
+ assertNull(l.getRel());
+ assertNull(l.getDescribedby());
+ assertNull(l.getTitle());
+ assertNull(l.getType());
+ assertNull(l.getHreflang());
+ assertNull(l.getMeta());
+ }
+ }
+
+ @Nested
+ class B_Version {
+
+ @Test
+ void roundTrip() {
+ var v = new JsonApiVersion("1.1").setMeta(map("k", "v"));
+ assertJsonRoundTrip(v, JsonApiVersion.class);
+ assertEquals("1.1", v.getVersion());
+ }
+
+ @Test
+ void emptyConstructor() {
+ var v = new JsonApiVersion();
+ assertNull(v.getVersion());
+ assertNull(v.getMeta());
+ assertNotNull(v.toString());
+ }
+ }
+
+ @Nested
+ class C_ResourceIdentifier {
+
+ @Test
+ void roundTrip() {
+ var rid = new JsonApiResourceIdentifier("people", "9").setMeta(map("k", "v"));
+ assertJsonRoundTrip(rid, JsonApiResourceIdentifier.class);
+ }
+
+ @Test
+ void settersAndDefaultConstructor() {
+ var rid = new JsonApiResourceIdentifier()
+ .setType("articles")
+ .setId("1")
+ .setMeta(map("m", 1));
+ assertEquals("articles", rid.getType());
+ assertEquals("1", rid.getId());
+ assertNotNull(rid.getMeta());
+ assertNotNull(rid.toString());
+ }
+ }
+
+ @Nested
+ class D_Relationship {
+
+ @Test
+ void singleIdentifierData_roundTrip() {
+ var rel = new JsonApiRelationship()
+ .setData(new JsonApiResourceIdentifier("people", "9"))
+ .setLinks(new LinkedHashMap<>(Map.of("self", "/articles/1/relationships/author")))
+ .setMeta(map("k", "v"));
+ assertJsonRoundTrip(rel, JsonApiRelationship.class);
+ assertNotNull(rel.getData());
+ }
+
+ @Test
+ void manyIdentifiersData_roundTrip() {
+ var rel = new JsonApiRelationship()
+ .setData(list(
+ new JsonApiResourceIdentifier("comments", "5"),
+ new JsonApiResourceIdentifier("comments", "12")
+ ));
+ assertJsonRoundTrip(rel, JsonApiRelationship.class);
+ }
+
+ @Test
+ void linksAcceptStringOrLinkObject() {
+ var rel = new JsonApiRelationship()
+ .setLinks(new LinkedHashMap<>(Map.of(
+ "self", "/articles/1/relationships/author",
+ "related", new JsonApiLink("/articles/1/author").setTitle("Author")
+ )));
+ var j = SER.serialize(rel);
+ // One value should be a string, the other a JSON object.
+ assertTrue(j.contains("\"self\":\"/articles/1/relationships/author\""), () -> j);
+ assertTrue(j.contains("\"related\":{"), () -> j);
+ var back = PAR.parse(j, JsonApiRelationship.class);
+ assertInstanceOf(String.class, back.getLinks().get("self"));
+ assertInstanceOf(JsonApiLink.class, back.getLinks().get("related"));
+ }
+
+ @Test
+ void nullData_isPreserved() {
+ var rel = new JsonApiRelationship().setData((Object) null).setMeta(map("k", "v"));
+ assertNull(rel.getData());
+ assertJsonRoundTrip(rel, JsonApiRelationship.class);
+ }
+
+ @Test
+ void toString_doesNotThrow() {
+ assertNotNull(new JsonApiRelationship().toString());
+ }
+ }
+
+ @Nested
+ class E_ErrorAndSource {
+
+ @Test
+ void allFields_roundTrip() {
+ var src = new JsonApiErrorSource()
+ .setPointer("/data/attributes/title")
+ .setParameter("filter[title]")
+ .setHeader("If-Match");
+ var err = new JsonApiError()
+ .setId("err-1")
+ .setLinks(new LinkedHashMap<>(Map.of("about", "/errors/err-1")))
+ .setStatus("422")
+ .setCode("E422")
+ .setTitle("Invalid Attribute")
+ .setDetail("Title must contain at least three characters.")
+ .setSource(src)
+ .setMeta(map("k", "v"));
+ assertJsonRoundTrip(err, JsonApiError.class);
+ }
+
+ @Test
+ void source_emptyConstructorAndToString() {
+ var s = new JsonApiErrorSource();
+ assertNull(s.getPointer());
+ assertNull(s.getParameter());
+ assertNull(s.getHeader());
+ assertNotNull(s.toString());
+ }
+
+ @Test
+ void error_emptyConstructorAndToString() {
+ var e = new JsonApiError();
+ assertNull(e.getId());
+ assertNull(e.getLinks());
+ assertNull(e.getStatus());
+ assertNull(e.getCode());
+ assertNull(e.getTitle());
+ assertNull(e.getDetail());
+ assertNull(e.getSource());
+ assertNull(e.getMeta());
+ assertNotNull(e.toString());
+ }
+ }
+
+ @Nested
+ class F_Resource {
+
+ @Test
+ void allFields_roundTrip() {
+ var res = new JsonApiResource("articles", "1")
+ .setAttributes(JsonMap.of("title", "JSON:API paints my bikeshed!"))
+ .putAttribute("body", "lorem")
+ .putRelationship("author",
+ new JsonApiRelationship().setData(new JsonApiResourceIdentifier("people", "9"))
+ )
+ .setLinks(new LinkedHashMap<>(Map.of("self", "/articles/1")))
+ .setMeta(map("rev", "abc"));
+ assertJsonRoundTrip(res, JsonApiResource.class);
+ }
+
+ @Test
+ void putAttribute_setsBackingMapIfNull() {
+ var r = new JsonApiResource().putAttribute("k", "v");
+ assertEquals("v", r.getAttributes().get("k"));
+ }
+
+ @Test
+ void putRelationship_setsBackingMapIfNull() {
+ var r = new JsonApiResource().putRelationship("author",
+ new JsonApiRelationship().setData(new JsonApiResourceIdentifier("people", "9")));
+ assertNotNull(r.getRelationships().get("author"));
+ }
+
+ @Test
+ void typeIsPlainString_notDiscriminator() {
+ // JSON:API ‘type’ is just data. Round-tripping a custom value must not require a Java dictionary entry.
+ var r = new JsonApiResource("very-custom-resource-not-in-any-registry", "1");
+ var j = SER.serialize(r);
+ assertTrue(j.contains("\"type\":\"very-custom-resource-not-in-any-registry\""), () -> j);
+ var back = PAR.parse(j, JsonApiResource.class);
+ assertEquals("very-custom-resource-not-in-any-registry", back.getType());
+ }
+
+ @Test
+ void settersAndDefaultConstructor() {
+ var r = new JsonApiResource()
+ .setType("a")
+ .setId("1")
+ .setAttributes(map("k", "v"))
+ .setRelationships(map("r", new JsonApiRelationship()))
+ .setLinks(new LinkedHashMap<>(Map.of("self", "/a/1")))
+ .setMeta(map("m", 1));
+ assertEquals("a", r.getType());
+ assertEquals("1", r.getId());
+ assertNotNull(r.getAttributes());
+ assertNotNull(r.getRelationships());
+ assertNotNull(r.getLinks());
+ assertNotNull(r.getMeta());
+ assertNotNull(r.toString());
+ }
+ }
+
+ @Nested
+ class G_Document {
+
+ @Test
+ void singleResource_roundTrip() {
+ var doc = new JsonApiDocument()
+ .setData(new JsonApiResource("articles", "1"))
+ .setJsonapi(new JsonApiVersion("1.1"));
+ assertJsonRoundTrip(doc, JsonApiDocument.class);
+ }
+
+ @Test
+ void manyResources_roundTrip() {
+ var doc = new JsonApiDocument().setData(list(
+ new JsonApiResource("articles", "1"),
+ new JsonApiResource("articles", "2")
+ ));
+ assertJsonRoundTrip(doc, JsonApiDocument.class);
+ }
+
+ @Test
+ void errorsDocument_roundTrip() {
+ var doc = new JsonApiDocument().addErrors(
+ new JsonApiError().setStatus("422").setTitle("Bad")
+ );
+ assertJsonRoundTrip(doc, JsonApiDocument.class);
+ }
+
+ @Test
+ void compoundDocumentExample_roundTrip() {
+ // Spec example: top-level data + included.
+ var author = new JsonApiResource("people", "9")
+ .setAttributes(JsonMap.of("firstName", "Dan", "lastName", "Gebhardt"))
+ .setLinks(new LinkedHashMap<>(Map.of("self", "/people/9")));
+ var comment1 = new JsonApiResource("comments", "5")
+ .setAttributes(JsonMap.of("body", "First!"));
+ var comment2 = new JsonApiResource("comments", "12")
+ .setAttributes(JsonMap.of("body", "I like XML better"));
+ var article = new JsonApiResource("articles", "1")
+ .setAttributes(JsonMap.of("title", "JSON:API paints my bikeshed!"))
+ .putRelationship("author",
+ new JsonApiRelationship()
+ .setData(new JsonApiResourceIdentifier("people", "9"))
+ .setLinks(new LinkedHashMap<>(Map.of(
+ "self", "/articles/1/relationships/author",
+ "related", "/articles/1/author"
+ )))
+ )
+ .putRelationship("comments",
+ new JsonApiRelationship().setData(list(
+ new JsonApiResourceIdentifier("comments", "5"),
+ new JsonApiResourceIdentifier("comments", "12")
+ ))
+ )
+ .setLinks(new LinkedHashMap<>(Map.of("self", "/articles/1")));
+ var doc = new JsonApiDocument()
+ .setLinks(new LinkedHashMap<>(Map.of("self", "/articles")))
+ .setJsonapi(new JsonApiVersion("1.1"))
+ .setData(article)
+ .addIncluded(author, comment1, comment2)
+ .setMeta(map("count", 1));
+ assertJsonRoundTrip(doc, JsonApiDocument.class);
+ }
+
+ @Test
+ void setData_allOverloads() {
+ var doc1 = new JsonApiDocument().setData((Object) "x");
+ assertEquals("x", doc1.getData());
+ var doc2 = new JsonApiDocument().setData(new JsonApiResource("a", "1"));
+ assertInstanceOf(JsonApiResource.class, doc2.getData());
+ var doc3 = new JsonApiDocument().setData(list(new JsonApiResource("a", "1")));
+ assertInstanceOf(List.class, doc3.getData());
+ }
+
+ @Test
+ void setIncludedAndErrors_replacesBackingList() {
+ var doc = new JsonApiDocument()
+ .setIncluded(list(new JsonApiResource("a", "1")))
+ .setErrors(list(new JsonApiError().setTitle("x")));
+ assertEquals(1, doc.getIncluded().size());
+ assertEquals(1, doc.getErrors().size());
+ }
+
+ @Test
+ void validate_throwsWhenDataAndErrorsCoexist() {
+ var doc = new JsonApiDocument()
+ .setData(new JsonApiResource("a", "1"))
+ .addErrors(new JsonApiError());
+ assertThrows(IllegalStateException.class, doc::validate);
+ }
+
+ @Test
+ void validate_throwsWhenEmpty() {
+ var doc = new JsonApiDocument();
+ assertThrows(IllegalStateException.class, doc::validate);
+ }
+
+ @Test
+ void validate_okWithJustMeta() {
+ var doc = new JsonApiDocument().setMeta(map("k", "v"));
+ assertSame(doc, doc.validate());
+ }
+
+ @Test
+ void validate_okWithJustData() {
+ var doc = new JsonApiDocument().setData(new JsonApiResource("a", "1"));
+ assertSame(doc, doc.validate());
+ }
+
+ @Test
+ void validate_okWithJustErrors() {
+ var doc = new JsonApiDocument().addErrors(new JsonApiError().setTitle("x"));
+ assertSame(doc, doc.validate());
+ }
+
+ @Test
+ void addErrors_andAddIncluded_appendToBackingLists() {
+ var doc = new JsonApiDocument()
+ .addErrors(new JsonApiError().setTitle("a"))
+ .addErrors(new JsonApiError().setTitle("b"));
+ assertEquals(2, doc.getErrors().size());
+ doc.addIncluded(new JsonApiResource("a", "1")).addIncluded(new JsonApiResource("a", "2"));
+ assertEquals(2, doc.getIncluded().size());
+ }
+
+ @Test
+ void links_emptyConstructorTrip() {
+ var d = new JsonApiDocument();
+ assertNull(d.getLinks());
+ assertNull(d.getMeta());
+ assertNull(d.getJsonapi());
+ assertNull(d.getData());
+ assertNull(d.getErrors());
+ assertNull(d.getIncluded());
+ assertNotNull(d.toString());
+ }
+ }
+
+ @Nested
+ class H_LinkSwap_directInvocation {
+
+ @Test
+ void unswap_nullIsNull() {
+ var swap = new JsonApiLinkOrStringSwap();
+ assertNull(swap.unswap(null, null, null));
+ }
+
+ @Test
+ void swap_isIdentity() {
+ var swap = new JsonApiLinkOrStringSwap();
+ var input = new Object();
+ assertSame(input, swap.swap(null, input));
+ }
+
+ @Test
+ void unswap_nonMapPassesThrough() {
+ var swap = new JsonApiLinkOrStringSwap();
+ var input = "raw-string";
+ assertSame(input, swap.unswap(null, input, null));
+ }
+
+ @Test
+ void unswap_mapWithScalarEntry_passesValueThrough() {
+ // A nested map (object) that itself contains a scalar at the "x" key still parses to a JsonApiLink
+ // (the per-entry rule); a more direct scalar-passthrough is hit via the JsonApiDocument case below.
+ var json = "{\"links\":{\"self\":\"/x\"}}";
+ var rel = JsonParser.DEFAULT.parse(json, JsonApiRelationship.class);
+ assertEquals("/x", rel.getLinks().get("self"));
+ }
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonpatch/JsonPatch_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonpatch/JsonPatch_RoundTrip_Test.java
new file mode 100644
index 0000000000..b6d999b59f
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonpatch/JsonPatch_RoundTrip_Test.java
@@ -0,0 +1,236 @@
+/*
+ * 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.bean.jsonpatch;
+
+import static org.apache.juneau.commons.utils.CollectionUtils.list;
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@code juneau-bean-jsonpatch} wire beans.
+ */
+class JsonPatch_RoundTrip_Test {
+
+ private static final JsonSerializer SER =
+ JsonSerializer.create()
+ .addBeanTypes()
+ .addRootType()
+ .typePropertyName(JsonPatchOperation.class, "op")
+ .build();
+
+ private static final JsonParser PAR =
+ JsonParser.create()
+ .typePropertyName(JsonPatchOperation.class, "op")
+ .build();
+
+ private static void assertJsonRoundTrip(Object bean, Class<?> type) {
+ var j1 = SER.serialize(bean);
+ var copy = PAR.parse(j1, type);
+ var j2 = SER.serialize(copy);
+ assertEquals(j1, j2, () -> "Round-trip JSON mismatch for " + type.getName() + ": " + j1);
+ }
+
+ @Nested
+ class A_IndividualOps {
+
+ @Test
+ void addOp_roundTrip() {
+ var op = new AddOp("/a/b/c", JsonMap.of("k", "v"));
+ assertJsonRoundTrip(op, JsonPatchOperation.class);
+ }
+
+ @Test
+ void removeOp_roundTrip() {
+ var op = new RemoveOp("/a/b/c");
+ assertJsonRoundTrip(op, JsonPatchOperation.class);
+ }
+
+ @Test
+ void replaceOp_roundTrip() {
+ var op = new ReplaceOp("/a/b/c", 42);
+ assertJsonRoundTrip(op, JsonPatchOperation.class);
+ }
+
+ @Test
+ void moveOp_roundTrip() {
+ var op = new MoveOp("/a/b/c", "/a/b/d");
+ assertJsonRoundTrip(op, JsonPatchOperation.class);
+ }
+
+ @Test
+ void copyOp_roundTrip() {
+ var op = new CopyOp("/a/b/c", "/a/b/e");
+ assertJsonRoundTrip(op, JsonPatchOperation.class);
+ }
+
+ @Test
+ void testOp_roundTrip() {
+ var op = new TestOp("/a/b/c", "foo");
+ assertJsonRoundTrip(op, JsonPatchOperation.class);
+ }
+ }
+
+ @Nested
+ class B_OpsContainNullValuesAndDefaultConstructors {
+
+ @Test
+ void addOp_defaultsAreNull() {
+ var op = new AddOp();
+ assertNull(op.getPath());
+ assertNull(op.getValue());
+ op.setValue("v").setPath("/x");
+ assertEquals("/x", op.getPath());
+ assertEquals("v", op.getValue());
+ }
+
+ @Test
+ void removeOp_defaultsAreNull() {
+ var op = new RemoveOp();
+ assertNull(op.getPath());
+ op.setPath("/x");
+ assertEquals("/x", op.getPath());
+ }
+
+ @Test
+ void replaceOp_defaultsAreNull() {
+ var op = new ReplaceOp();
+ assertNull(op.getPath());
+ assertNull(op.getValue());
+ op.setValue(99).setPath("/x");
+ assertEquals("/x", op.getPath());
+ assertEquals(99, op.getValue());
+ }
+
+ @Test
+ void moveOp_defaultsAreNull() {
+ var op = new MoveOp();
+ assertNull(op.getPath());
+ assertNull(op.getFrom());
+ op.setFrom("/y").setPath("/x");
+ assertEquals("/x", op.getPath());
+ assertEquals("/y", op.getFrom());
+ }
+
+ @Test
+ void copyOp_defaultsAreNull() {
+ var op = new CopyOp();
+ assertNull(op.getPath());
+ assertNull(op.getFrom());
+ op.setFrom("/y").setPath("/x");
+ assertEquals("/x", op.getPath());
+ assertEquals("/y", op.getFrom());
+ }
+
+ @Test
+ void testOp_defaultsAreNull() {
+ var op = new TestOp();
+ assertNull(op.getPath());
+ assertNull(op.getValue());
+ op.setValue("v").setPath("/x");
+ assertEquals("/x", op.getPath());
+ assertEquals("v", op.getValue());
+ }
+ }
+
+ @Nested
+ class C_PolymorphicDispatch {
+
+ @Test
+ void allSixOps_roundTripViaPatchDocument() {
+ var patch = new JsonPatch()
+ .append(new AddOp("/a/b/c", "foo"))
+ .append(new RemoveOp("/a/b/c"))
+ .append(new ReplaceOp("/a/b/c", 42))
+ .append(new MoveOp("/a/b/c", "/a/b/d"))
+ .append(new CopyOp("/a/b/c", "/a/b/e"))
+ .append(new TestOp("/a/b/c", "foo"));
+ assertJsonRoundTrip(patch, JsonPatch.class);
+ }
+
+ @Test
+ void parserPicksRightSubclassForEachOp() {
+ var patch = new JsonPatch()
+ .append(new AddOp("/a", 1))
+ .append(new RemoveOp("/b"))
+ .append(new ReplaceOp("/c", "x"))
+ .append(new MoveOp("/d", "/e"))
+ .append(new CopyOp("/f", "/g"))
+ .append(new TestOp("/h", true));
+ var j = SER.serialize(patch);
+ var back = PAR.parse(j, JsonPatch.class);
+ assertEquals(6, back.size());
+ assertInstanceOf(AddOp.class, back.get(0));
+ assertInstanceOf(RemoveOp.class, back.get(1));
+ assertInstanceOf(ReplaceOp.class, back.get(2));
+ assertInstanceOf(MoveOp.class, back.get(3));
+ assertInstanceOf(CopyOp.class, back.get(4));
+ assertInstanceOf(TestOp.class, back.get(5));
+ assertEquals("/a", back.get(0).getPath());
+ }
+
+ @Test
+ void wireFormat_isJsonArrayOfOperationObjects() {
+ var patch = new JsonPatch().append(new AddOp("/x", 1));
+ var j = SER.serialize(patch);
+ assertTrue(j.startsWith("["), () -> "Top-level should be a JSON array: " + j);
+ assertTrue(j.contains("\"op\":\"add\""), () -> j);
+ assertTrue(j.contains("\"path\":\"/x\""), () -> j);
+ assertTrue(j.contains("\"value\":1"), () -> j);
+ }
+ }
+
+ @Nested
+ class D_PatchConstruction {
+
+ @Test
+ void varargsConstructor_andAppend() {
+ var p = new JsonPatch(new AddOp("/a", 1), new RemoveOp("/b"));
+ assertEquals(2, p.size());
+ p.append(new TestOp("/c", "v"));
+ assertEquals(3, p.size());
+ }
+
+ @Test
+ void emptyConstructor() {
+ var p = new JsonPatch();
+ assertTrue(p.isEmpty());
+ }
+
+ @Test
+ void roundTrips_aListReadFromTheSpec() {
+ // Mirrors the JSON Patch RFC 6902 §3 example.
+ var p = new JsonPatch().append(
+ new TestOp("/a/b/c", "foo"),
+ new RemoveOp("/a/b/c"),
+ new AddOp("/a/b/c", list("foo", "bar")),
+ new ReplaceOp("/a/b/c", 42),
+ new MoveOp("/a/b/d", "/a/b/c"),
+ new CopyOp("/a/b/e", "/a/b/d")
+ );
+ assertJsonRoundTrip(p, JsonPatch.class);
+ }
+
+ @Test
+ void operationToStringDoesNotThrow() {
+ assertNotNull(new AddOp("/x", 1).toString());
+ assertNotNull(new RemoveOp("/x").toString());
+ }
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java
index 7b0929b4b7..b212602e3e 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/jsonschema/JsonSchemaBeanGenerator_Test.java
@@ -96,7 +96,7 @@ class JsonSchemaBeanGenerator_Test extends TestBase {
}
@Test void a08_toBeanConversion() {
- var map = new JsonMap(Json5Map.ofText("{type:'integer',format:'int32','$comment':'c',deprecated:true}"));
+ var map = new JsonMap(Json5Map.ofString("{type:'integer',format:'int32','$comment':'c',deprecated:true}"));
var bean = JsonSchemaBeanGenerator.toBean(map);
assertEquals("integer", bean.getTypeAsJsonType().toString().toLowerCase(Locale.ROOT));
assertEquals("int32", bean.getFormat());
diff --git a/juneau-utest/src/test/java/org/apache/juneau/bean/rfc7807/Problem_RoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/bean/rfc7807/Problem_RoundTrip_Test.java
new file mode 100644
index 0000000000..0b86db6275
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/bean/rfc7807/Problem_RoundTrip_Test.java
@@ -0,0 +1,276 @@
+/*
+ * 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.bean.rfc7807;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.net.*;
+
+import org.apache.juneau.http.header.*;
+import org.apache.juneau.json.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Round-trip tests for {@link Problem} (RFC 7807 {@code application/problem+json}).
+ */
+class Problem_RoundTrip_Test {
+
+ private static final JsonSerializer SER = JsonSerializer.DEFAULT;
+ private static final JsonSerializer READABLE = JsonSerializer.DEFAULT_READABLE;
+ private static final JsonParser PAR = JsonParser.DEFAULT;
+
+ @Nested
+ class A_basicTests {
+
+ @Test
+ void a01_basicProperties() throws Exception {
+ var a = new Problem()
+ .setType(new URI("https://example.com/probs/out-of-credit"))
+ .setTitle("You do not have enough credit.")
+ .setStatus(403)
+ .setDetail("Your current balance is 30, but that costs 50.")
+ .setInstance(new URI("/account/12345/msgs/abc"));
+
+ assertEquals(new URI("https://example.com/probs/out-of-credit"), a.getType());
+ assertEquals("You do not have enough credit.", a.getTitle());
+ assertEquals(Integer.valueOf(403), a.getStatus());
+ assertEquals("Your current balance is 30, but that costs 50.", a.getDetail());
+ assertEquals(new URI("/account/12345/msgs/abc"), a.getInstance());
+ }
+
+ @Test
+ void a02_fluentSetters_returnThis() {
+ var a = new Problem();
+ assertSame(a, a.setType(null));
+ assertSame(a, a.setTitle(null));
+ assertSame(a, a.setStatus(null));
+ assertSame(a, a.setDetail(null));
+ assertSame(a, a.setInstance(null));
+ assertSame(a, a.set("k", "v"));
+ }
+
+ @Test
+ void a03_typeNullByDefault() {
+ var a = new Problem();
+ assertNull(a.getType());
+ assertNull(a.getTitle());
+ assertNull(a.getStatus());
+ assertNull(a.getDetail());
+ assertNull(a.getInstance());
+ }
+
+ @Test
+ void a04_typeOrDefault_returnsAboutBlank_whenUnset() {
+ var a = new Problem();
+ assertEquals(URI.create("about:blank"), a.getTypeOrDefault());
+ assertEquals(Problem.DEFAULT_TYPE, a.getTypeOrDefault());
+ }
+
+ @Test
+ void a05_typeOrDefault_returnsSetValue_whenPresent() throws Exception {
+ var a = new Problem().setType(new URI("https://example.com/x"));
+ assertEquals(new URI("https://example.com/x"), a.getTypeOrDefault());
+ }
+
+ @Test
+ void a06_fromStatus_populatesThreeFields() {
+ var a = Problem.fromStatus(404, "Not Found", "Resource missing");
+ assertNull(a.getType());
+ assertEquals("Not Found", a.getTitle());
+ assertEquals(Integer.valueOf(404), a.getStatus());
+ assertEquals("Resource missing", a.getDetail());
+ assertNull(a.getInstance());
+ }
+
+ @Test
+ void a07_fromStatus_handlesNullStrings() {
+ var a = Problem.fromStatus(500, null, null);
+ assertEquals(Integer.valueOf(500), a.getStatus());
+ assertNull(a.getTitle());
+ assertNull(a.getDetail());
+ }
+ }
+
+ @Nested
+ class B_serialization {
+
+ @Test
+ void b01_allFiveFields_roundTripReadable() throws Exception {
+ var a = new Problem()
+ .setType(new URI("https://example.com/probs/out-of-credit"))
+ .setTitle("You do not have enough credit.")
+ .setStatus(403)
+ .setDetail("Your current balance is 30, but that costs 50.")
+ .setInstance(new URI("/account/12345/msgs/abc"));
+
+ var j1 = READABLE.serialize(a);
+ var copy = PAR.parse(j1, Problem.class);
+ assertEquals(a.getType(), copy.getType());
+ assertEquals(a.getTitle(), copy.getTitle());
+ assertEquals(a.getStatus(), copy.getStatus());
+ assertEquals(a.getDetail(), copy.getDetail());
+ assertEquals(a.getInstance(), copy.getInstance());
+ var j2 = READABLE.serialize(copy);
+ assertEquals(j1, j2);
+ }
+
+ @Test
+ void b02_allFiveFields_roundTripCompact() throws Exception {
+ var a = new Problem()
+ .setType(new URI("https://example.com/x"))
+ .setTitle("t")
+ .setStatus(400)
+ .setDetail("d")
+ .setInstance(new URI("/i"));
+
+ var j1 = SER.serialize(a);
+ assertEquals(
+ "{\"detail\":\"d\",\"instance\":\"/i\",\"status\":400,\"title\":\"t\",\"type\":\"https://example.com/x\"}",
+ j1
+ );
+ var copy = PAR.parse(j1, Problem.class);
+ assertEquals(j1, SER.serialize(copy));
+ }
+
+ @Test
+ void b03_emptyProblem_serializesAsEmptyObject() {
+ var a = new Problem();
+ assertEquals("{}", SER.serialize(a));
+ }
+
+ @Test
+ void b04_typeAbsent_doesNotAppearInJson() {
+ var a = new Problem().setTitle("t").setStatus(500);
+ var json = SER.serialize(a);
+ assertFalse(json.contains("type"), () -> "JSON should not contain 'type': " + json);
+ assertFalse(json.contains("about:blank"), () -> "JSON should not contain 'about:blank': " + json);
+ }
+
+ @Test
+ void b05_typeOrDefault_doesNotMutateField() {
+ var a = new Problem().setTitle("t");
+ a.getTypeOrDefault();
+ assertNull(a.getType(), "getTypeOrDefault() must not eagerly populate the type field");
+ assertFalse(SER.serialize(a).contains("type"));
+ }
+
+ @Test
+ void b06_fromStatus_exactJson_404() {
+ var a = Problem.fromStatus(404, "Not Found", "Resource missing");
+ assertEquals(
+ "{\"detail\":\"Resource missing\",\"status\":404,\"title\":\"Not Found\"}",
+ SER.serialize(a)
+ );
+ }
+
+ @Test
+ void b07_fromStatus_exactJson_500() {
+ var a = Problem.fromStatus(500, "Internal Server Error", "boom");
+ assertEquals(
+ "{\"detail\":\"boom\",\"status\":500,\"title\":\"Internal Server Error\"}",
+ SER.serialize(a)
+ );
+ }
+
+ @Test
+ void b08_statusOmitted_whenNull() {
+ var a = new Problem().setTitle("t");
+ var json = SER.serialize(a);
+ assertEquals("{\"title\":\"t\"}", json);
+ }
+
+ @Test
+ void b09_contentType_constant() {
+ assertEquals("application/problem+json", ContentType.APPLICATION_PROBLEM_JSON.getValue());
+ assertEquals("application/problem+xml", ContentType.APPLICATION_PROBLEM_XML.getValue());
+ }
+
+ @Test
+ void b10_contentType_roundTripsWhenSetExplicitly() {
+ var ct = ContentType.of("application/problem+json");
+ assertEquals(ContentType.APPLICATION_PROBLEM_JSON.getValue(), ct.getValue());
+ }
+ }
+
+ @Nested
+ class C_extraProperties {
+
+ @Test
+ void c01_extensionsFlatten_intoTopLevelJson() throws Exception {
+ var a = new Problem()
+ .setType(new URI("https://example.com/x"))
+ .setTitle("t")
+ .set("balance", 30)
+ .set("accounts", new String[]{"/account/12345", "/account/67890"});
+
+ var json = SER.serialize(a);
+ assertTrue(json.contains("\"balance\":30"), () -> "Missing flat balance: " + json);
+ assertTrue(json.contains("\"accounts\":[\"/account/12345\",\"/account/67890\"]"),
+ () -> "Missing flat accounts: " + json);
+ assertFalse(json.contains("\"extra\""), () -> "Extension members must not nest under 'extra': " + json);
+ assertFalse(json.contains("\"extensions\""),
+ () -> "Extension members must not nest under 'extensions': " + json);
+ }
+
+ @Test
+ void c02_extensionsRoundTrip_backIntoExtraMap() throws Exception {
+ var a = new Problem()
+ .setType(new URI("https://example.com/x"))
+ .setTitle("You do not have enough credit.")
+ .setStatus(403)
+ .set("balance", 30)
+ .set("accountId", "abc");
+
+ var json = SER.serialize(a);
+ var copy = PAR.parse(json, Problem.class);
+
+ assertEquals(30, ((Number) copy.get("balance")).intValue());
+ assertEquals("abc", copy.get("accountId"));
+ assertTrue(copy.extraKeys().contains("balance"));
+ assertTrue(copy.extraKeys().contains("accountId"));
+ assertEquals(json, SER.serialize(copy));
+ }
+
+ @Test
+ void c03_extraKeys_emptyOnFreshBean() {
+ var a = new Problem();
+ assertTrue(a.extraKeys().isEmpty());
+ assertNull(a.get("anything"));
+ }
+
+ @Test
+ void c04_extensionsRoundTripFromJson_intoExtra() throws Exception {
+ var json = "{\"type\":\"https://example.com/x\",\"title\":\"t\",\"status\":403,"
+ + "\"detail\":\"d\",\"instance\":\"/i\",\"balance\":30,\"answer\":42}";
+ var a = PAR.parse(json, Problem.class);
+
+ assertEquals(new URI("https://example.com/x"), a.getType());
+ assertEquals("t", a.getTitle());
+ assertEquals(Integer.valueOf(403), a.getStatus());
+ assertEquals("d", a.getDetail());
+ assertEquals(new URI("/i"), a.getInstance());
+ assertEquals(30, ((Number) a.get("balance")).intValue());
+ assertEquals(42, ((Number) a.get("answer")).intValue());
+ }
+
+ @Test
+ void c05_extensionsSetTwice_overwritesPreviousValue() {
+ var a = new Problem().set("k", "v1").set("k", "v2");
+ assertEquals("v2", a.get("k"));
+ }
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java
index 22fc4a79ef..4cd4d15147 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborCompliance_Test.java
@@ -90,7 +90,7 @@ class CborCompliance_Test extends TestBase {
void g17_simpleArray() throws Exception { enc(ints(1, 2, 3), "83 01 02 03"); }
@Test
- void g18_emptyMap() throws Exception { enc(JsonMap.ofText("{}"), "A0"); }
+ void g18_emptyMap() throws Exception { enc(JsonMap.ofString("{}"), "A0"); }
@Test
void g19_false() throws Exception { enc(false, "F4"); }
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java
index 3666a90edc..602ce1b63a 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborOutputStream_Test.java
@@ -219,12 +219,12 @@ class CborOutputStream_Test extends TestBase {
@Test
void a29_emptyMap() throws Exception {
- enc(JsonMap.ofText("{}"), "A0");
+ enc(JsonMap.ofString("{}"), "A0");
}
@Test
void a30_shortMap() throws Exception {
- enc(JsonMap.ofText("{\"a\":1,\"b\":2}"), "A2 61 61 01 61 62 02");
+ enc(JsonMap.ofString("{\"a\":1,\"b\":2}"), "A2 61 61 01 61 62 02");
}
@Test
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java
index 9c1f4a737f..5414907e9f 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborRoundTrip_Test.java
@@ -130,7 +130,7 @@ class CborRoundTrip_Test extends TestBase {
void e13_emptyCollectionsRoundTrip() throws Exception {
var emptyList = CborParser.DEFAULT.parse(CborSerializer.DEFAULT.serialize(list()), JsonList.class);
assertTrue(emptyList.isEmpty());
- var emptyMap = CborParser.DEFAULT.parse(CborSerializer.DEFAULT.serialize(JsonMap.ofText("{}")), JsonMap.class);
+ var emptyMap = CborParser.DEFAULT.parse(CborSerializer.DEFAULT.serialize(JsonMap.ofString("{}")), JsonMap.class);
assertTrue(emptyMap.isEmpty());
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java
index a9f69d806b..d37bd9daf6 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/cbor/CborSerializer_Test.java
@@ -55,7 +55,7 @@ class CborSerializer_Test extends TestBase {
test("a", "61 61");
test(ints(), "80");
test(ints(1, 2, 3), "83 01 02 03");
- test(JsonMap.ofText("{}"), "A0");
+ test(JsonMap.ofString("{}"), "A0");
}
public static class Person {
@@ -190,7 +190,7 @@ class CborSerializer_Test extends TestBase {
@Test
void c13_emptyBean() throws Exception {
- var bytes = CborSerializer.DEFAULT.serialize(JsonMap.ofText("{}"));
+ var bytes = CborSerializer.DEFAULT.serialize(JsonMap.ofString("{}"));
assertEquals("A0", toSpacedHex(bytes));
var parsed = CborParser.DEFAULT.parse(bytes, JsonMap.class);
assertTrue(parsed.isEmpty());
@@ -199,7 +199,7 @@ class CborSerializer_Test extends TestBase {
@Test
void c14_emptyCollections() throws Exception {
test(ints(), "80");
- test(JsonMap.ofText("{}"), "A0");
+ test(JsonMap.ofString("{}"), "A0");
}
@Test
diff --git a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java
index f3f639d1f6..79d4607089 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledList_Test.java
@@ -62,15 +62,21 @@ class MarshalledList_Test extends TestBase {
assertEquals("a", l.getString(0));
}
- @Test void a05_parseViaOfText() throws Exception {
- var l = MarshalledList.ofText("[1,2,3]", Json5Parser.DEFAULT);
+ @Test void a05_parseViaOfString() throws Exception {
+ var l = MarshalledList.ofString("[1,2,3]", Json5Parser.DEFAULT);
assertNotNull(l);
assertEquals(3, l.size());
assertEquals(2, l.getInt(1));
}
- @Test void a06_parseViaOfTextNullInput() throws Exception {
- assertNull(MarshalledList.ofText((CharSequence)null, Json5Parser.DEFAULT));
+ @Test void a06_parseViaOfStringNullInput() throws Exception {
+ assertNull(MarshalledList.ofString((CharSequence)null, Json5Parser.DEFAULT));
+ }
+
+ @Test void a06b_ofTextAliasStillWorks() throws Exception {
+ var l = MarshalledList.ofString("[1,2,3]", Json5Parser.DEFAULT);
+ assertNotNull(l);
+ assertEquals(3, l.size());
}
@Test void a07_getMapReturnsNeutralType() {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java
index cb78504194..3036e0578a 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/collections/MarshalledMap_Test.java
@@ -62,15 +62,21 @@ class MarshalledMap_Test extends TestBase {
assertEquals(1, m.getInt("a"));
}
- @Test void a05_parseViaOfText() throws Exception {
- var m = MarshalledMap.ofText("{a:1,b:'two'}", Json5Parser.DEFAULT);
+ @Test void a05_parseViaOfString() throws Exception {
+ var m = MarshalledMap.ofString("{a:1,b:'two'}", Json5Parser.DEFAULT);
assertNotNull(m);
assertEquals(1, m.getInt("a"));
assertEquals("two", m.getString("b"));
}
- @Test void a06_parseViaOfTextNullInput() throws Exception {
- assertNull(MarshalledMap.ofText((CharSequence)null, Json5Parser.DEFAULT));
+ @Test void a06_parseViaOfStringNullInput() throws Exception {
+ assertNull(MarshalledMap.ofString((CharSequence)null, Json5Parser.DEFAULT));
+ }
+
+ @Test void a06b_ofTextAliasStillWorks() throws Exception {
+ var m = MarshalledMap.ofString("{a:1}", Json5Parser.DEFAULT);
+ assertNotNull(m);
+ assertEquals(1, m.getInt("a"));
}
@Test void a07_getMapReturnsNeutralType() {
@@ -92,7 +98,7 @@ class MarshalledMap_Test extends TestBase {
}
@Test void a08b_parseProducesJson5MapInternally() throws Exception {
- var m = MarshalledMap.ofText("{nested:{x:1}}", Json5Parser.DEFAULT);
+ var m = MarshalledMap.ofString("{nested:{x:1}}", Json5Parser.DEFAULT);
var nested = m.getMap("nested");
assertTrue(nested instanceof Json5Map, "Json5Parser produces Json5Map for nested objects");
assertFalse(nested instanceof JsonMap, "Strict-JSON JsonMap is not used by Json5Parser");
@@ -158,7 +164,7 @@ class MarshalledMap_Test extends TestBase {
}
@Test void a17_castToMapInterfaceReturnsNeutralFallback() throws Exception {
- var m = MarshalledMap.ofText("{a:1,b:2}", Json5Parser.DEFAULT);
+ var m = MarshalledMap.ofString("{a:1,b:2}", Json5Parser.DEFAULT);
// Casting from a MarshalledMap to the Map interface should fall back to MarshalledMap.
Map<?,?> casted = m.cast(Map.class);
assertNotNull(casted);
diff --git a/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java b/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
index 09c9e7b640..e5535207fc 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/httppart/OpenApiPartSerializer_Test.java
@@ -799,8 +799,8 @@ class OpenApiPartSerializer_Test extends TestBase {
var ps = tObject().allowEmptyValue().build();
assertEquals("f1=1,f2=2,f3=true", serialize(ps, new H1("1",2,true)));
assertEquals("", serialize(ps, new H1(null,null,null)));
- assertEquals("f1=1,f2=2,f3=true", serialize(ps, Json5Map.ofText("{f1:'1',f2:2,f3:true}")));
- assertEquals("f1=null,f2=null,f3=null", serialize(ps, Json5Map.ofText("{f1:null,f2:null,f3:null}")));
+ assertEquals("f1=1,f2=2,f3=true", serialize(ps, Json5Map.ofString("{f1:'1',f2:2,f3:true}")));
+ assertEquals("f1=null,f2=null,f3=null", serialize(ps, Json5Map.ofString("{f1:null,f2:null,f3:null}")));
assertEquals("null", serialize(ps, null));
}
@@ -808,8 +808,8 @@ class OpenApiPartSerializer_Test extends TestBase {
var ps = T_OBJECT_UON;
assertEquals("(f1='1',f2=2,f3=true)", serialize(ps, new H1("1",2,true)));
assertEquals("()", serialize(ps, new H1(null,null,null)));
- assertEquals("(f1='1',f2=2,f3=true)", serialize(ps, Json5Map.ofText("{f1:'1',f2:2,f3:true}")));
- assertEquals("(f1=null,f2=null,f3=null)", serialize(ps, Json5Map.ofText("{f1:null,f2:null,f3:null}")));
+ assertEquals("(f1='1',f2=2,f3=true)", serialize(ps, Json5Map.ofString("{f1:'1',f2:2,f3:true}")));
+ assertEquals("(f1=null,f2=null,f3=null)", serialize(ps, Json5Map.ofString("{f1:null,f2:null,f3:null}")));
assertEquals("null", serialize(ps, null));
}
@@ -817,30 +817,30 @@ class OpenApiPartSerializer_Test extends TestBase {
var ps = tArray(tObject().allowEmptyValue()).build();
assertEquals("f1=1\\,f2=2\\,f3=true,,null", serialize(ps, a(new H1("1",2,true),new H1(null,null,null),null)));
assertEquals("f1=1\\,f2=2\\,f3=true,,null", serialize(ps, l(new H1("1",2,true),new H1(null,null,null),null)));
- assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
- assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
- assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
- assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
+ assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+ assertEquals("f1=1\\,f2=2\\,f3=true,f1=null\\,f2=null\\,f3=null,null", serialize(ps, l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+ assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
+ assertEquals("f1=1\\,f2=2\\,f3=true,f1=1\\,f2=2\\,f3=true,null", serialize(ps, l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
}
@Test void h03_objectType_2d_pipes() throws Exception {
var ps = tArrayPipes(tObject().allowEmptyValue()).build();
assertEquals("f1=1,f2=2,f3=true||null", serialize(ps, a(new H1("1",2,true),new H1(null,null,null),null)));
assertEquals("f1=1,f2=2,f3=true||null", serialize(ps, l(new H1("1",2,true),new H1(null,null,null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=null,f2=null,f3=null|null", serialize(ps, l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true|null", serialize(ps, l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
}
@Test void h04_objectType_2d_uon() throws Exception {
var ps = tArrayUon(tObject()).build();
assertEquals("@((f1='1',f2=2,f3=true),(),null)", serialize(ps, a(new H1("1",2,true),new H1(null,null,null),null)));
assertEquals("@((f1='1',f2=2,f3=true),(),null)", serialize(ps, l(new H1("1",2,true),new H1(null,null,null),null)));
- assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
- assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null)));
- assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
- assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}"),null)));
+ assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+ assertEquals("@((f1='1',f2=2,f3=true),(f1=null,f2=null,f3=null),null)", serialize(ps, l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null)));
+ assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
+ assertEquals("@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true),null)", serialize(ps, l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}"),null)));
}
@Test void h03_objectType_3d() throws Exception {
@@ -848,12 +848,12 @@ class OpenApiPartSerializer_Test extends TestBase {
assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=3\\\\\\,f3=false,\\,null,null", serialize(ps, a(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=3\\\\\\,f3=false,\\,null,null", serialize(ps, l(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=3\\\\\\,f3=false,\\,null,null", serialize(ps, l(l(new H1("1",2,true),new H1("x",3,false)),l(new H1(null,null,null),null),null)));
- assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=x\\\\\\,f2=4\\\\\\,f3=false,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1\\\\\\,f2=2\\\\\\,f3=true\\,f1=1\\\\\\,f2=2\\\\\\,f3=true,\\,f1=null\\\\\\,f2=null\\\\\\,f3=null\\,null,null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
}
@Test void h03_objectType_3d_ssvAndPipes() throws Exception {
@@ -863,12 +863,12 @@ class OpenApiPartSerializer_Test extends TestBase {
assertEquals("f1=1,f2=2,f3=true|f1=x,f2=3,f3=false |null null", serialize(ps, a(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
assertEquals("f1=1,f2=2,f3=true|f1=x,f2=3,f3=false |null null", serialize(ps, l(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
assertEquals("f1=1,f2=2,f3=true|f1=x,f2=3,f3=false |null null", serialize(ps, l(l(new H1("1",2,true),new H1("x",3,false)),l(new H1(null,null,null),null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, a(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, a(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=x,f2=4,f3=false f1=null,f2=null,f3=null|null null", serialize(ps, l(l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("f1=1,f2=2,f3=true|f1=1,f2=2,f3=true |f1=null,f2=null,f3=null|null null", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
}
@Test void h03_objectType_3d_uon() throws Exception {
@@ -876,12 +876,12 @@ class OpenApiPartSerializer_Test extends TestBase {
assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=3,f3=false)),@((),null),null)", serialize(ps, a(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=3,f3=false)),@((),null),null)", serialize(ps, l(a(new H1("1",2,true),new H1("x",3,false)),a(new H1(null,null,null),null),null)));
assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=3,f3=false)),@((),null),null)", serialize(ps, l(l(new H1("1",2,true),new H1("x",3,false)),l(new H1(null,null,null),null),null)));
- assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, a(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(a(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(Json5Map.ofText("{f1:'1',f2:2,f3:true}"),Json5Map.ofText("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
- assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofText("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofText("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, a(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(a(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),a(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("@(@((f1='1',f2=2,f3=true),(f1=x,f2=4,f3=false)),@((f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(Json5Map.ofString("{f1:'1',f2:2,f3:true}"),Json5Map.ofString("{f1:'x',f2:4,f3:false}")),l(Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, a2(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(ao(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),ao(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
+ assertEquals("@(@((f1='1',f2=2,f3=true),(f1='1',f2=2,f3=true)),@((),(f1=null,f2=null,f3=null),null),null)", serialize(ps, l(l(new H1("1",2,true),Json5Map.ofString("{f1:'1',f2:2,f3:true}")),l(new H1(null,null,null),Json5Map.ofString("{f1:null,f2:null,f3:null}"),null),null)));
}
public static class H2 {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java b/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java
index 41993c1000..89da55123f 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/json/Json_Test.java
@@ -62,7 +62,7 @@ class Json_Test extends TestBase{
var r = "\\w[\\w\\-\\.]{3,}\\w";
m.put("x", r);
assertEquals("{\"x\":\"\\\\w[\\\\w\\\\-\\\\.]{3,}\\\\w\"}", s2.serialize(m));
- assertEquals(r, JsonMap.ofText(s2.serialize(m)).getString("x"));
+ assertEquals(r, JsonMap.ofString(s2.serialize(m)).getString("x"));
// String = [foo\bar]
m.clear();
@@ -138,7 +138,7 @@ class Json_Test extends TestBase{
// Indentation
//====================================================================================================
@Test void a04_indentation() throws Exception {
- var m = Json5Map.ofText("{J:{B:['c',{D:'e'},['f',{G:'h'},1,false]]},I:'j'}");
+ var m = Json5Map.ofString("{J:{B:['c',{D:'e'},['f',{G:'h'},1,false]]},I:'j'}");
var e = """
{
J: {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java
index 21b32ae3b0..63d4cb0b75 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5List_Test.java
@@ -39,8 +39,8 @@ class Json5List_Test extends TestBase {
assertEquals("[1,'two',true]", l.toString());
}
- @Test void a03_factoryOfText() throws Exception {
- var l = Json5List.ofText("[1,'two',true]");
+ @Test void a03_factoryOfString() throws Exception {
+ var l = Json5List.ofString("[1,'two',true]");
assertNotNull(l);
assertEquals(3, l.size());
assertEquals(1, l.getInt(0));
@@ -48,8 +48,14 @@ class Json5List_Test extends TestBase {
assertTrue(l.getBoolean(2));
}
- @Test void a04_factoryOfTextNullInput() throws Exception {
- assertNull(Json5List.ofText((CharSequence)null));
+ @Test void a04_factoryOfStringNullInput() throws Exception {
+ assertNull(Json5List.ofString((CharSequence)null));
+ }
+
+ @Test void a04b_ofTextAliasStillWorks() throws Exception {
+ var l = Json5List.ofString("[1,2,3]");
+ assertNotNull(l);
+ assertEquals(3, l.size());
}
@Test void a05_factoryCreateAndAppend() {
@@ -139,7 +145,7 @@ class Json5List_Test extends TestBase {
@Test void a17_roundTripJson5() throws Exception {
var original = Json5List.of(1, "two", true);
var serialized = original.toString();
- var parsed = Json5List.ofText(serialized);
+ var parsed = Json5List.ofString(serialized);
assertEquals(1, parsed.getInt(0));
assertEquals("two", parsed.getString(1));
assertTrue(parsed.getBoolean(2));
diff --git a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java
index 2b07a3dc28..31972cf992 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/json5/Json5Map_Test.java
@@ -31,7 +31,7 @@ import org.junit.jupiter.api.*;
* These confirm that:
* <ul>
* <li>{@link Json5Map#toString()} produces JSON5 form.
- * <li>JSON5 default-parser constructors and {@code ofText} factories work.
+ * <li>JSON5 default-parser constructors and {@code ofString} factories work.
* <li>Covariant overrides on the get/fluent surface return {@code Json5Map} / {@code Json5List}, not the neutral base.
* <li>{@code writeTo(Writer)} uses {@link Json5Serializer#DEFAULT}.
* </ul>
@@ -49,15 +49,21 @@ class Json5Map_Test extends TestBase {
assertEquals("{a:1,b:'two'}", m.toString());
}
- @Test void a03_factoryOfText() throws Exception {
- var m = Json5Map.ofText("{a:1,b:'two'}");
+ @Test void a03_factoryOfString() throws Exception {
+ var m = Json5Map.ofString("{a:1,b:'two'}");
assertNotNull(m);
assertEquals(1, m.getInt("a"));
assertEquals("two", m.getString("b"));
}
- @Test void a04_factoryOfTextNullInput() throws Exception {
- assertNull(Json5Map.ofText((CharSequence)null));
+ @Test void a04_factoryOfStringNullInput() throws Exception {
+ assertNull(Json5Map.ofString((CharSequence)null));
+ }
+
+ @Test void a04b_ofTextAliasStillWorks() throws Exception {
+ var m = Json5Map.ofString("{a:1}");
+ assertNotNull(m);
+ assertEquals(1, m.getInt("a"));
}
@Test void a05_factoryOfCreateAppend() {
@@ -180,7 +186,7 @@ class Json5Map_Test extends TestBase {
@Test void a21_roundTripJson5() throws Exception {
var original = Json5Map.of("a", 1, "b", "two", "c", true);
var serialized = original.toString();
- var parsed = Json5Map.ofText(serialized);
+ var parsed = Json5Map.ofString(serialized);
assertEquals(1, parsed.getInt("a"));
assertEquals("two", parsed.getString("b"));
assertTrue(parsed.getBoolean("c"));
diff --git a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackMediaType_Test.java b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackMediaType_Test.java
new file mode 100644
index 0000000000..955c208e89
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackMediaType_Test.java
@@ -0,0 +1,90 @@
+/*
+ * 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.msgpack;
+
+import static org.apache.juneau.commons.utils.StringUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Media type tests for MessagePack.
+ *
+ * <p>
+ * Verifies that the serializer emits the RFC-standard <c>application/msgpack</c> Content-Type and
+ * that the parser accepts both the new <c>application/msgpack</c> value and the legacy
+ * <c>octal/msgpack</c> alias (backward compatibility).
+ */
+class MsgPackMediaType_Test extends TestBase {
+
+ @Test
+ void i01_producesCorrectMediaType() {
+ var ct = MsgPackSerializer.DEFAULT.getResponseContentType();
+ assertEquals("application", ct.getType());
+ assertEquals("msgpack", ct.getSubType());
+ assertEquals("application/msgpack", ct.toString());
+ }
+
+ @Test
+ void i02_consumesApplicationMsgpack() {
+ var types = MsgPackParser.DEFAULT.getMediaTypes().stream()
+ .map(mt -> mt.getType() + "/" + mt.getSubType())
+ .toList();
+ assertTrue(types.contains("application/msgpack"),
+ "Expected parser to consume application/msgpack: " + types);
+ assertTrue(MsgPackParser.DEFAULT.canHandle("application/msgpack"),
+ "Expected canHandle(application/msgpack) to be true");
+ }
+
+ @Test
+ void i03_consumesOctalMsgpackAlias() {
+ var types = MsgPackParser.DEFAULT.getMediaTypes().stream()
+ .map(mt -> mt.getType() + "/" + mt.getSubType())
+ .toList();
+ assertTrue(types.contains("octal/msgpack"),
+ "Expected parser to retain octal/msgpack as a backward-compat alias: " + types);
+ assertTrue(MsgPackParser.DEFAULT.canHandle("octal/msgpack"),
+ "Expected canHandle(octal/msgpack) to be true");
+ }
+
+ @Test
+ void i04_serializerAcceptsBothMediaTypes() {
+ var types = new ArrayList<String>();
+ MsgPackSerializer.DEFAULT.forEachAcceptMediaType(mt -> types.add(mt.getType() + "/" + mt.getSubType()));
+ assertTrue(types.contains("application/msgpack"),
+ "Expected serializer Accept list to contain application/msgpack: " + types);
+ assertTrue(types.contains("octal/msgpack"),
+ "Expected serializer Accept list to retain octal/msgpack alias: " + types);
+ }
+
+ @Test
+ void i05_primaryMediaTypeIsApplicationMsgpack() {
+ assertEquals("application/msgpack", MsgPackSerializer.DEFAULT.getPrimaryMediaType().toString());
+ assertEquals("application/msgpack", MsgPackParser.DEFAULT.getPrimaryMediaType().toString());
+ }
+
+ @Test
+ void i06_parserStillDecodesWireData() throws Exception {
+ var bytes = fromSpacedHex("A5 68 65 6C 6C 6F");
+ var s = MsgPackParser.DEFAULT.parse(new ByteArrayInputStream(bytes), String.class);
+ assertEquals("hello", s);
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java
index a3e029a4dc..aa8c5423e2 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/msgpack/MsgPackSerializerTest.java
@@ -206,10 +206,10 @@ class MsgPackSerializerTest extends TestBase {
// * odd elements in objects are keys of a map
// * the next element of a key is its associated value
- test(Json5Map.ofText("{}"), "80");
- test(Json5Map.ofText("{1:1}"), "81 A1 31 01");
- test(Json5Map.ofText("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1}"), "8F A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01");
- test(Json5Map.ofText("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1,g:1}"), "DE 00 10 A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01 A1 67 01");
+ test(Json5Map.ofString("{}"), "80");
+ test(Json5Map.ofString("{1:1}"), "81 A1 31 01");
+ test(Json5Map.ofString("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1}"), "8F A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01");
+ test(Json5Map.ofString("{1:1,2:1,3:1,4:1,5:1,6:1,7:1,8:1,9:1,a:1,b:1,c:1,d:1,e:1,f:1,g:1}"), "DE 00 10 A1 31 01 A1 32 01 A1 33 01 A1 34 01 A1 35 01 A1 36 01 A1 37 01 A1 38 01 A1 39 01 A1 61 01 A1 62 01 A1 63 01 A1 64 01 A1 65 01 A1 66 01 A1 67 01");
}
public static class Person {
diff --git a/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java b/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
index d1448ace77..0cc23fca7a 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/objecttools/ObjectRest_Test.java
@@ -174,11 +174,11 @@ class ObjectRest_Test extends TestBase {
assertEquals("{}", s);
// Make sure doing a PUT against "" or "/" replaces the root object.
- var m2 = Json5Map.ofText("{x:1}");
+ var m2 = Json5Map.ofString("{x:1}");
model.put("", m2);
s = model.get("").toString();
assertEquals("{x:1}", s);
- m2 = Json5Map.ofText("{x:2}");
+ m2 = Json5Map.ofString("{x:2}");
model.put("/", m2);
s = model.get("").toString();
assertEquals("{x:2}", s);
@@ -325,8 +325,8 @@ class ObjectRest_Test extends TestBase {
//====================================================================================================
@Test void f01_getMethods() throws Exception {
var model = ObjectRest.create(new A());
- var l = Json5List.ofText("[{a:'b'}]");
- var m = Json5Map.ofText("{a:'b'}");
+ var l = Json5List.ofString("[{a:'b'}]");
+ var m = Json5Map.ofString("{a:'b'}");
var jm = new JsonMap(m);
var jl = new JsonList(l);
@@ -719,10 +719,10 @@ class ObjectRest_Test extends TestBase {
f3a = 3L;
f4a = true;
try {
- f5 = Json5Map.ofText("{f5a:'a'}");
- f6 = Json5List.ofText("[{f6a:'a'}]");
- f7 = Json5Map.ofText("{f5a:'a'}");
- f8 = Json5List.ofText("[{f6a:'a'}]");
+ f5 = Json5Map.ofString("{f5a:'a'}");
+ f6 = Json5List.ofString("[{f6a:'a'}]");
+ f7 = Json5Map.ofString("{f5a:'a'}");
+ f8 = Json5List.ofString("[{f6a:'a'}]");
} catch (ParseException e) {
throw new RuntimeException(e);
}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Logging_StreamClosed_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Logging_StreamClosed_Test.java
new file mode 100644
index 0000000000..00093ec7ec
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Logging_StreamClosed_Test.java
@@ -0,0 +1,244 @@
+/*
+ * 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.rest.client.classic;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.logging.*;
+
+import org.apache.http.*;
+import org.apache.http.entity.*;
+import org.apache.http.message.*;
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.rest.servlet.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Verifies the fix for a bug where calling {@code RestResponse.getContent().asString()}
+ * after the response is closed (e.g. inside or after try-with-resources) would throw
+ * a "Stream closed" {@link IOException} when {@link DetailLevel#FULL} request logging
+ * is enabled.
+ *
+ * <p>
+ * The root cause: {@code RestResponse.close()} previously consumed/closed the underlying
+ * Apache HTTP entity stream <i>before</i> running FULL logging. The FULL logging path
+ * then tried to read the same (now-closed) entity to render the body, leaving
+ * {@code ResponseContent.body} unset. A subsequent user call to
+ * {@code getContent().asString()} would re-attempt to read the closed stream and surface
+ * the exception to the caller.
+ *
+ * <p>
+ * {@link MockRestClient} alone is insufficient to reproduce the bug because it wraps the
+ * response in an {@code InputStreamEntity} backed by {@link ByteArrayInputStream}, whose
+ * {@link InputStream#close()} is a no-op — so even after close(), the stream can be
+ * re-read. The tests below inject a streaming entity whose underlying input stream
+ * faithfully throws "Stream closed" after close.
+ */
+class RestClient_Logging_StreamClosed_Test extends TestBase {
+
+ @Rest
+ public static class A extends BasicRestObject {
+ @RestGet
+ public String hello() {
+ return "hello world";
+ }
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Test helpers.
+ //------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * Streaming input stream that throws {@link IOException} on any read attempt after close.
+ * Mimics real HTTP client behavior (the Apache HTTP client closes the network stream when
+ * {@code EntityUtils.consume} runs, and subsequent reads fail).
+ */
+ private static final class FailingAfterCloseStream extends InputStream {
+ private final ByteArrayInputStream delegate;
+ private boolean closed;
+
+ FailingAfterCloseStream(byte[] data) {
+ delegate = new ByteArrayInputStream(data);
+ }
+
+ @Override public int read() throws IOException {
+ if (closed) throw new IOException("Stream closed");
+ return delegate.read();
+ }
+
+ @Override public int read(byte[] b, int off, int len) throws IOException {
+ if (closed) throw new IOException("Stream closed");
+ return delegate.read(b, off, len);
+ }
+
+ @Override public void close() throws IOException {
+ closed = true;
+ delegate.close();
+ }
+ }
+
+ /**
+ * Test client that lets us inject an arbitrary {@link HttpEntity} as the response body.
+ */
+ public static class TestClient extends MockRestClient {
+ private HttpEntity responseEntity;
+
+ public TestClient(MockRestClient.Builder builder) {
+ super(builder);
+ }
+
+ public TestClient entity(HttpEntity entity) {
+ responseEntity = entity;
+ return this;
+ }
+
+ @Override
+ protected MockRestResponse createResponse(RestRequest request, HttpResponse httpResponse, Parser parser) throws RestCallException {
+ var r = new BasicHttpResponse(new ProtocolVersion("http", 1, 1), 200, "");
+ r.setEntity(responseEntity);
+ r.addHeader("Content-Type", "application/json");
+ return new MockRestResponse(this, request, r, parser);
+ }
+ }
+
+ private static InputStreamEntity failingEntity(String body) {
+ var data = body.getBytes(StandardCharsets.UTF_8);
+ return new InputStreamEntity(new FailingAfterCloseStream(data));
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Reproduction / regression tests for the FULL logging stream-closed bug.
+ //------------------------------------------------------------------------------------------------------------------
+
+ /**
+ * After fix: closing the response and then calling {@code asString()} should
+ * return the body instead of throwing "Stream closed".
+ */
+ @Test void a01_asStringAfterCloseWithFullLogging() throws Exception {
+ var c = MockConsole.create();
+ var l = MockLogger.create();
+
+ var client = MockRestClient.create(A.class).json()
+ .logRequests(DetailLevel.FULL, Level.SEVERE, null)
+ .logToConsole()
+ .logger(l)
+ .console(c)
+ .build(TestClient.class)
+ .entity(failingEntity("\"hello world\""));
+
+ var response = client.get("/hello").run();
+ response.close();
+ var body = response.getContent().asString();
+ assertEquals("\"hello world\"", body);
+ }
+
+ /**
+ * After fix: try-with-resources + asString() works the same as before, without
+ * stream-closed errors, even with the failing stream wrapper.
+ */
+ @Test void a02_asStringInsideTryWithResources() throws Exception {
+ var c = MockConsole.create();
+ var l = MockLogger.create();
+
+ var client = MockRestClient.create(A.class).json()
+ .logRequests(DetailLevel.FULL, Level.SEVERE, null)
+ .logToConsole()
+ .logger(l)
+ .console(c)
+ .build(TestClient.class)
+ .entity(failingEntity("\"hello world\""));
+
+ try (var response = client.get("/hello").run()) {
+ var body = response.getContent().asString();
+ assertEquals("\"hello world\"", body);
+ }
+ }
+
+ /**
+ * After fix: FULL logging output still contains the response body (the buffering
+ * fix must not regress what FULL logging actually logs).
+ */
+ @Test void b01_fullLoggingStillContainsResponseBody() throws Exception {
+ var c = MockConsole.create();
+ c.reset();
+ var l = MockLogger.create();
+
+ var client = MockRestClient.create(A.class).json()
+ .logRequests(DetailLevel.FULL, Level.SEVERE, null)
+ .logToConsole()
+ .logger(l)
+ .console(c)
+ .build(TestClient.class)
+ .entity(failingEntity("\"hello world\""));
+
+ client.get("/hello").complete();
+
+ var logged = c.toString();
+ assertTrue(logged.contains("---response content---"), () -> "Missing response-content marker in: " + logged);
+ assertTrue(logged.contains("\"hello world\""), () -> "Missing response body in log output: " + logged);
+ }
+
+ /**
+ * After fix: when FULL logging is NOT enabled, the response body is NOT eagerly
+ * cached — original behavior preserved. Reading via {@code asString()} after
+ * {@code close()} should fail on a non-repeatable closed entity, just like before
+ * the fix. This guards against accidental over-buffering for the SIMPLE/NONE
+ * paths.
+ */
+ @Test void c01_nonFullLoggingDoesNotBufferEntity() throws Exception {
+ var l = MockLogger.create();
+
+ var client = MockRestClient.create(A.class).json()
+ .logRequests(DetailLevel.NONE, Level.SEVERE, null)
+ .logger(l)
+ .build(TestClient.class)
+ .entity(failingEntity("\"hello world\""));
+
+ var response = client.get("/hello").run();
+ response.close();
+ assertThrows(RestCallException.class, () -> response.getContent().asString());
+ }
+
+ /**
+ * After fix: SIMPLE logging continues to log just the status line (no body access).
+ */
+ @Test void c02_simpleLoggingDoesNotTouchBody() throws Exception {
+ var c = MockConsole.create();
+ c.reset();
+ var l = MockLogger.create();
+
+ var client = MockRestClient.create(A.class).json()
+ .logRequests(DetailLevel.SIMPLE, Level.SEVERE, null)
+ .logToConsole()
+ .logger(l)
+ .console(c)
+ .build(TestClient.class)
+ .entity(failingEntity("\"hello world\""));
+
+ client.get("/hello").complete();
+
+ var logged = c.toString();
+ assertTrue(logged.contains("HTTP GET"), () -> "Expected SIMPLE log line, got: " + logged);
+ assertFalse(logged.contains("---response content---"),
+ () -> "SIMPLE log should not contain body markers: " + logged);
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java
index f2b961b48d..386e3a8041 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/rest/client/classic/RestClient_Marshalls_Test.java
@@ -84,7 +84,7 @@ public class RestClient_Marshalls_Test extends TestBase {
x2.post("/a01",bean).header("X-Accept","text/xml").header("X-Content-Type","text/xml").run().assertStatus(200).getContent().as(Bean.class).check();
x3.post("/a01",bean).header("X-Accept","text/html").header("X-Content-Type","text/html").run().assertStatus(200).getContent().as(Bean.class).check();
x4.post("/a01",bean).header("X-Accept","text/plain").header("X-Content-Type","text/plain").run().assertStatus(200).getContent().as(Bean.class).check();
- x5.post("/a01",bean).header("X-Accept","octal/msgpack").header("X-Content-Type","octal/msgpack").run().assertStatus(200).getContent().as(Bean.class).check();
+ x5.post("/a01",bean).header("X-Accept","application/msgpack").header("X-Content-Type","application/msgpack").run().assertStatus(200).getContent().as(Bean.class).check();
x6.post("/a01",bean).header("X-Accept","text/uon").header("X-Content-Type","text/uon").run().assertStatus(200).getContent().as(Bean.class).check();
x7.post("/a01",bean).header("X-Accept","application/x-www-form-urlencoded").header("X-Content-Type","application/x-www-form-urlencoded").run().assertStatus(200).getContent().as(Bean.class).check();
x8.post("/a01",bean).header("X-Accept","text/openapi").header("X-Content-Type","text/openapi").run().assertStatus(200).getContent().as(Bean.class).check();
@@ -101,7 +101,7 @@ public class RestClient_Marshalls_Test extends TestBase {
x.post("/a01",bean).header("X-Accept","text/xml").header("X-Content-Type","text/xml").xml().run().assertStatus(200).getContent().as(Bean.class).check();
x.post("/a01",bean).header("X-Accept","text/html").header("X-Content-Type","text/html").html().run().assertStatus(200).getContent().as(Bean.class).check();
x.post("/a01",bean).header("X-Accept","text/plain").header("X-Content-Type","text/plain").plainText().run().assertStatus(200).getContent().as(Bean.class).check();
- x.post("/a01",bean).header("X-Accept","octal/msgpack").header("X-Content-Type","octal/msgpack").msgPack().run().assertStatus(200).getContent().as(Bean.class).check();
+ x.post("/a01",bean).header("X-Accept","application/msgpack").header("X-Content-Type","application/msgpack").msgPack().run().assertStatus(200).getContent().as(Bean.class).check();
x.post("/a01",bean).header("X-Accept","text/uon").header("X-Content-Type","text/uon").uon().run().assertStatus(200).getContent().as(Bean.class).check();
x.post("/a01",bean).header("X-Accept","application/x-www-form-urlencoded").header("X-Content-Type","application/x-www-form-urlencoded").urlEnc().run().assertStatus(200).getContent().as(Bean.class).check();
x.post("/a01",bean).header("X-Accept","text/openapi").header("X-Content-Type","text/openapi").openApi().run().assertStatus(200).getContent().as(Bean.class).check();
diff --git a/juneau-utest/src/test/java/org/apache/juneau/sse/Sse_Test.java b/juneau-utest/src/test/java/org/apache/juneau/sse/Sse_Test.java
new file mode 100644
index 0000000000..b630d5ccb0
--- /dev/null
+++ b/juneau-utest/src/test/java/org/apache/juneau/sse/Sse_Test.java
@@ -0,0 +1,728 @@
+/*
+ * 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.sse;
+
+import static org.apache.juneau.junit.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.stream.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.marshaller.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.rest.annotation.*;
+import org.apache.juneau.rest.mock.classic.*;
+import org.apache.juneau.serializer.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Comprehensive tests for the {@code org.apache.juneau.sse} package and the {@link Sse} marshaller helper.
+ *
+ * <p>
+ * Covers WHATWG spec compliance, round-trips, edge cases, iterator semantics, and a REST smoke test.
+ */
+@SuppressWarnings({
+ "java:S5961" // High assertion count acceptable in comprehensive test
+})
+class Sse_Test extends TestBase {
+
+ //------------------------------------------------------------------------------------------------------------------
+ // SseEvent bean.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test void a01_emptyConstructor() {
+ var e = new SseEvent();
+ assertBean(e, "event,data,id,retry", "<null>,<null>,<null>,<null>");
+ }
+
+ @Test void a02_twoArgConstructor() {
+ var e = new SseEvent("progress", "step 1");
+ assertBean(e, "event,data,id,retry", "progress,step 1,<null>,<null>");
+ }
+
+ @Test void a03_setters() {
+ var e = new SseEvent()
+ .setEvent("update")
+ .setData("payload")
+ .setId("42")
+ .setRetry(5000L);
+ assertBean(e, "event,data,id,retry", "update,payload,42,5000");
+ }
+
+ @Test void a04_equalsAndHashCode() {
+ var e1 = new SseEvent("e", "d").setId("1").setRetry(100L);
+ var e2 = new SseEvent("e", "d").setId("1").setRetry(100L);
+ var e3 = new SseEvent("e", "different").setId("1").setRetry(100L);
+ assertEquals(e1, e1);
+ assertEquals(e1, e2);
+ assertEquals(e1.hashCode(), e2.hashCode());
+ assertNotEquals(e1, e3);
+ assertNotEquals(e1, "not an event");
+ assertNotEquals(e1, null);
+ }
+
+ @Test void a05_toString() {
+ var e = new SseEvent("e1", "d1").setId("id1").setRetry(50L);
+ var s = e.toString();
+ assertTrue(s.contains("e1"));
+ assertTrue(s.contains("d1"));
+ assertTrue(s.contains("id1"));
+ assertTrue(s.contains("50"));
+ }
+
+ @Test void a06_defaultEventConstant() {
+ assertEquals("message", SseEvent.DEFAULT_EVENT);
+ }
+
+ @Test void a07_equalsSelfReferenceShortCircuit() {
+ // Direct equals() invocation to exercise the this==o reflexive branch — JUnit assertEquals
+ // routes through Objects.equals() which short-circuits on identity before calling equals().
+ var e = new SseEvent("e", "d").setId("1").setRetry(100L);
+ assertTrue(e.equals(e)); // NOSONAR java:S5863 — intentional reflexive equals call for branch coverage
+ }
+
+ @Test void a08_equalsDifferentEvent() {
+ var e1 = new SseEvent("a", "d").setId("1").setRetry(100L);
+ var e2 = new SseEvent("b", "d").setId("1").setRetry(100L);
+ assertNotEquals(e1, e2);
+ }
+
+ @Test void a09_equalsDifferentId() {
+ var e1 = new SseEvent("e", "d").setId("1").setRetry(100L);
+ var e2 = new SseEvent("e", "d").setId("2").setRetry(100L);
+ assertNotEquals(e1, e2);
+ }
+
+ @Test void a10_equalsDifferentRetry() {
+ var e1 = new SseEvent("e", "d").setId("1").setRetry(100L);
+ var e2 = new SseEvent("e", "d").setId("1").setRetry(200L);
+ assertNotEquals(e1, e2);
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Serializer wire format.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test void b01_singleEvent() throws Exception {
+ var e = new SseEvent("progress", "step 1").setId("1").setRetry(3000L);
+ var s = Sse.of(e);
+ assertEquals("event: progress\ndata: step 1\nid: 1\nretry: 3000\n\n", s);
+ }
+
+ @Test void b02_listOfEvents() throws Exception {
+ var events = List.of(
+ new SseEvent("progress", "one"),
+ new SseEvent("progress", "two")
+ );
+ var s = Sse.of(events);
+ assertEquals("event: progress\ndata: one\n\nevent: progress\ndata: two\n\n", s);
+ }
+
+ @Test void b03_arrayOfEvents() throws Exception {
+ var arr = new SseEvent[]{ new SseEvent("e", "a"), new SseEvent("e", "b") };
+ var s = Sse.of(arr);
+ assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+ }
+
+ @Test void b04_objectArrayOfEvents() throws Exception {
+ var arr = new Object[]{ new SseEvent("e", "a"), new SseEvent("e", "b") };
+ var s = Sse.of(arr);
+ assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+ }
+
+ @Test void b05_streamOfEvents() throws Exception {
+ var events = Stream.of(new SseEvent("e", "a"), new SseEvent("e", "b"));
+ var s = Sse.of(events);
+ assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+ }
+
+ @Test void b06_multiLineData() throws Exception {
+ var e = new SseEvent("e", "line one\nline two\nline three");
+ var s = Sse.of(e);
+ assertEquals("event: e\ndata: line one\ndata: line two\ndata: line three\n\n", s);
+ }
+
+ @Test void b07_eventNullOrEmptyOmitted() throws Exception {
+ var e1 = new SseEvent().setData("d");
+ assertEquals("data: d\n\n", Sse.of(e1));
+ var e2 = new SseEvent("", "d");
+ assertEquals("data: d\n\n", Sse.of(e2));
+ }
+
+ @Test void b08_nullDataOmitted() throws Exception {
+ var e = new SseEvent("e", null);
+ assertEquals("event: e\n\n", Sse.of(e));
+ }
+
+ @Test void b09_emptyDataEmitsEmptyDataLine() throws Exception {
+ var e = new SseEvent("e", "");
+ assertEquals("event: e\ndata: \n\n", Sse.of(e));
+ }
+
+ @Test void b10_idEmittedWhenSet() throws Exception {
+ var e = new SseEvent().setData("d").setId("123");
+ assertEquals("data: d\nid: 123\n\n", Sse.of(e));
+ }
+
+ @Test void b11_retryEmittedWhenSet() throws Exception {
+ var e = new SseEvent().setData("d").setRetry(2500L);
+ assertEquals("data: d\nretry: 2500\n\n", Sse.of(e));
+ }
+
+ @Test void b12_nullElementsInIterableSkipped() throws Exception {
+ var events = new ArrayList<SseEvent>();
+ events.add(new SseEvent("e", "a"));
+ events.add(null);
+ events.add(new SseEvent("e", "b"));
+ var s = Sse.of(events);
+ assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+ }
+
+ @Test void b13_emptyIterable() throws Exception {
+ assertEquals("", Sse.of(List.of()));
+ }
+
+ @Test void b14_emptyArray() throws Exception {
+ assertEquals("", Sse.of(new SseEvent[0]));
+ }
+
+ @Test void b15_nonEventThrows() {
+ assertThrows(SerializeException.class, () -> Sse.of("not an event"));
+ }
+
+ @Test void b16_iterableWithNonEventThrows() {
+ assertThrows(SerializeException.class, () -> Sse.of(List.of("not", "events")));
+ }
+
+ @Test void b17_nullInputReturnsEmpty() throws Exception {
+ assertEquals("", Sse.of(null));
+ }
+
+ @Test void b18_writeComment() throws Exception {
+ var sw = new StringWriter();
+ SseSerializer.writeComment(sw, "ping");
+ assertEquals(": ping\n\n", sw.toString());
+ }
+
+ @Test void b19_writeCommentNullTreatedAsEmpty() throws Exception {
+ var sw = new StringWriter();
+ SseSerializer.writeComment(sw, null);
+ assertEquals(": \n\n", sw.toString());
+ }
+
+ @Test void b20_writeCommentMultiline() throws Exception {
+ var sw = new StringWriter();
+ SseSerializer.writeComment(sw, "line one\nline two");
+ assertEquals(": line one\n: line two\n\n", sw.toString());
+ }
+
+ @Test void b21_serializeToWriter() throws Exception {
+ var sw = new StringWriter();
+ Sse.of(new SseEvent("e", "d"), sw);
+ assertEquals("event: e\ndata: d\n\n", sw.toString());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Parser / SseEventReader compliance.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test void c01_canonicalExample() throws Exception {
+ var wire = "event: progress\ndata: step 1\nid: 42\nretry: 1000\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data,id,retry", "progress,step 1,42,1000");
+ }
+
+ @Test void c02_multiLineData() throws Exception {
+ var wire = "data: line one\ndata: line two\ndata: line three\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("line one\nline two\nline three", events.get(0).getData());
+ }
+
+ @Test void c03_crLineTerminator() throws Exception {
+ var wire = "event: e\rdata: d\r\r";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data", "e,d");
+ }
+
+ @Test void c04_lfLineTerminator() throws Exception {
+ var wire = "event: e\ndata: d\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data", "e,d");
+ }
+
+ @Test void c05_crLfLineTerminator() throws Exception {
+ var wire = "event: e\r\ndata: d\r\n\r\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data", "e,d");
+ }
+
+ @Test void c06_mixedLineTerminators() throws Exception {
+ var wire = "event: e\rdata: line1\ndata: line2\r\n\r\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("line1\nline2", events.get(0).getData());
+ }
+
+ @Test void c07_singleBomStripped() throws Exception {
+ var wire = "\uFEFFevent: e\ndata: d\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("e", events.get(0).getEvent());
+ }
+
+ @Test void c08_secondBomPreserved() throws Exception {
+ var wire = "\uFEFFdata: foo\uFEFF\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("foo\uFEFF", events.get(0).getData());
+ }
+
+ @Test void c09_idWithNullByteIgnored() throws Exception {
+ var wire = "id: ab\u0000cd\ndata: x\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertNull(events.get(0).getId());
+ }
+
+ @Test void c10_retryNonDigitIgnored() throws Exception {
+ var wire = "retry: abc\ndata: x\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertNull(events.get(0).getRetry());
+ }
+
+ @Test void c11_retryEmptyIgnored() throws Exception {
+ var wire = "retry: \ndata: x\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertNull(events.get(0).getRetry());
+ }
+
+ @Test void c12_retryMixedDigitsIgnored() throws Exception {
+ var wire = "retry: 12x4\ndata: x\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertNull(events.get(0).getRetry());
+ }
+
+ @Test void c13_commentLinesIgnored() throws Exception {
+ var wire = ": ping\n: heartbeat\nevent: e\ndata: d\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data", "e,d");
+ }
+
+ @Test void c14_noColonLineSpec() throws Exception {
+ var wire = "data\ndata: real\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("\nreal", events.get(0).getData());
+ }
+
+ @Test void c15_colonSpaceHandling() throws Exception {
+ var wire = "data: value\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals(" value", events.get(0).getData());
+ }
+
+ @Test void c16_noSpaceAfterColon() throws Exception {
+ var wire = "data:value\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("value", events.get(0).getData());
+ }
+
+ @Test void c17_unknownFieldIgnored() throws Exception {
+ var wire = "unknown: x\ndata: real\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("real", events.get(0).getData());
+ }
+
+ @Test void c18_multipleEvents() throws Exception {
+ var wire = "data: one\n\ndata: two\n\ndata: three\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(3, events.size());
+ assertEquals("one", events.get(0).getData());
+ assertEquals("two", events.get(1).getData());
+ assertEquals("three", events.get(2).getData());
+ }
+
+ @Test void c19_emptyEventValueDefaultsToMessage() throws Exception {
+ var wire = "data: d\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertNull(events.get(0).getEvent());
+ }
+
+ @Test void c20_emptyEventField() throws Exception {
+ var wire = "event: \ndata: d\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("", events.get(0).getEvent());
+ }
+
+ @Test void c21_emptyIdField() throws Exception {
+ var wire = "id: \ndata: d\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("", events.get(0).getId());
+ }
+
+ @Test void c22_inputWithoutTrailingBlankLine() throws Exception {
+ var wire = "event: e\ndata: d\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data", "e,d");
+ }
+
+ @Test void c23_pureBlankLinesProduceNoEvents() throws Exception {
+ var wire = "\n\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertTrue(events.isEmpty());
+ }
+
+ @Test void c24_emptyInput() throws Exception {
+ List<SseEvent> events = Sse.to("", List.class, SseEvent.class);
+ assertTrue(events.isEmpty());
+ }
+
+ @Test void c25_parseAsSingleEvent() throws Exception {
+ var wire = "event: e\ndata: d\n\n";
+ var e = Sse.to(wire, SseEvent.class);
+ assertNotNull(e);
+ assertBean(e, "event,data", "e,d");
+ }
+
+ @Test void c26_parseAsSingleEventEmptyInput() throws Exception {
+ var e = Sse.to("", SseEvent.class);
+ assertNull(e);
+ }
+
+ @Test void c27_parseAsArray() throws Exception {
+ var wire = "data: one\n\ndata: two\n\n";
+ var events = Sse.to(wire, SseEvent[].class);
+ assertEquals(2, events.length);
+ assertEquals("one", events[0].getData());
+ assertEquals("two", events[1].getData());
+ }
+
+ @Test void c28_parseUnsupportedTargetTypeThrows() {
+ var wire = "data: x\n\n";
+ assertThrows(ParseException.class, () -> Sse.to(wire, Integer.class));
+ }
+
+ @Test void c29_emptyDataFieldEmitsEmptyString() throws Exception {
+ // "data:" with no value — exercises processField's fieldValue.isEmpty() branch.
+ var wire = "data:\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertEquals("", events.get(0).getData());
+ }
+
+ @Test void c30_retryLowAsciiNonDigitIgnored() throws Exception {
+ // '!' (0x21) is below '0' — exercises parseRetry's c < '0' branch.
+ var wire = "retry: !23\ndata: x\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertNull(events.get(0).getRetry());
+ }
+
+ @Test void c31_eventWithoutDataFieldDispatches() throws Exception {
+ // Event with metadata fields but no data — exercises dispatch's dataBuf.length() == 0 branch.
+ var wire = "event: e\nid: 1\n\n";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data,id,retry", "e,<null>,1,<null>");
+ }
+
+ @Test void c32_inputEndsMidLineWithoutTerminator() throws Exception {
+ // Stream ends without a final \n — exercises readLine's sawAny=true at EOF branch.
+ var wire = "event: e\ndata: d";
+ List<SseEvent> events = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(1, events.size());
+ assertBean(events.get(0), "event,data", "e,d");
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // SseEventReader iterator semantics.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test void d01_iteratorDrainsOneAtATime() throws Exception {
+ var wire = "data: a\n\ndata: b\n\ndata: c\n\n";
+ try (var r = new SseEventReader(new StringReader(wire))) {
+ assertTrue(r.hasNext());
+ assertEquals("a", r.next().getData());
+ assertTrue(r.hasNext());
+ assertEquals("b", r.next().getData());
+ assertTrue(r.hasNext());
+ assertEquals("c", r.next().getData());
+ assertFalse(r.hasNext());
+ }
+ }
+
+ @Test void d02_nextWithoutHasNext() throws Exception {
+ var wire = "data: a\n\n";
+ try (var r = new SseEventReader(new StringReader(wire))) {
+ assertEquals("a", r.next().getData());
+ }
+ }
+
+ @Test void d03_hasNextIdempotent() throws Exception {
+ var wire = "data: a\n\n";
+ try (var r = new SseEventReader(new StringReader(wire))) {
+ assertTrue(r.hasNext());
+ assertTrue(r.hasNext());
+ assertTrue(r.hasNext());
+ assertEquals("a", r.next().getData());
+ assertFalse(r.hasNext());
+ assertFalse(r.hasNext());
+ }
+ }
+
+ @Test void d04_nextThrowsNoSuchElement() throws Exception {
+ try (var r = new SseEventReader(new StringReader(""))) {
+ assertThrows(NoSuchElementException.class, r::next);
+ }
+ }
+
+ @Test void d05_nullReaderRejected() {
+ assertThrows(IllegalArgumentException.class, () -> new SseEventReader(null));
+ }
+
+ @Test void d06_closeClosesUnderlyingReader() throws Exception {
+ var closed = new boolean[1];
+ var sr = new StringReader("data: a\n\n") {
+ @Override public void close() { closed[0] = true; super.close(); }
+ };
+ var r = new SseEventReader(sr);
+ r.close();
+ assertTrue(closed[0]);
+ }
+
+ @Test void d07_toListClosesUnderlyingReader() throws Exception {
+ var closed = new boolean[1];
+ var sr = new StringReader("data: a\n\ndata: b\n\n") {
+ @Override public void close() { closed[0] = true; super.close(); }
+ };
+ var r = new SseEventReader(sr);
+ var events = r.toList();
+ assertEquals(2, events.size());
+ assertTrue(closed[0]);
+ }
+
+ @Test void d08_ioExceptionWrapped() {
+ var failingReader = new Reader() {
+ @Override public int read(char[] cbuf, int off, int len) throws IOException { throw new IOException("boom"); }
+ @Override public void close() {}
+ };
+ var r = new SseEventReader(failingReader);
+ var ex = assertThrows(UncheckedIOException.class, r::hasNext);
+ assertEquals("boom", ex.getCause().getMessage());
+ }
+
+ @Test void d09_toListPropagatesIo() {
+ var failingReader = new Reader() {
+ @Override public int read(char[] cbuf, int off, int len) throws IOException { throw new IOException("boom"); }
+ @Override public void close() {}
+ };
+ var r = new SseEventReader(failingReader);
+ var ex = assertThrows(IOException.class, r::toList);
+ assertEquals("boom", ex.getMessage());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Round-trip via Sse marshaller.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test void e01_roundTripList() throws Exception {
+ var events = List.of(
+ new SseEvent("progress", "step 1").setId("1"),
+ new SseEvent("progress", "step 2").setId("2").setRetry(1000L)
+ );
+ var wire = Sse.of(events);
+ List<SseEvent> parsed = Sse.to(wire, List.class, SseEvent.class);
+ assertEquals(2, parsed.size());
+ assertBean(parsed.get(0), "event,data,id,retry", "progress,step 1,1,<null>");
+ assertBean(parsed.get(1), "event,data,id,retry", "progress,step 2,2,1000");
+ }
+
+ @Test void e02_roundTripMultiLine() throws Exception {
+ var e = new SseEvent("e", "line a\nline b");
+ var wire = Sse.of(e);
+ var parsed = Sse.to(wire, SseEvent.class);
+ assertEquals("line a\nline b", parsed.getData());
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // Defaults / context.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Test void f01_serializerMediaType() {
+ assertEquals("text/event-stream", SseSerializer.DEFAULT.getResponseContentType().toString());
+ }
+
+ @Test void f02_serializerCopy() {
+ var copy = SseSerializer.DEFAULT.copy().build();
+ assertNotNull(copy);
+ }
+
+ @Test void f03_parserCopy() {
+ var copy = SseParser.DEFAULT.copy().build();
+ assertNotNull(copy);
+ }
+
+ @Test void f04_serializerGetSession() {
+ assertNotNull(SseSerializer.DEFAULT.getSession());
+ }
+
+ @Test void f05_parserGetSession() {
+ assertNotNull(SseParser.DEFAULT.getSession());
+ }
+
+ @Test void f06_marshallerInstanceMethods() throws Exception {
+ var m = Sse.DEFAULT;
+ var s = m.write(new SseEvent("e", "d"));
+ assertEquals("event: e\ndata: d\n\n", s);
+ }
+
+ @Test void f07_explicitConstructor() throws Exception {
+ var m = new Sse(SseSerializer.DEFAULT, SseParser.DEFAULT);
+ assertNotNull(m);
+ assertEquals("event: e\ndata: d\n\n", m.write(new SseEvent("e", "d")));
+ }
+
+ @Test void f08_builderHashKeyStable() {
+ var b1 = SseSerializer.create();
+ var b2 = SseSerializer.create();
+ assertEquals(b1.hashKey(), b2.hashKey());
+ var pb1 = SseParser.create();
+ var pb2 = SseParser.create();
+ assertEquals(pb1.hashKey(), pb2.hashKey());
+ }
+
+ @Test void f09_splitDataKeepsTrailingEmpties() {
+ var parts = SseSerializerSession.splitData("a\nb\n");
+ assertEquals(3, parts.size());
+ assertEquals("a", parts.get(0));
+ assertEquals("b", parts.get(1));
+ assertEquals("", parts.get(2));
+ }
+
+ @Test void f10_builderCopyConstructorParser() {
+ var b1 = SseParser.create();
+ var b2 = b1.copy();
+ assertNotNull(b2);
+ assertNotSame(b1, b2);
+ }
+
+ @Test void f11_builderCopyConstructorSerializer() {
+ var b1 = SseSerializer.create();
+ var b2 = b1.copy();
+ assertNotNull(b2);
+ assertNotSame(b1, b2);
+ }
+
+ @Test void f12_arrayWithNullElement() throws Exception {
+ var arr = new SseEvent[]{ new SseEvent("e", "a"), null, new SseEvent("e", "b") };
+ // SseEvent[] direct array path: writeEvent(null) returns early.
+ var s = Sse.of(arr);
+ assertEquals("event: e\ndata: a\n\nevent: e\ndata: b\n\n", s);
+ }
+
+ @Test void f13_streamWithNonEventWrapsInRuntime() {
+ // Stream path: writeIfEventUnchecked wraps SerializeException as RuntimeException.
+ var s = Stream.of("not an event");
+ assertThrows(RuntimeException.class, () -> Sse.of(s));
+ }
+
+ @Test void f14_parseAsObjectClass() throws Exception {
+ // Target type Object.class hits the type.isObject() branch.
+ var wire = "data: x\n\n";
+ var out = Sse.to(wire, Object.class);
+ assertNotNull(out);
+ assertTrue(out instanceof List);
+ }
+
+ @Test void f15_parseAsRawList() throws Exception {
+ // Raw List.class without args hits the "List.class.isAssignableFrom" fallback.
+ var wire = "data: x\n\n";
+ var out = Sse.to(wire, (Class<?>) List.class);
+ assertNotNull(out);
+ assertTrue(out instanceof List);
+ }
+
+ @Test void f16_parseAsSseEventEmptyReturnsNull() throws Exception {
+ // Empty input + target SseEvent.class hits the events.isEmpty() branch.
+ var e = Sse.to("\n\n", SseEvent.class);
+ assertNull(e);
+ }
+
+ @Test void f17_parseNullInputReturnsNullViaPublicApi() throws Exception {
+ // Null input -> ParserPipe.getReader() returns null -> doParse hits the r==null early-return branch.
+ var result = Sse.to((Object) null, SseEvent.class);
+ assertNull(result);
+ }
+
+ @Test void f18_doParseWithNullClassMetaReturnsList() throws Exception {
+ // Public parse() path requires non-null type (parseInner calls type.isVoid()), so the
+ // type==null branch in SseParserSession.doParse() is only reachable via direct invocation.
+ // Same package -> protected doParse is accessible.
+ var session = SseParser.DEFAULT.getSession();
+ try (var pipe = session.createPipe("data: x\n\n")) {
+ Object result = session.doParse(pipe, null);
+ assertTrue(result instanceof List);
+ var events = (List<?>) result;
+ assertEquals(1, events.size());
+ assertEquals("x", ((SseEvent) events.get(0)).getData());
+ }
+ }
+
+ //------------------------------------------------------------------------------------------------------------------
+ // REST smoke test.
+ //------------------------------------------------------------------------------------------------------------------
+
+ @Rest(serializers = SseSerializer.class)
+ public static class SseEndpoint {
+ @RestGet(path = "/stream")
+ public List<SseEvent> stream() {
+ return List.of(
+ new SseEvent("progress", "step 1").setId("1"),
+ new SseEvent("progress", "step 2").setId("2"),
+ new SseEvent("progress", "step 3").setId("3")
+ );
+ }
+ }
+
+ @Test void g01_restEndpointEmitsAllEventsInOrder() throws Exception {
+ var c = MockRestClient.buildLax(SseEndpoint.class);
+ var body = c.get("/stream").header("Accept", "text/event-stream").run().getContent().asString();
+ var expected =
+ "event: progress\ndata: step 1\nid: 1\n\n"
+ + "event: progress\ndata: step 2\nid: 2\n\n"
+ + "event: progress\ndata: step 3\nid: 3\n\n";
+ assertEquals(expected, body);
+ }
+}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java b/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java
index 09040836c3..05b703d2b9 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/transforms/ReaderObjectSwap_ComboSerializeTest.java
@@ -132,8 +132,8 @@ public class ReaderObjectSwap_ComboSerializeTest extends ComboSerializeTest_Base
.urlEnc("foo-application/x-www-form-urlencoded")
.urlEncT("foo-application/x-www-form-urlencoded")
.urlEncR("foo-application/x-www-form-urlencoded")
- .msgPack("666F6F2D6F6374616C2F6D73677061636B")
- .msgPackT("666F6F2D6F6374616C2F6D73677061636B")
+ .msgPack("666F6F2D6170706C69636174696F6E2F6D73677061636B")
+ .msgPackT("666F6F2D6170706C69636174696F6E2F6D73677061636B")
.build(),
tester(5, "BeanWithSwappedField", ()->new BeanWithSwappedField("x"))
.json("{\"f\":x-json}")
diff --git a/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java b/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java
index e5e14755de..d8582c5360 100644
--- a/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/transforms/SwapsAnnotation_ComboSerializeTest.java
@@ -428,7 +428,7 @@ class SwapsAnnotation_ComboSerializeTest extends ComboSerializeTest_Base {
@Swap(value=SwapHtml.class, mediaTypes={"text/html"})
@Swap(value=SwapUon.class, mediaTypes={"text/uon"})
@Swap(value=SwapUrlEncoding.class, mediaTypes={"application/x-www-form-urlencoded"})
- @Swap(value=SwapMsgPack.class, mediaTypes={"octal/msgpack"})
+ @Swap(value=SwapMsgPack.class, mediaTypes={"application/msgpack"})
@Swap(value=SwapRdfXml.class, mediaTypes={"text/xml+rdf"})
public static class TestMediaTypeLiterals {}
diff --git a/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java b/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java
index 1dbff17c94..7a7c61ceab 100755
--- a/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java
+++ b/juneau-utest/src/test/java/org/apache/juneau/uon/UonSerializer_Test.java
@@ -43,26 +43,26 @@ class UonSerializer_Test {
assertEquals("a", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{a:'a'}");
+ t = Json5Map.ofString("{a:'a'}");
assertEquals("(a=a)", s.serialize(t));
assertEquals("(a=a)", su.serialize(t));
assertEquals("(\n\ta=a\n)", sr.serialize(t));
// Simple map
// Top level
- t = Json5Map.ofText("{a:'b',c:123,d:false,e:true,f:null}");
+ t = Json5Map.ofString("{a:'b',c:123,d:false,e:true,f:null}");
assertEquals("(a=b,c=123,d=false,e=true,f=null)", s.serialize(t));
assertEquals("(a=b,c=123,d=false,e=true,f=null)", su.serialize(t));
assertEquals("(\n\ta=b,\n\tc=123,\n\td=false,\n\te=true,\n\tf=null\n)", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{a:{a:'b',c:123,d:false,e:true,f:null}}");
+ t = Json5Map.ofString("{a:{a:'b',c:123,d:false,e:true,f:null}}");
assertEquals("(a=(a=b,c=123,d=false,e=true,f=null))", s.serialize(t));
assertEquals("(a=(a=b,c=123,d=false,e=true,f=null))", su.serialize(t));
assertEquals("(\n\ta=(\n\t\ta=b,\n\t\tc=123,\n\t\td=false,\n\t\te=true,\n\t\tf=null\n\t)\n)", sr.serialize(t));
// Simple map with primitives as literals
- t = Json5Map.ofText("{a:'b',c:'123',d:'false',e:'true',f:'null'}");
+ t = Json5Map.ofString("{a:'b',c:'123',d:'false',e:'true',f:'null'}");
assertEquals("(a=b,c='123',d='false',e='true',f='null')", s.serialize(t));
assertEquals("(a=b,c='123',d='false',e='true',f='null')", su.serialize(t));
assertEquals("(\n\ta=b,\n\tc='123',\n\td='false',\n\te='true',\n\tf='null'\n)", sr.serialize(t));
@@ -76,13 +76,13 @@ class UonSerializer_Test {
assertEquals("null", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{null:null}");
+ t = Json5Map.ofString("{null:null}");
assertEquals("(null=null)", s.serialize(t));
assertEquals("(null=null)", su.serialize(t));
assertEquals("(\n\tnull=null\n)", sr.serialize(t));
// 3rd level
- t = Json5Map.ofText("{null:{null:null}}");
+ t = Json5Map.ofString("{null:{null:null}}");
assertEquals("(null=(null=null))", s.serialize(t));
assertEquals("(null=(null=null))", su.serialize(t));
assertEquals("(\n\tnull=(\n\t\tnull=null\n\t)\n)", sr.serialize(t));
@@ -95,7 +95,7 @@ class UonSerializer_Test {
assertEquals("@()", sr.serialize(t));
// 2nd level in map
- t = Json5Map.ofText("{x:[]}");
+ t = Json5Map.ofString("{x:[]}");
assertEquals("(x=@())", s.serialize(t));
assertEquals("(x=@())", su.serialize(t));
assertEquals("(\n\tx=@()\n)", sr.serialize(t));
@@ -114,7 +114,7 @@ class UonSerializer_Test {
assertEquals("@(\n\t''\n)", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{x:['']}");
+ t = Json5Map.ofString("{x:['']}");
assertEquals("(x=@(''))", s.serialize(t));
assertEquals("(x=@(''))", su.serialize(t));
assertEquals("(\n\tx=@(\n\t\t''\n\t)\n)", sr.serialize(t));
@@ -133,7 +133,7 @@ class UonSerializer_Test {
assertEquals("\u0000", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{'\u0000':'\u0000'}");
+ t = Json5Map.ofString("{'\u0000':'\u0000'}");
assertEquals("(%00=%00)", s.serialize(t));
assertEquals("(\u0000=\u0000)", su.serialize(t));
assertEquals("(\n\t\u0000=\u0000\n)", sr.serialize(t));
@@ -146,7 +146,7 @@ class UonSerializer_Test {
assertEquals("false", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{x:false}");
+ t = Json5Map.ofString("{x:false}");
assertEquals("(x=false)", s.serialize(t));
assertEquals("(x=false)", su.serialize(t));
assertEquals("(\n\tx=false\n)", sr.serialize(t));
@@ -159,7 +159,7 @@ class UonSerializer_Test {
assertEquals("123", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{x:123}");
+ t = Json5Map.ofString("{x:123}");
assertEquals("(x=123)", s.serialize(t));
assertEquals("(x=123)", su.serialize(t));
assertEquals("(\n\tx=123\n)", sr.serialize(t));
@@ -172,7 +172,7 @@ class UonSerializer_Test {
assertEquals("x;/?:@-_.!*~'", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{x:'x;/?:@-_.!*\\''}");
+ t = Json5Map.ofString("{x:'x;/?:@-_.!*\\''}");
assertEquals("(x=x;/?:@-_.!*~')", s.serialize(t));
assertEquals("(x=x;/?:@-_.!*~')", su.serialize(t));
assertEquals("(\n\tx=x;/?:@-_.!*~'\n)", sr.serialize(t));
@@ -185,7 +185,7 @@ class UonSerializer_Test {
assertEquals("x{}|\\^[]`<>#%\"&+", sr.serialize(t));
// 2nd level
- t = Json5Map.ofText("{'x{}|\\\\^[]`<>#%\"&+':'x{}|\\\\^[]`<>#%\"&+'}");
+ t = Json5Map.ofString("{'x{}|\\\\^[]`<>#%\"&+':'x{}|\\\\^[]`<>#%\"&+'}");
assertEquals("(x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B=x%7B%7D%7C%5C%5E%5B%5D%60%3C%3E%23%25%22%26%2B)", s.serialize(t));
assertEquals("(x{}|\\^[]`<>#%\"&+=x{}|\\^[]`<>#%\"&+)", su.serialize(t));
assertEquals("(\n\tx{}|\\^[]`<>#%\"&+=x{}|\\^[]`<>#%\"&+\n)", sr.serialize(t));
@@ -199,14 +199,14 @@ class UonSerializer_Test {
// 2nd level
// Note behavior on serializeParams() is different since 2nd-level is top level.
- t = Json5Map.ofText("{'x$,()~':'x$,()~'}");
+ t = Json5Map.ofString("{'x$,()~':'x$,()~'}");
assertEquals("('x$,()~~'='x$,()~~')", s.serialize(t));
assertEquals("('x$,()~~'='x$,()~~')", su.serialize(t));
assertEquals("(\n\t'x$,()~~'='x$,()~~'\n)", sr.serialize(t));
// 3rd level
// Note behavior on serializeParams().
... 1451 lines suppressed ...